JavaScript · 16138 bytes Raw Blame History
1 // rendering.js - All visual rendering and particle effects
2
3 // ============= PARTICLE SYSTEM =============
4 let particles = [];
5
6 function createImpactParticles(x, y, velX, velY) {
7 for (let i = 0; i < IMPACT_PARTICLES; i++) {
8 let angle = Math.random() * Math.PI * 2;
9 let speed = Math.random() * 6 + 2;
10 let size = Math.random() * 4 + 2;
11
12 particles.push({
13 x: x + (Math.random() - 0.5) * 10,
14 y: y + (Math.random() - 0.5) * 10,
15 vx: Math.cos(angle) * speed - velX * 0.2,
16 vy: Math.sin(angle) * speed - velY * 0.2,
17 size: size,
18 life: PARTICLE_LIFE,
19 maxLife: PARTICLE_LIFE,
20 color: { r: 255, g: Math.random() * 155 + 100, b: Math.random() * 50 + 100 },
21 type: 'impact'
22 });
23 }
24 }
25
26 function createSpringParticles(springPos, compression) {
27 if (Math.random() < SPRING_PARTICLE_RATE * compression) {
28 let angle = Math.random() * Math.PI * 2;
29 let speed = (Math.random() * 2 + 1) * compression;
30
31 particles.push({
32 x: springPos.x + (Math.random() - 0.5) * 20,
33 y: springPos.y + (Math.random() - 0.5) * 20,
34 vx: Math.cos(angle) * speed,
35 vy: Math.sin(angle) * speed,
36 size: Math.random() * 2 + 1,
37 life: PARTICLE_LIFE * 0.5,
38 maxLife: PARTICLE_LIFE * 0.5,
39 color: { r: 0, g: 255, b: 136 },
40 type: 'spring'
41 });
42 }
43 }
44
45 function updateParticles() {
46 for (let i = particles.length - 1; i >= 0; i--) {
47 let p = particles[i];
48
49 p.x += p.vx;
50 p.y += p.vy;
51 p.vx *= 0.98;
52 p.vy *= 0.98;
53 p.life--;
54
55 if (p.life <= 0) {
56 particles.splice(i, 1);
57 }
58 }
59
60 if (particles.length > MAX_PARTICLES) {
61 particles.splice(0, particles.length - MAX_PARTICLES);
62 }
63 }
64
65 function drawParticles() {
66 for (let p of particles) {
67 let alpha = map(p.life, 0, p.maxLife, 0, 255);
68
69 push();
70 translate(p.x, p.y);
71
72 if (p.type === 'impact') {
73 fill(p.color.r, p.color.g, p.color.b, alpha);
74 noStroke();
75 ellipse(0, 0, p.size, p.size);
76
77 fill(p.color.r, p.color.g, p.color.b, alpha * 0.3);
78 ellipse(0, 0, p.size * 2, p.size * 2);
79 } else if (p.type === 'spring') {
80 fill(p.color.r, p.color.g, p.color.b, alpha);
81 noStroke();
82 ellipse(0, 0, p.size, p.size);
83 }
84
85 pop();
86 }
87 }
88
89 // ============= PADDLE RENDERING =============
90 function drawPaddlesWithGlow(ball, leftPaddle, rightPaddle,
91 bopState, aiEnabled, aiState, millis) {
92 let ballPos = ball.position;
93 let leftDist = dist(ballPos.x, ballPos.y, leftPaddle.position.x, leftPaddle.position.y);
94 let rightDist = dist(ballPos.x, ballPos.y, rightPaddle.position.x, rightPaddle.position.y);
95
96 drawSinglePaddleEnhanced(leftPaddle, leftDist, true, false,
97 bopState, aiEnabled, aiState, millis);
98 drawSinglePaddleEnhanced(rightPaddle, rightDist, false, aiEnabled,
99 bopState, aiEnabled, aiState, millis);
100 }
101
102 function drawSinglePaddleEnhanced(paddle, ballDistance, isLeft, isAI,
103 bopState, aiEnabled, aiState, currentMillis) {
104 let pos = paddle.position;
105 let angle = paddle.angle;
106
107 let glowIntensity = map(ballDistance, 0, PADDLE_GLOW_DISTANCE, 150, 0);
108 glowIntensity = constrain(glowIntensity, 0, 150);
109
110 // Add bop glow effect
111 let bopGlow = 0;
112 if (isLeft && bopState.left.active) {
113 let bopProgress = (currentMillis - bopState.left.startTime) / bopState.left.duration;
114 bopGlow = (1 - bopProgress) * 100;
115 } else if (!isLeft && bopState.right.active) {
116 let bopProgress = (currentMillis - bopState.right.startTime) / bopState.right.duration;
117 bopGlow = (1 - bopProgress) * 100;
118 }
119
120 glowIntensity += bopGlow;
121
122 // Add AI state-based effects
123 if (isAI) {
124 if (aiState.mode === 'WINDING_UP') {
125 glowIntensity += 50;
126 } else if (aiState.mode === 'SWINGING') {
127 glowIntensity += 100;
128 }
129 glowIntensity += aiState.aggressionLevel * 30;
130 }
131
132 push();
133 translate(pos.x, pos.y);
134 rotate(angle);
135
136 // Color schemes
137 let paddleColor = isAI ? [255, 100, 100] : [0, 255, 136];
138
139 if (isAI && aiState.mode === 'WINDING_UP') {
140 paddleColor = [255, 150, 50];
141 } else if (isAI && aiState.mode === 'SWINGING') {
142 paddleColor = [255, 50, 50];
143 }
144
145 // Bop color override
146 if ((isLeft && bopState.left.active) || (!isLeft && bopState.right.active)) {
147 paddleColor = [255, 255, 100];
148
149 if (isAI && bopState.right.active) {
150 paddleColor = [255, 50, 255];
151 glowIntensity = Math.min(255, glowIntensity + 50);
152 }
153 }
154
155 // Draw glow effect
156 if (glowIntensity > 0) {
157 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.6);
158 noStroke();
159 rectMode(CENTER);
160 rect(0, 0, PADDLE_WIDTH + 12, PADDLE_HEIGHT + 12);
161
162 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.3);
163 rect(0, 0, PADDLE_WIDTH + 20, PADDLE_HEIGHT + 20);
164 }
165
166 // Draw main paddle
167 fill(paddleColor[0], paddleColor[1], paddleColor[2]);
168 stroke(paddleColor[0], paddleColor[1], paddleColor[2], 220 + glowIntensity * 0.5);
169 strokeWeight(3);
170 rectMode(CENTER);
171 rect(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT);
172
173 // Core highlight
174 if (isAI) {
175 fill(255, 200, 200, 100);
176 } else {
177 fill(150, 255, 200, 100);
178 }
179 noStroke();
180 rect(0, 0, PADDLE_WIDTH - 4, PADDLE_HEIGHT - 4);
181
182 pop();
183 }
184
185 // ============= SPRING RENDERING =============
186 function drawSpringsEnhanced(leftSupport, leftPaddle, rightSupport, rightPaddle) {
187 let leftSupportPos = leftSupport.position;
188 let leftPaddlePos = leftPaddle.position;
189 let leftCompression = drawSpringLineEnhanced(leftSupportPos, leftPaddlePos);
190 createSpringParticles(leftPaddlePos, leftCompression);
191
192 let rightSupportPos = rightSupport.position;
193 let rightPaddlePos = rightPaddle.position;
194 let rightCompression = drawSpringLineEnhanced(rightSupportPos, rightPaddlePos);
195 createSpringParticles(rightPaddlePos, rightCompression);
196 }
197
198 function drawSpringLineEnhanced(startPos, endPos) {
199 let segments = 12;
200 let amplitude = 10;
201
202 let currentLength = dist(startPos.x, startPos.y, endPos.x, endPos.y);
203 let compression = SPRING_LENGTH / currentLength;
204 amplitude *= compression;
205
206 let glowIntensity = 150 + compression * SPRING_GLOW_INTENSITY;
207 stroke(0, 255, 136, glowIntensity);
208 strokeWeight(3 + compression * 2);
209
210 beginShape();
211 noFill();
212
213 for (let i = 0; i <= segments; i++) {
214 let t = i / segments;
215 let x = lerp(startPos.x, endPos.x, t);
216 let y = lerp(startPos.y, endPos.y, t);
217
218 if (i > 0 && i < segments) {
219 let perpX = -(endPos.y - startPos.y) / currentLength;
220 let perpY = (endPos.x - startPos.x) / currentLength;
221 let offset = sin(i * PI * 1.5) * amplitude;
222 x += perpX * offset;
223 y += perpY * offset;
224 }
225
226 vertex(x, y);
227 }
228
229 endShape();
230
231 // Glow effect
232 let pulse = sin(frameCount * 0.1) * 0.2 + 1;
233 stroke(0, 255, 136, glowIntensity * 0.4 * pulse);
234 strokeWeight(8 + compression * 3);
235 beginShape();
236 noFill();
237
238 for (let i = 0; i <= segments; i++) {
239 let t = i / segments;
240 let x = lerp(startPos.x, endPos.x, t);
241 let y = lerp(startPos.y, endPos.y, t);
242 vertex(x, y);
243 }
244
245 endShape();
246
247 return compression;
248 }
249
250 // ============= SUPPORT POINTS RENDERING =============
251 function drawSupportPointsEnhanced(leftSupport, rightSupport, inputBuffer) {
252 let leftActivity = Math.abs(inputBuffer.left) * 255;
253 let rightActivity = Math.abs(inputBuffer.right) * 255;
254
255 let leftPulse = sin(frameCount * 0.2) * 0.3 + 1;
256 fill(0, 255, 136, 100 + leftActivity * 0.6);
257 noStroke();
258 ellipse(leftSupport.position.x, leftSupport.position.y,
259 (8 + leftActivity * 0.15) * leftPulse,
260 (8 + leftActivity * 0.15) * leftPulse);
261
262 let rightPulse = sin(frameCount * 0.2 + PI) * 0.3 + 1;
263 fill(0, 255, 136, 100 + rightActivity * 0.6);
264 ellipse(rightSupport.position.x, rightSupport.position.y,
265 (8 + rightActivity * 0.15) * rightPulse,
266 (8 + rightActivity * 0.15) * rightPulse);
267 }
268
269 // ============= BALL RENDERING =============
270 function drawBallEnhanced(ball) {
271 let ballPos = ball.position;
272 let ballVel = ball.velocity;
273 let speed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
274
275 let speedIntensity = map(speed, 0, 15, 50, 255);
276
277 // Trail effect
278 for (let i = 0; i < 3; i++) {
279 let offset = i * 3;
280 fill(255, 100, 100, 40 - i * 10);
281 noStroke();
282 ellipse(ballPos.x - ballVel.x * offset * 0.1,
283 ballPos.y - ballVel.y * offset * 0.1,
284 BALL_RADIUS * (4 - i), BALL_RADIUS * (4 - i));
285 }
286
287 // Main ball
288 fill(255, 100, 100);
289 stroke(255, 200, 200, speedIntensity);
290 strokeWeight(3 + speed * 0.15);
291 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 2, BALL_RADIUS * 2);
292
293 // Speed core
294 if (speed > 8) {
295 fill(255, 255, 255, speedIntensity * 0.8);
296 noStroke();
297 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 0.8, BALL_RADIUS * 0.8);
298 }
299
300 // Energy ring
301 if (speed > 12) {
302 noFill();
303 stroke(255, 255, 255, speedIntensity * 0.5);
304 strokeWeight(2);
305 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 3, BALL_RADIUS * 3);
306 }
307 }
308
309 // ============= UI RENDERING =============
310 function drawBoundaries() {
311 stroke(0, 255, 136, 30);
312 strokeWeight(1);
313 noFill();
314 line(0, 0, width, 0);
315 line(0, height, width, height);
316 }
317
318 function drawCenterLine() {
319 stroke(0, 255, 136, 50);
320 strokeWeight(2);
321
322 for (let y = 0; y < height; y += 20) {
323 line(width/2, y, width/2, y + 10);
324 }
325 }
326
327 function drawDebugInfo(ball, leftSupport, leftPaddle, rightSupport, rightPaddle,
328 inputBuffer, particles, gameMode, aiState, bopState, aiEnabled) {
329 fill(255, 100);
330 textAlign(LEFT);
331 textSize(12);
332 text(`FPS: ${Math.round(frameRate())}`, 10, 20);
333 text(`Ball Speed: ${Math.round(getBallSpeed(ball))}`, 10, 35);
334 text(`Particles: ${particles.length}`, 10, 50);
335 text(`Mode: ${gameMode} | Difficulty: ${aiState.difficulty}`, 10, 65);
336
337 // Spring info
338 let leftSpringLength = dist(leftSupport.position.x, leftSupport.position.y,
339 leftPaddle.position.x, leftPaddle.position.y);
340 let rightSpringLength = dist(rightSupport.position.x, rightSupport.position.y,
341 rightPaddle.position.x, rightPaddle.position.y);
342
343 text(`L Spring: ${Math.round(leftSpringLength)}px (${((SPRING_LENGTH/leftSpringLength - 1) * 100).toFixed(0)}%)`, 10, 80);
344 text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 95);
345 text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 110);
346
347 // AI debug info
348 if (aiEnabled) {
349 text(`AI State: ${aiState.mode} | Aggression: ${aiState.aggressionLevel.toFixed(2)}`, 10, 125);
350 text(`Target: ${Math.round(aiState.targetY)} | Intercept: ${Math.round(aiState.interceptY)}`, 10, 140);
351 text(`Ball: (${Math.round(ball.position.x)}, ${Math.round(ball.position.y)}) Vel: (${ball.velocity.x.toFixed(1)}, ${ball.velocity.y.toFixed(1)})`, 10, 155);
352
353 if (aiState.mode === 'WINDING_UP') {
354 fill(255, 150, 50, 200);
355 text("🔄 AI WINDING UP FOR POWER SHOT", 10, 175);
356 } else if (aiState.mode === 'SWINGING') {
357 fill(255, 50, 50, 200);
358 text("⚡ AI POWER SWING!", 10, 175);
359 } else if (aiState.consideringBop) {
360 fill(255, 255, 100, 200);
361 text("💥 AI PREPARING BOP!", 10, 175);
362 }
363
364 if (bopState.right.active) {
365 fill(255, 255, 0, 255);
366 text("🚀 AI BOPPING!", 10, 190);
367 }
368 }
369 }
370
371 function drawStartMessage(aiEnabled, aiDifficulty) {
372 fill(0, 255, 136, 200);
373 textAlign(CENTER);
374 textSize(20);
375 text("Press any key to start!", width/2, height/2 + 100);
376 textSize(14);
377
378 if (aiEnabled) {
379 text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
380 text(`AI Difficulty: ${aiDifficulty.toUpperCase()}`, width/2, height/2 + 145);
381 } else {
382 text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
383 }
384
385 textSize(12);
386 fill(0, 255, 136, 120);
387 text("Press ESC to return to menu", width/2, height/2 + 170);
388 }
389
390 // ============= MENU RENDERING =============
391 function drawMenu(menuState) {
392 drawMenuBackground();
393
394 // Title
395 push();
396 let titlePulse = sin(frameCount * 0.05) * 0.2 + 1;
397 fill(0, 255, 136);
398 textAlign(CENTER);
399 textSize(60 * titlePulse);
400 text("SPRONG", width/2, 120);
401
402 fill(0, 255, 136, 150);
403 textSize(16);
404 text("Physics-based Pong with Spring Paddles", width/2, 150);
405 pop();
406
407 // Menu options
408 let startY = height/2 - 20;
409 let spacing = 60;
410
411 for (let i = 0; i < menuState.options.length; i++) {
412 let y = startY + i * spacing;
413 let isSelected = i === menuState.selectedOption;
414
415 if (isSelected) {
416 push();
417 let pulse = sin(frameCount * 0.15) * 0.3 + 1;
418 fill(0, 255, 136, 100 * pulse);
419 noStroke();
420 rectMode(CENTER);
421 rect(width/2, y, 300, 45);
422 pop();
423 }
424
425 fill(isSelected ? 255 : 200);
426 textAlign(CENTER);
427 textSize(isSelected ? 24 : 20);
428 text(menuState.options[i], width/2, y + 8);
429
430 if (i === 0 && isSelected && menuState.showDifficulty) {
431 fill(0, 255, 136, 180);
432 textSize(14);
433 text(`Difficulty: ${menuState.difficulties[menuState.difficultySelected]}`, width/2, y + 28);
434 text("(Use ← → to change)", width/2, y + 45);
435 }
436 }
437
438 // Instructions
439 fill(0, 255, 136, 120);
440 textAlign(CENTER);
441 textSize(14);
442 text("Use ↑↓ to select, ENTER to confirm", width/2, height - 80);
443 text("or click/touch to select", width/2, height - 60);
444
445 textSize(12);
446 fill(255, 100);
447 if (menuState.selectedOption === 0) {
448 text("Controls: W/S keys + LEFT SHIFT (bop) or Mouse/Touch", width/2, height - 30);
449 } else {
450 text("Controls: P1 (W/S + L.Shift) | P2 (↑/↓ + Enter) | Mouse/Touch", width/2, height - 30);
451 }
452 }
453
454 function drawMenuBackground() {
455 push();
456 stroke(0, 255, 136, 30);
457 strokeWeight(1);
458
459 for (let x = 0; x < width; x += 40) {
460 let offset = sin(frameCount * 0.01 + x * 0.01) * 5;
461 line(x, 0, x, height + offset);
462 }
463
464 for (let y = 0; y < height; y += 40) {
465 let offset = cos(frameCount * 0.01 + y * 0.01) * 5;
466 line(0, y, width + offset, y);
467 }
468
469 for (let i = 0; i < 20; i++) {
470 let x = (frameCount * 0.5 + i * 137) % width;
471 let y = (sin(frameCount * 0.01 + i) * 50 + height/2);
472 let alpha = sin(frameCount * 0.02 + i) * 30 + 50;
473
474 fill(0, 255, 136, alpha);
475 noStroke();
476 ellipse(x, y, 3, 3);
477 }
478 pop();
479 }
480
481 // ============= HELPER FUNCTIONS =============
482 function getBallSpeed(ball) {
483 let velocity = ball.velocity;
484 return Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
485 }