JavaScript · 35070 bytes Raw Blame History
1 // Matter.js module aliases
2 const Body = Matter.Body;
3 const World = Matter.World;
4 const Engine = Matter.Engine;
5 const Bodies = Matter.Bodies;
6 const Render = Matter.Render;
7 const Constraint = Matter.Constraint;
8
9 // Game variables
10 let ball;
11 let world;
12 let engine;
13
14 // Spring paddle system components
15 let boundaries = [];
16 let leftSupport, leftPaddle, leftSpring;
17 let rightSupport, rightPaddle, rightSpring;
18
19 // Game state
20 let leftScore = 0;
21 let rightScore = 0;
22 let gameStarted = false;
23 let gameState = 'menu'; // 'menu', 'playing', 'paused'
24 let gameMode = 'vs-cpu'; // 'vs-cpu' or 'vs-human'
25 let aiEnabled = true;
26
27 // Menu state
28 let menuState = {
29 selectedOption: 0, // 0 = 1 Player, 1 = 2 Player
30 options: ['1 Player vs CPU', '2 Player'],
31 difficultySelected: 1, // 0 = Easy, 1 = Medium, 2 = Hard
32 difficulties: ['Easy', 'Medium', 'Hard'],
33 showDifficulty: true
34 };
35
36 // AI system
37 let aiState = {
38 targetY: 200,
39 reactionDelay: 0,
40 difficulty: 'medium', // 'easy', 'medium', 'hard'
41 lastBallX: 0,
42 lastUpdateTime: 0
43 };
44
45 // Player input
46 let keys = {};
47 let inputBuffer = { left: 0, right: 0 };
48
49 // Touch/mouse input
50 let mouseInput = {
51 active: false,
52 targetY: 0,
53 leftPaddleTarget: 0,
54 rightPaddleTarget: 0,
55 smoothing: 0.08, // Slower smoothing for deliberate lag
56 deadZone: 15 // Minimum distance before movement starts
57 };
58
59 // Particle systems
60 let particles = [];
61 let impactParticles = [];
62
63 // Canvas settings
64 const CANVAS_WIDTH = 800;
65 const CANVAS_HEIGHT = 400;
66
67 // Game constants
68 const BALL_SPEED = 6;
69 const BALL_RADIUS = 12;
70 const PADDLE_WIDTH = 20;
71 const PADDLE_HEIGHT = 80;
72
73 // Enhanced movement constants (tuned for faster response)
74 const SUPPORT_SPEED = 6.5; // Bumped up from 4.5
75 const SUPPORT_ACCEL = 1.2; // Increased acceleration
76 const INPUT_SMOOTHING = 0.25; // More responsive
77 const SUPPORT_MAX_SPEED = 8; // Higher max speed
78
79 // Touch/mouse control constants
80 const MOUSE_SPEED_LIMIT = 4; // Max speed for mouse movement
81 const MOUSE_LAG_FACTOR = 0.12; // How much lag in mouse following
82 const TOUCH_SENSITIVITY = 1.2; // Touch movement multiplier
83
84 // Spring physics constants (toned down for stability)
85 const PADDLE_MASS = 0.8; // Back to more stable value
86 const SPRING_LENGTH = 40;
87 const SPRING_DAMPING = 0.6; // More damping = less erratic
88 const SPRING_STIFFNESS = 0.025; // Slightly lower for smoother motion
89
90 // AI difficulty settings
91 const AI_SETTINGS = {
92 easy: {
93 reactionTime: 400, // ms delay
94 accuracy: 0.7, // 70% accuracy
95 speed: 0.6, // 60% of normal speed
96 prediction: 0.3 // 30% prediction vs reaction
97 },
98 medium: {
99 reactionTime: 250,
100 accuracy: 0.85,
101 speed: 0.8,
102 prediction: 0.6
103 },
104 hard: {
105 reactionTime: 150,
106 accuracy: 0.95,
107 speed: 1.0,
108 prediction: 0.8
109 }
110 };
111
112 // Visual enhancement constants
113 const TRAIL_SEGMENTS = 8;
114 const PADDLE_GLOW_DISTANCE = 25;
115 const SPRING_GLOW_INTENSITY = 120; // More intense glow
116
117 // Particle system constants
118 const MAX_PARTICLES = 100;
119 const PARTICLE_LIFE = 60;
120 const IMPACT_PARTICLES = 8;
121 const SPRING_PARTICLE_RATE = 0.3;
122
123 function setup() {
124 // Create p5.js canvas
125 let canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
126 canvas.parent('gameCanvas');
127
128 // Initialize Matter.js physics engine
129 engine = Engine.create();
130 world = engine.world;
131
132 // Disable gravity for classic Pong feel
133 engine.world.gravity.y = 0;
134 engine.world.gravity.x = 0;
135
136 // Create game boundaries (top and bottom walls)
137 let topWall = Bodies.rectangle(width/2, -10, width, 20, { isStatic: true });
138 let bottomWall = Bodies.rectangle(width/2, height + 10, width, 20, { isStatic: true });
139 boundaries.push(topWall, bottomWall);
140
141 // Create spring paddle systems
142 createSpringPaddleSystem('left');
143 createSpringPaddleSystem('right');
144
145 // Create ball
146 resetBall();
147
148 // Add everything to the world
149 World.add(world, [
150 ...boundaries,
151 ball,
152 leftSupport, leftPaddle, leftSpring,
153 rightSupport, rightPaddle, rightSpring
154 ]);
155
156 console.log("🎮 Sprong Phase 5 Complete!");
157 console.log("✓ Particle effects system");
158 console.log("✓ Tuned physics for maximum bounce");
159 console.log("✓ Faster, more responsive paddles");
160 }
161
162 function createSpringPaddleSystem(side) {
163 let supportX = side === 'left' ? 60 : width - 60;
164 let paddleX = side === 'left' ? 60 + SPRING_LENGTH : width - 60 - SPRING_LENGTH;
165 let startY = height / 2;
166
167 if (side === 'left') {
168 // Left support (invisible anchor point controlled by player)
169 leftSupport = Bodies.rectangle(supportX, startY, 10, 10, {
170 isStatic: true,
171 render: { visible: false }
172 });
173
174 // Left paddle (the actual hitting surface)
175 leftPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
176 mass: PADDLE_MASS,
177 restitution: 1.3, // Even bouncier!
178 friction: 0,
179 frictionAir: 0.005 // Less air resistance
180 });
181
182 // Spring constraint connecting support to paddle
183 leftSpring = Constraint.create({
184 bodyA: leftSupport,
185 bodyB: leftPaddle,
186 length: SPRING_LENGTH,
187 stiffness: SPRING_STIFFNESS,
188 damping: SPRING_DAMPING
189 });
190 } else {
191 // Right support (invisible anchor point controlled by player/AI)
192 rightSupport = Bodies.rectangle(supportX, startY, 10, 10, {
193 isStatic: true,
194 render: { visible: false }
195 });
196
197 // Right paddle (the actual hitting surface)
198 rightPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
199 mass: PADDLE_MASS,
200 restitution: 1.2, // Slightly toned down
201 friction: 0,
202 frictionAir: 0.008 // Bit more air resistance for stability
203 });
204
205 // Spring constraint connecting support to paddle
206 rightSpring = Constraint.create({
207 bodyA: rightSupport,
208 bodyB: rightPaddle,
209 length: SPRING_LENGTH,
210 stiffness: SPRING_STIFFNESS,
211 damping: SPRING_DAMPING
212 });
213 }
214 }
215
216 function draw() {
217 // Update physics
218 Engine.update(engine);
219
220 // Clear canvas
221 background(10, 10, 10);
222
223 if (gameState === 'menu') {
224 drawMenu();
225 } else {
226 // Handle enhanced player input
227 handleEnhancedInput();
228
229 // Update particle systems
230 updateParticles();
231 checkCollisions();
232
233 // Check for scoring
234 checkBallPosition();
235
236 // Draw particles behind everything
237 drawParticles();
238
239 // Draw game objects with enhanced visuals
240 drawSpringPaddleSystemsEnhanced();
241 drawBallEnhanced();
242 drawBoundaries();
243 drawCenterLine();
244
245 // Draw debug info
246 drawDebugInfo();
247
248 // Start message
249 if (!gameStarted) {
250 drawStartMessage();
251 }
252 }
253 }
254
255 function handleEnhancedInput() {
256 // Handle both keyboard and mouse/touch input
257 handleKeyboardInput();
258 handleMouseTouchInput();
259
260 // Handle AI if enabled
261 if (aiEnabled && gameStarted) {
262 handleAI();
263 }
264 }
265
266 function handleKeyboardInput() {
267 // Smooth input accumulation with acceleration
268 let leftInput = 0;
269 let rightInput = 0;
270
271 // Left paddle input (W/S keys) - always player controlled
272 if (keys['w'] || keys['W']) leftInput -= 1;
273 if (keys['s'] || keys['S']) leftInput += 1;
274
275 // Right paddle input (Arrow keys) - only if AI is disabled
276 if (!aiEnabled) {
277 if (keys['ArrowUp']) rightInput -= 1;
278 if (keys['ArrowDown']) rightInput += 1;
279 }
280
281 // Apply acceleration and smoothing for keyboard
282 inputBuffer.left = lerp(inputBuffer.left, leftInput, INPUT_SMOOTHING);
283 if (!aiEnabled) {
284 inputBuffer.right = lerp(inputBuffer.right, rightInput, INPUT_SMOOTHING);
285 }
286
287 // Move supports with enhanced physics (only if not using mouse)
288 if (!mouseInput.active) {
289 if (Math.abs(inputBuffer.left) > 0.01) {
290 moveSupportEnhanced(leftSupport, inputBuffer.left * SUPPORT_SPEED);
291 }
292 if (!aiEnabled && Math.abs(inputBuffer.right) > 0.01) {
293 moveSupportEnhanced(rightSupport, inputBuffer.right * SUPPORT_SPEED);
294 }
295 }
296 }
297
298 function handleMouseTouchInput() {
299 if (!mouseInput.active) return;
300
301 // Determine which paddle to control based on mouse X position
302 let controllingLeft = mouseX < width / 2;
303
304 // Don't allow mouse control of AI paddle
305 if (!controllingLeft && aiEnabled) return;
306
307 let targetSupport = controllingLeft ? leftSupport : rightSupport;
308
309 // Calculate target Y with dead zone
310 let currentY = targetSupport.position.y;
311 let targetY = mouseY;
312 let deltaY = targetY - currentY;
313
314 // Apply dead zone - don't move unless mouse is far enough
315 if (Math.abs(deltaY) < mouseInput.deadZone) {
316 return;
317 }
318
319 // Calculate movement with lag and speed limiting
320 let movement = deltaY * MOUSE_LAG_FACTOR * TOUCH_SENSITIVITY;
321
322 // Limit maximum speed to prevent snappy movement
323 movement = constrain(movement, -MOUSE_SPEED_LIMIT, MOUSE_SPEED_LIMIT);
324
325 // Apply the lagged movement
326 moveSupportEnhanced(targetSupport, movement);
327
328 // Visual feedback - update input buffer for particle effects
329 if (controllingLeft) {
330 inputBuffer.left = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
331 } else if (!aiEnabled) {
332 inputBuffer.right = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
333 }
334 }
335
336 function handleAI() {
337 let currentTime = millis();
338 let ballPos = ball.position;
339 let ballVel = ball.velocity;
340 let aiSettings = AI_SETTINGS[aiState.difficulty];
341
342 // Only update AI decision if enough time has passed (reaction delay)
343 if (currentTime - aiState.lastUpdateTime > aiSettings.reactionTime) {
344
345 // Calculate where ball will be (basic prediction)
346 let ballFutureX = ballPos.x + ballVel.x * 30; // Look 30 frames ahead
347 let ballFutureY = ballPos.y + ballVel.y * 30;
348
349 // Only react if ball is moving toward AI paddle
350 if (ballVel.x > 0) {
351 // Mix prediction with current position based on AI skill
352 let targetY = lerp(ballPos.y, ballFutureY, aiSettings.prediction);
353
354 // Add some inaccuracy to make it beatable
355 let error = (random() - 0.5) * 50 * (1 - aiSettings.accuracy);
356 targetY += error;
357
358 // Keep target in bounds
359 targetY = constrain(targetY, 80, height - 80);
360
361 aiState.targetY = targetY;
362 aiState.lastUpdateTime = currentTime;
363 }
364 }
365
366 // Move AI paddle toward target with speed limitation
367 let currentY = rightSupport.position.y;
368 let deltaY = aiState.targetY - currentY;
369
370 if (Math.abs(deltaY) > 5) { // Dead zone
371 let movement = deltaY * 0.08 * aiSettings.speed; // Smooth movement
372 movement = constrain(movement, -SUPPORT_SPEED * 0.8, SUPPORT_SPEED * 0.8);
373
374 moveSupportEnhanced(rightSupport, movement);
375
376 // Update input buffer for visual effects
377 inputBuffer.right = constrain(movement / (SUPPORT_SPEED * 0.8), -1, 1);
378 } else {
379 // Gradually reduce input buffer when AI is not moving
380 inputBuffer.right *= 0.9;
381 }
382 }
383
384 function moveSupportEnhanced(support, deltaY) {
385 let newY = support.position.y + deltaY;
386
387 // Keep support within reasonable bounds with smooth clamping
388 let minY = 50;
389 let maxY = height - 50;
390
391 if (newY < minY) {
392 newY = minY + (newY - minY) * 0.1; // Soft boundary
393 } else if (newY > maxY) {
394 newY = maxY + (newY - maxY) * 0.1; // Soft boundary
395 }
396
397 Body.setPosition(support, { x: support.position.x, y: newY });
398 }
399
400 function checkCollisions() {
401 let ballPos = ball.position;
402 let ballVel = ball.velocity;
403
404 // Check paddle collisions for particle effects
405 let leftDist = dist(ballPos.x, ballPos.y, leftPaddle.position.x, leftPaddle.position.y);
406 let rightDist = dist(ballPos.x, ballPos.y, rightPaddle.position.x, rightPaddle.position.y);
407
408 // Collision threshold
409 let collisionDist = BALL_RADIUS + PADDLE_WIDTH/2 + 5;
410
411 // Left paddle collision
412 if (leftDist < collisionDist && ballVel.x < 0) {
413 createImpactParticles(ballPos.x, ballPos.y, ballVel.x, ballVel.y);
414 }
415
416 // Right paddle collision
417 if (rightDist < collisionDist && ballVel.x > 0) {
418 createImpactParticles(ballPos.x, ballPos.y, ballVel.x, ballVel.y);
419 }
420 }
421
422 function createImpactParticles(x, y, velX, velY) {
423 for (let i = 0; i < IMPACT_PARTICLES; i++) {
424 let angle = random(TWO_PI);
425 let speed = random(2, 8);
426 let size = random(2, 6);
427
428 particles.push({
429 x: x + random(-5, 5),
430 y: y + random(-5, 5),
431 vx: cos(angle) * speed - velX * 0.2,
432 vy: sin(angle) * speed - velY * 0.2,
433 size: size,
434 life: PARTICLE_LIFE,
435 maxLife: PARTICLE_LIFE,
436 color: { r: 255, g: random(100, 255), b: random(100, 150) },
437 type: 'impact'
438 });
439 }
440 }
441
442 function createSpringParticles(springPos, compression) {
443 if (random() < SPRING_PARTICLE_RATE * compression) {
444 let angle = random(TWO_PI);
445 let speed = random(1, 3) * compression;
446
447 particles.push({
448 x: springPos.x + random(-10, 10),
449 y: springPos.y + random(-10, 10),
450 vx: cos(angle) * speed,
451 vy: sin(angle) * speed,
452 size: random(1, 3),
453 life: PARTICLE_LIFE * 0.5,
454 maxLife: PARTICLE_LIFE * 0.5,
455 color: { r: 0, g: 255, b: 136 },
456 type: 'spring'
457 });
458 }
459 }
460
461 function updateParticles() {
462 // Update and remove dead particles
463 for (let i = particles.length - 1; i >= 0; i--) {
464 let p = particles[i];
465
466 // Update position
467 p.x += p.vx;
468 p.y += p.vy;
469
470 // Apply drag
471 p.vx *= 0.98;
472 p.vy *= 0.98;
473
474 // Update life
475 p.life--;
476
477 // Remove dead particles
478 if (p.life <= 0) {
479 particles.splice(i, 1);
480 }
481 }
482
483 // Limit particle count
484 if (particles.length > MAX_PARTICLES) {
485 particles.splice(0, particles.length - MAX_PARTICLES);
486 }
487 }
488
489 function drawParticles() {
490 for (let p of particles) {
491 let alpha = map(p.life, 0, p.maxLife, 0, 255);
492
493 push();
494 translate(p.x, p.y);
495
496 if (p.type === 'impact') {
497 // Impact particles: bright sparks
498 fill(p.color.r, p.color.g, p.color.b, alpha);
499 noStroke();
500 ellipse(0, 0, p.size, p.size);
501
502 // Add glow
503 fill(p.color.r, p.color.g, p.color.b, alpha * 0.3);
504 ellipse(0, 0, p.size * 2, p.size * 2);
505
506 } else if (p.type === 'spring') {
507 // Spring particles: green energy
508 fill(p.color.r, p.color.g, p.color.b, alpha);
509 noStroke();
510 ellipse(0, 0, p.size, p.size);
511 }
512
513 pop();
514 }
515 }
516
517 function drawSpringPaddleSystemsEnhanced() {
518 // Draw springs with enhanced visuals and particles
519 drawSpringsEnhanced();
520
521 // Draw paddles with glow effects
522 drawPaddlesWithGlow();
523
524 // Draw support points with input feedback
525 drawSupportPointsEnhanced();
526 }
527
528 function drawSpringsEnhanced() {
529 // Left spring
530 let leftSupportPos = leftSupport.position;
531 let leftPaddlePos = leftPaddle.position;
532 let leftCompression = drawSpringLineEnhanced(leftSupportPos, leftPaddlePos);
533 createSpringParticles(leftPaddlePos, leftCompression);
534
535 // Right spring
536 let rightSupportPos = rightSupport.position;
537 let rightPaddlePos = rightPaddle.position;
538 let rightCompression = drawSpringLineEnhanced(rightSupportPos, rightPaddlePos);
539 createSpringParticles(rightPaddlePos, rightCompression);
540 }
541
542 function drawSpringLineEnhanced(startPos, endPos) {
543 let segments = 12; // More segments for smoother springs
544 let amplitude = 10; // Bigger amplitude for more dramatic effect
545
546 // Calculate spring compression for visual effects
547 let currentLength = dist(startPos.x, startPos.y, endPos.x, endPos.y);
548 let compression = SPRING_LENGTH / currentLength;
549 amplitude *= compression;
550
551 // Enhanced spring glow based on compression
552 let glowIntensity = 150 + compression * SPRING_GLOW_INTENSITY;
553 stroke(0, 255, 136, glowIntensity);
554 strokeWeight(3 + compression * 2); // Thicker when compressed
555
556 // Draw spring coil with smooth curves
557 beginShape();
558 noFill();
559
560 for (let i = 0; i <= segments; i++) {
561 let t = i / segments;
562 let x = lerp(startPos.x, endPos.x, t);
563 let y = lerp(startPos.y, endPos.y, t);
564
565 // Enhanced zigzag with smoother curves
566 if (i > 0 && i < segments) {
567 let perpX = -(endPos.y - startPos.y) / currentLength;
568 let perpY = (endPos.x - startPos.x) / currentLength;
569 let offset = sin(i * PI * 1.5) * amplitude; // More dramatic oscillation
570 x += perpX * offset;
571 y += perpY * offset;
572 }
573
574 vertex(x, y);
575 }
576
577 endShape();
578
579 // Add spring glow effect with pulsing
580 let pulse = sin(frameCount * 0.1) * 0.2 + 1;
581 stroke(0, 255, 136, glowIntensity * 0.4 * pulse);
582 strokeWeight(8 + compression * 3);
583 beginShape();
584 noFill();
585
586 for (let i = 0; i <= segments; i++) {
587 let t = i / segments;
588 let x = lerp(startPos.x, endPos.x, t);
589 let y = lerp(startPos.y, endPos.y, t);
590 vertex(x, y);
591 }
592
593 endShape();
594
595 return compression; // Return compression for particle effects
596 }
597
598 function drawPaddlesWithGlow() {
599 // Calculate ball distance for glow effects
600 let ballPos = ball.position;
601 let leftDist = dist(ballPos.x, ballPos.y, leftPaddle.position.x, leftPaddle.position.y);
602 let rightDist = dist(ballPos.x, ballPos.y, rightPaddle.position.x, rightPaddle.position.y);
603
604 // Enhanced paddle drawing
605 drawSinglePaddleEnhanced(leftPaddle, leftDist);
606 drawSinglePaddleEnhanced(rightPaddle, rightDist);
607 }
608
609 function drawSinglePaddleEnhanced(paddle, ballDistance) {
610 let pos = paddle.position;
611 let angle = paddle.angle;
612
613 // Check if this is the AI paddle
614 let isAI = aiEnabled && paddle === rightPaddle;
615
616 // Calculate glow intensity based on ball proximity
617 let glowIntensity = map(ballDistance, 0, PADDLE_GLOW_DISTANCE, 150, 0);
618 glowIntensity = constrain(glowIntensity, 0, 150);
619
620 push();
621 translate(pos.x, pos.y);
622 rotate(angle);
623
624 // Different color scheme for AI paddle
625 let paddleColor = isAI ? [255, 100, 100] : [0, 255, 136]; // Red for AI, green for player
626
627 // Draw enhanced glow effect first
628 if (glowIntensity > 0) {
629 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.6);
630 noStroke();
631 rectMode(CENTER);
632 rect(0, 0, PADDLE_WIDTH + 12, PADDLE_HEIGHT + 12);
633
634 // Add outer glow
635 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.3);
636 rect(0, 0, PADDLE_WIDTH + 20, PADDLE_HEIGHT + 20);
637 }
638
639 // Draw main paddle with enhanced visual
640 fill(paddleColor[0], paddleColor[1], paddleColor[2]);
641 stroke(paddleColor[0], paddleColor[1], paddleColor[2], 220 + glowIntensity * 0.5);
642 strokeWeight(3);
643 rectMode(CENTER);
644 rect(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT);
645
646 // Add core highlight
647 if (isAI) {
648 fill(255, 200, 200, 100); // Light red highlight for AI
649 } else {
650 fill(150, 255, 200, 100); // Light green highlight for player
651 }
652 noStroke();
653 rect(0, 0, PADDLE_WIDTH - 4, PADDLE_HEIGHT - 4);
654
655 pop();
656 }
657
658 function drawSupportPointsEnhanced() {
659 // Enhanced support indicators with input feedback
660 let leftActivity = Math.abs(inputBuffer.left) * 255;
661 let rightActivity = Math.abs(inputBuffer.right) * 255;
662
663 // Left support with pulsing effect
664 let leftPulse = sin(frameCount * 0.2) * 0.3 + 1;
665 fill(0, 255, 136, 100 + leftActivity * 0.6);
666 noStroke();
667 ellipse(leftSupport.position.x, leftSupport.position.y,
668 (8 + leftActivity * 0.15) * leftPulse,
669 (8 + leftActivity * 0.15) * leftPulse);
670
671 // Right support with pulsing effect
672 let rightPulse = sin(frameCount * 0.2 + PI) * 0.3 + 1;
673 fill(0, 255, 136, 100 + rightActivity * 0.6);
674 ellipse(rightSupport.position.x, rightSupport.position.y,
675 (8 + rightActivity * 0.15) * rightPulse,
676 (8 + rightActivity * 0.15) * rightPulse);
677 }
678
679 function drawBallEnhanced() {
680 let ballPos = ball.position;
681 let ballVel = ball.velocity;
682 let speed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
683
684 // Enhanced ball with speed-based effects
685 let speedIntensity = map(speed, 0, 15, 50, 255);
686
687 // Multi-layered trail effect
688 for (let i = 0; i < 3; i++) {
689 let offset = i * 3;
690 fill(255, 100, 100, 40 - i * 10);
691 noStroke();
692 ellipse(ballPos.x - ballVel.x * offset * 0.1,
693 ballPos.y - ballVel.y * offset * 0.1,
694 BALL_RADIUS * (4 - i), BALL_RADIUS * (4 - i));
695 }
696
697 // Main ball with enhanced glow
698 fill(255, 100, 100);
699 stroke(255, 200, 200, speedIntensity);
700 strokeWeight(3 + speed * 0.15);
701 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 2, BALL_RADIUS * 2);
702
703 // Speed indicator core
704 if (speed > 8) {
705 fill(255, 255, 255, speedIntensity * 0.8);
706 noStroke();
707 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 0.8, BALL_RADIUS * 0.8);
708 }
709
710 // Outer energy ring for high speeds
711 if (speed > 12) {
712 noFill();
713 stroke(255, 255, 255, speedIntensity * 0.5);
714 strokeWeight(2);
715 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 3, BALL_RADIUS * 3);
716 }
717 }
718
719 function drawBoundaries() {
720 stroke(0, 255, 136, 30);
721 strokeWeight(1);
722 noFill();
723 line(0, 0, width, 0);
724 line(0, height, width, height);
725 }
726
727 function drawCenterLine() {
728 stroke(0, 255, 136, 50);
729 strokeWeight(2);
730
731 for (let y = 0; y < height; y += 20) {
732 line(width/2, y, width/2, y + 10);
733 }
734 }
735
736 function drawDebugInfo() {
737 fill(255, 100);
738 textAlign(LEFT);
739 textSize(12);
740 text(`FPS: ${Math.round(frameRate())}`, 10, 20);
741 text(`Ball Speed: ${Math.round(getBallSpeed())}`, 10, 35);
742 text(`Particles: ${particles.length}`, 10, 50);
743 text(`Mode: ${aiEnabled ? 'vs CPU' : '2 Player'} | Difficulty: ${aiState.difficulty}`, 10, 65);
744
745 // Enhanced spring info
746 let leftSpringLength = dist(leftSupport.position.x, leftSupport.position.y,
747 leftPaddle.position.x, leftPaddle.position.y);
748 let rightSpringLength = dist(rightSupport.position.x, rightSupport.position.y,
749 rightPaddle.position.x, rightPaddle.position.y);
750
751 text(`L Spring: ${Math.round(leftSpringLength)}px (${((SPRING_LENGTH/leftSpringLength - 1) * 100).toFixed(0)}%)`, 10, 80);
752 text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 95);
753 text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 110);
754
755 // AI debug info
756 if (aiEnabled) {
757 text(`AI Target: ${Math.round(aiState.targetY)} | Ball Y: ${Math.round(ball.position.y)}`, 10, 125);
758 text(`Ball Vel: X=${ball.velocity.x.toFixed(1)} Y=${ball.velocity.y.toFixed(1)}`, 10, 140);
759 }
760
761 // Mouse/touch input debug
762 if (mouseInput.active) {
763 text(`Mouse: Active | Side: ${mouseX < width/2 ? 'Left' : 'Right'} | Y: ${mouseY}`, 10, 155);
764 }
765 }
766
767 function drawMenu() {
768 // Draw animated background
769 drawMenuBackground();
770
771 // Main title
772 push();
773 let titlePulse = sin(frameCount * 0.05) * 0.2 + 1;
774 fill(0, 255, 136);
775 textAlign(CENTER);
776 textSize(60 * titlePulse);
777 text("SPRONG", width/2, 120);
778
779 // Subtitle
780 fill(0, 255, 136, 150);
781 textSize(16);
782 text("Physics-based Pong with Spring Paddles", width/2, 150);
783 pop();
784
785 // Menu options
786 let startY = height/2 - 20;
787 let spacing = 60;
788
789 for (let i = 0; i < menuState.options.length; i++) {
790 let y = startY + i * spacing;
791 let isSelected = i === menuState.selectedOption;
792
793 // Selection indicator
794 if (isSelected) {
795 push();
796 let pulse = sin(frameCount * 0.15) * 0.3 + 1;
797 fill(0, 255, 136, 100 * pulse);
798 noStroke();
799 rectMode(CENTER);
800 rect(width/2, y, 300, 45);
801 pop();
802 }
803
804 // Option text
805 fill(isSelected ? 255 : 200);
806 textAlign(CENTER);
807 textSize(isSelected ? 24 : 20);
808 text(menuState.options[i], width/2, y + 8);
809
810 // Show difficulty selector for 1 Player option
811 if (i === 0 && isSelected && menuState.showDifficulty) {
812 fill(0, 255, 136, 180);
813 textSize(14);
814 text(`Difficulty: ${menuState.difficulties[menuState.difficultySelected]}`, width/2, y + 28);
815 text("(Use ← → to change)", width/2, y + 45);
816 }
817 }
818
819 // Instructions
820 fill(0, 255, 136, 120);
821 textAlign(CENTER);
822 textSize(14);
823 text("Use ↑↓ to select, ENTER to confirm", width/2, height - 80);
824 text("or click/touch to select", width/2, height - 60);
825
826 // Show controls preview
827 textSize(12);
828 fill(255, 100);
829 if (menuState.selectedOption === 0) {
830 text("Controls: W/S keys or Mouse/Touch to move paddle", width/2, height - 30);
831 } else {
832 text("Controls: Player 1 (W/S) | Player 2 (↑/↓) | Mouse/Touch", width/2, height - 30);
833 }
834 }
835
836 function drawMenuBackground() {
837 // Draw subtle animated background elements
838 push();
839 stroke(0, 255, 136, 30);
840 strokeWeight(1);
841
842 // Animated grid
843 for (let x = 0; x < width; x += 40) {
844 let offset = sin(frameCount * 0.01 + x * 0.01) * 5;
845 line(x, 0, x, height + offset);
846 }
847
848 for (let y = 0; y < height; y += 40) {
849 let offset = cos(frameCount * 0.01 + y * 0.01) * 5;
850 line(0, y, width + offset, y);
851 }
852
853 // Floating particles
854 for (let i = 0; i < 20; i++) {
855 let x = (frameCount * 0.5 + i * 137) % width;
856 let y = (sin(frameCount * 0.01 + i) * 50 + height/2);
857 let alpha = sin(frameCount * 0.02 + i) * 30 + 50;
858
859 fill(0, 255, 136, alpha);
860 noStroke();
861 ellipse(x, y, 3, 3);
862 }
863 pop();
864 }
865
866 function drawStartMessage() {
867 fill(0, 255, 136, 200);
868 textAlign(CENTER);
869 textSize(20);
870 text("Press any key to start!", width/2, height/2 + 100);
871 textSize(14);
872
873 if (aiEnabled) {
874 text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
875 text(`AI Difficulty: ${aiState.difficulty.toUpperCase()}`, width/2, height/2 + 145);
876 } else {
877 text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
878 }
879
880 textSize(12);
881 fill(0, 255, 136, 120);
882 text("Press ESC to return to menu", width/2, height/2 + 170);
883 }
884
885 function resetBall() {
886 if (ball) {
887 World.remove(world, ball);
888 }
889
890 // Create new ball at center
891 ball = Bodies.circle(width/2, height/2, BALL_RADIUS, {
892 restitution: 1,
893 friction: 0,
894 frictionAir: 0
895 });
896
897 if (world) {
898 World.add(world, ball);
899 }
900
901 // Start ball moving after a short delay
902 setTimeout(() => {
903 let direction = random() > 0.5 ? 1 : -1;
904 let angle = random(-PI/6, PI/6);
905
906 Body.setVelocity(ball, {
907 x: direction * BALL_SPEED * cos(angle),
908 y: BALL_SPEED * sin(angle)
909 });
910
911 gameStarted = true;
912 }, 1000);
913 }
914
915 function checkBallPosition() {
916 let ballX = ball.position.x;
917
918 if (ballX < -BALL_RADIUS) {
919 rightScore++;
920 updateScore();
921 resetBall();
922 gameStarted = false;
923 }
924
925 if (ballX > width + BALL_RADIUS) {
926 leftScore++;
927 updateScore();
928 resetBall();
929 gameStarted = false;
930 }
931 }
932
933 function updateScore() {
934 document.getElementById('leftScore').textContent = leftScore;
935 document.getElementById('rightScore').textContent = rightScore;
936 }
937
938 function getBallSpeed() {
939 let velocity = ball.velocity;
940 return Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
941 }
942
943 // Input handling
944 function keyPressed() {
945 keys[key] = true;
946 keys[keyCode] = true;
947
948 if (gameState === 'menu') {
949 handleMenuInput();
950 return;
951 }
952
953 if (!gameStarted && key !== ' ') {
954 gameStarted = true;
955 }
956
957 // Toggle game mode (only during gameplay)
958 if (key === 'm' || key === 'M') {
959 aiEnabled = !aiEnabled;
960 gameMode = aiEnabled ? 'vs-cpu' : 'vs-human';
961 console.log(`🎮 Switched to ${gameMode} mode`);
962 }
963
964 // Change AI difficulty (only during gameplay)
965 if (key === 'd' || key === 'D') {
966 if (aiState.difficulty === 'easy') {
967 aiState.difficulty = 'medium';
968 } else if (aiState.difficulty === 'medium') {
969 aiState.difficulty = 'hard';
970 } else {
971 aiState.difficulty = 'easy';
972 }
973 console.log(`🤖 AI difficulty: ${aiState.difficulty}`);
974 }
975
976 // Reset game with spacebar
977 if (key === ' ') {
978 leftScore = 0;
979 rightScore = 0;
980 updateScore();
981 resetBall();
982 gameStarted = false;
983
984 // Reset input buffers
985 inputBuffer.left = 0;
986 inputBuffer.right = 0;
987
988 // Reset mouse input
989 mouseInput.active = false;
990
991 // Reset AI state
992 aiState.targetY = height / 2;
993 aiState.lastUpdateTime = 0;
994
995 // Clear particles
996 particles = [];
997
998 console.log("🔄 Game reset!");
999 }
1000
1001 // Return to menu with ESC
1002 if (keyCode === 27) { // ESC key
1003 gameState = 'menu';
1004 gameStarted = false;
1005 particles = [];
1006 console.log("📋 Returned to menu");
1007 }
1008 }
1009
1010 function handleMenuInput() {
1011 // Navigate menu with arrow keys
1012 if (keyCode === UP_ARROW) {
1013 menuState.selectedOption = Math.max(0, menuState.selectedOption - 1);
1014 } else if (keyCode === DOWN_ARROW) {
1015 menuState.selectedOption = Math.min(menuState.options.length - 1, menuState.selectedOption + 1);
1016 }
1017
1018 // Change difficulty for 1 Player mode
1019 if (menuState.selectedOption === 0) {
1020 if (keyCode === LEFT_ARROW) {
1021 menuState.difficultySelected = Math.max(0, menuState.difficultySelected - 1);
1022 } else if (keyCode === RIGHT_ARROW) {
1023 menuState.difficultySelected = Math.min(menuState.difficulties.length - 1, menuState.difficultySelected + 1);
1024 }
1025 }
1026
1027 // Confirm selection with ENTER
1028 if (keyCode === ENTER || key === ' ') {
1029 startGameWithSelection();
1030 }
1031 }
1032
1033 function startGameWithSelection() {
1034 // Set game mode based on selection
1035 if (menuState.selectedOption === 0) {
1036 // 1 Player vs CPU
1037 aiEnabled = true;
1038 gameMode = 'vs-cpu';
1039 aiState.difficulty = menuState.difficulties[menuState.difficultySelected].toLowerCase();
1040 } else {
1041 // 2 Player
1042 aiEnabled = false;
1043 gameMode = 'vs-human';
1044 }
1045
1046 // Start the game
1047 gameState = 'playing';
1048 gameStarted = false; // Will start when user presses a key
1049
1050 // Reset game state
1051 leftScore = 0;
1052 rightScore = 0;
1053 updateScore();
1054 resetBall();
1055
1056 // Reset input buffers
1057 inputBuffer.left = 0;
1058 inputBuffer.right = 0;
1059 mouseInput.active = false;
1060
1061 // Reset AI state
1062 aiState.targetY = height / 2;
1063 aiState.lastUpdateTime = 0;
1064
1065 // Clear particles
1066 particles = [];
1067
1068 console.log(`🎮 Started ${gameMode} mode${aiEnabled ? ' - Difficulty: ' + aiState.difficulty : ''}`);
1069 }
1070
1071 function keyReleased() {
1072 keys[key] = false;
1073 keys[keyCode] = false;
1074 }
1075
1076 // Mouse/touch input handlers
1077 function mousePressed() {
1078 if (gameState === 'menu') {
1079 handleMenuClick();
1080 return false;
1081 }
1082
1083 // Start mouse/touch input when clicking in game area
1084 if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) {
1085 mouseInput.active = true;
1086
1087 // Start game if not started
1088 if (!gameStarted) {
1089 gameStarted = true;
1090 }
1091
1092 return false; // Prevent default behavior
1093 }
1094 }
1095
1096 function handleMenuClick() {
1097 let startY = height/2 - 20;
1098 let spacing = 60;
1099
1100 // Check if clicked on menu options
1101 for (let i = 0; i < menuState.options.length; i++) {
1102 let y = startY + i * spacing;
1103
1104 if (mouseY > y - 25 && mouseY < y + 25) {
1105 if (menuState.selectedOption === i) {
1106 // Double click or click on already selected - start game
1107 startGameWithSelection();
1108 } else {
1109 // Select this option
1110 menuState.selectedOption = i;
1111 }
1112 break;
1113 }
1114 }
1115
1116 // Check difficulty selection area for 1 Player mode
1117 if (menuState.selectedOption === 0) {
1118 let diffY = startY + 28;
1119 if (mouseY > diffY && mouseY < diffY + 20) {
1120 // Cycle through difficulties on click
1121 menuState.difficultySelected = (menuState.difficultySelected + 1) % menuState.difficulties.length;
1122 }
1123 }
1124 }
1125
1126 function mouseDragged() {
1127 // Continue mouse/touch input while dragging
1128 if (mouseInput.active) {
1129 return false; // Prevent default behavior
1130 }
1131 }
1132
1133 function mouseReleased() {
1134 // Stop mouse/touch input when releasing
1135 mouseInput.active = false;
1136
1137 // Gradually reduce input buffer when mouse is released
1138 inputBuffer.left *= 0.8;
1139 inputBuffer.right *= 0.8;
1140 }
1141
1142 function touchStarted() {
1143 // Handle touch events same as mouse
1144 return mousePressed();
1145 }
1146
1147 function touchMoved() {
1148 // Handle touch drag same as mouse
1149 return mouseDragged();
1150 }
1151
1152 function touchEnded() {
1153 // Handle touch end same as mouse
1154 mouseReleased();
1155 return false;
1156 }