JavaScript · 16407 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 // AI technique indicators
354 if (aiState.mode === 'WINDING_UP') {
355 fill(255, 150, 50, 200);
356 text(`AI WINDING UP | Phase: ${(aiState.windupPhase % (Math.PI * 2)).toFixed(2)} | Velocity: ${aiState.currentVelocity.toFixed(1)}`, 10, 175);
357
358 if (aiState.comboBop) {
359 fill(255, 50, 255, 200);
360 text("⚡ COMBO PLANNED!", 10, 190);
361 }
362 } else if (aiState.mode === 'SWINGING') {
363 fill(255, 50, 50, 200);
364 text("AI POWER SWING!", 10, 175);
365 } else if (aiState.consideringBop) {
366 fill(255, 255, 100, 200);
367 text("AI PREPARING BOP!", 10, 175);
368 }
369
370 if (bopState.right.active) {
371 fill(255, 255, 0, 255);
372 text("AI BOPPING!", 10, 190);
373 }
374 }
375 }
376
377 function drawStartMessage(aiEnabled, aiDifficulty) {
378 fill(0, 255, 136, 200);
379 textAlign(CENTER);
380 textSize(20);
381 text("Press any key to start!", width/2, height/2 + 100);
382 textSize(14);
383
384 if (aiEnabled) {
385 text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
386 text(`AI Difficulty: ${aiDifficulty.toUpperCase()}`, width/2, height/2 + 145);
387 } else {
388 text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
389 }
390
391 textSize(12);
392 fill(0, 255, 136, 120);
393 text("Press ESC to return to menu", width/2, height/2 + 170);
394 }
395
396 // ============= MENU RENDERING =============
397 function drawMenu(menuState) {
398 drawMenuBackground();
399
400 // Title
401 push();
402 let titlePulse = sin(frameCount * 0.05) * 0.2 + 1;
403 fill(0, 255, 136);
404 textAlign(CENTER);
405 textSize(60 * titlePulse);
406 text("SPRONG", width/2, 120);
407
408 fill(0, 255, 136, 150);
409 textSize(16);
410 text("Physics-based Pong with Spring Paddles", width/2, 150);
411 pop();
412
413 // Menu options
414 let startY = height/2 - 20;
415 let spacing = 60;
416
417 for (let i = 0; i < menuState.options.length; i++) {
418 let y = startY + i * spacing;
419 let isSelected = i === menuState.selectedOption;
420
421 if (isSelected) {
422 push();
423 let pulse = sin(frameCount * 0.15) * 0.3 + 1;
424 fill(0, 255, 136, 100 * pulse);
425 noStroke();
426 rectMode(CENTER);
427 rect(width/2, y, 300, 45);
428 pop();
429 }
430
431 fill(isSelected ? 255 : 200);
432 textAlign(CENTER);
433 textSize(isSelected ? 24 : 20);
434 text(menuState.options[i], width/2, y + 8);
435
436 if (i === 0 && isSelected && menuState.showDifficulty) {
437 fill(0, 255, 136, 180);
438 textSize(14);
439 text(`Difficulty: ${menuState.difficulties[menuState.difficultySelected]}`, width/2, y + 28);
440 text("(Use ← → to change)", width/2, y + 45);
441 }
442 }
443
444 // Instructions
445 fill(0, 255, 136, 120);
446 textAlign(CENTER);
447 textSize(14);
448 text("Use ↑↓ to select, ENTER to confirm", width/2, height - 80);
449 text("or click/touch to select", width/2, height - 60);
450
451 textSize(12);
452 fill(255, 100);
453 if (menuState.selectedOption === 0) {
454 text("Controls: W/S keys + LEFT SHIFT (bop) or Mouse/Touch", width/2, height - 30);
455 } else {
456 text("Controls: P1 (W/S + L.Shift) | P2 (↑/↓ + Enter) | Mouse/Touch", width/2, height - 30);
457 }
458 }
459
460 function drawMenuBackground() {
461 push();
462 stroke(0, 255, 136, 30);
463 strokeWeight(1);
464
465 for (let x = 0; x < width; x += 40) {
466 let offset = sin(frameCount * 0.01 + x * 0.01) * 5;
467 line(x, 0, x, height + offset);
468 }
469
470 for (let y = 0; y < height; y += 40) {
471 let offset = cos(frameCount * 0.01 + y * 0.01) * 5;
472 line(0, y, width + offset, y);
473 }
474
475 for (let i = 0; i < 20; i++) {
476 let x = (frameCount * 0.5 + i * 137) % width;
477 let y = (sin(frameCount * 0.01 + i) * 50 + height/2);
478 let alpha = sin(frameCount * 0.02 + i) * 30 + 50;
479
480 fill(0, 255, 136, alpha);
481 noStroke();
482 ellipse(x, y, 3, 3);
483 }
484 pop();
485 }
486
487 // ============= HELPER FUNCTIONS =============
488 function getBallSpeed(ball) {
489 let velocity = ball.velocity;
490 return Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
491 }