JavaScript · 47321 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 // Advanced AI state machine
45 mode: 'TRACKING', // TRACKING, WINDING_UP, SWINGING, RECOVERING, ANTICIPATING
46 windupStartTime: 0,
47 swingStartTime: 0,
48 interceptY: 200,
49 windupDirection: 1, // 1 for up, -1 for down
50 aggressionLevel: 0.5, // 0 = defensive, 1 = maximum aggression
51 lastHitTime: 0,
52
53 // Oscillation parameters (increased for better windup)
54 windupDistance: 120, // Much bigger - about half canvas height
55 swingPower: 1.05, // Reduced from 1.1 for more control
56 timingWindow: 40, // Slightly longer execution window
57
58 // Lifelike movement
59 idleTarget: 200, // Where AI "wants" to be when idle
60 microAdjustment: 0, // Small random movements
61 breathingOffset: 0, // Subtle breathing-like motion
62 lastMicroTime: 0 // For micro-movement timing
63 };
64
65 // Player input
66 let keys = {};
67 let inputBuffer = { left: 0, right: 0 };
68
69 // Touch/mouse input
70 let mouseInput = {
71 active: false,
72 targetY: 0,
73 leftPaddleTarget: 0,
74 rightPaddleTarget: 0,
75 smoothing: 0.08, // Slower smoothing for deliberate lag
76 deadZone: 15 // Minimum distance before movement starts
77 };
78
79 // Particle systems
80 let particles = [];
81 let impactParticles = [];
82
83 // Canvas settings
84 const CANVAS_WIDTH = 800;
85 const CANVAS_HEIGHT = 400;
86
87 // Game constants
88 const BALL_SPEED = 6;
89 const BALL_RADIUS = 12;
90 const PADDLE_WIDTH = 20;
91 const PADDLE_HEIGHT = 80;
92
93 // Enhanced movement constants (tuned for faster response)
94 const SUPPORT_SPEED = 6.5; // Bumped up from 4.5
95 const SUPPORT_ACCEL = 1.2; // Increased acceleration
96 const INPUT_SMOOTHING = 0.25; // More responsive
97 const SUPPORT_MAX_SPEED = 8; // Higher max speed
98
99 // Touch/mouse control constants
100 const MOUSE_LAG_FACTOR = 0.12; // How much lag in mouse following
101 const MOUSE_SPEED_LIMIT = 4; // Max speed for mouse movement
102 const TOUCH_SENSITIVITY = 1.2; // Touch movement multiplier
103
104 // Spring physics constants (adjusted for better oscillation)
105 const PADDLE_MASS = 0.8; // Back to more stable value
106 const SPRING_LENGTH = 50; // Increased from 40 for more room
107 const SPRING_DAMPING = 0.6; // More damping = less erratic
108 const SPRING_STIFFNESS = 0.025; // Slightly lower for smoother motion
109
110 // AI difficulty settings
111 const AI_SETTINGS = {
112 easy: {
113 reactionTime: 400, // ms delay
114 accuracy: 0.7, // 70% accuracy
115 speed: 0.6, // 60% of normal speed
116 prediction: 0.3, // 30% prediction vs reaction
117 aggression: 0.2, // Low aggression
118 oscillation: 0.3 // Minimal oscillation
119 },
120 medium: {
121 reactionTime: 250,
122 accuracy: 0.85,
123 speed: 0.8,
124 prediction: 0.6,
125 aggression: 0.5, // Moderate aggression
126 oscillation: 0.7 // Good oscillation technique
127 },
128 hard: {
129 reactionTime: 150,
130 accuracy: 0.95,
131 speed: 1.0,
132 prediction: 0.8,
133 aggression: 0.8, // High aggression
134 oscillation: 1.0 // Master-level oscillation
135 }
136 };
137
138 // Visual enhancement constants
139 const TRAIL_SEGMENTS = 8;
140 const PADDLE_GLOW_DISTANCE = 25;
141 const SPRING_GLOW_INTENSITY = 120; // More intense glow
142
143 // Particle system constants
144 const MAX_PARTICLES = 100;
145 const PARTICLE_LIFE = 60;
146 const IMPACT_PARTICLES = 8;
147 const SPRING_PARTICLE_RATE = 0.3;
148
149 function setup() {
150 // Create p5.js canvas
151 let canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
152 canvas.parent('gameCanvas');
153
154 // Initialize Matter.js physics engine
155 engine = Engine.create();
156 world = engine.world;
157
158 // Disable gravity for classic Pong feel
159 engine.world.gravity.y = 0;
160 engine.world.gravity.x = 0;
161
162 // Create game boundaries (top and bottom walls)
163 let topWall = Bodies.rectangle(width/2, -10, width, 20, { isStatic: true });
164 let bottomWall = Bodies.rectangle(width/2, height + 10, width, 20, { isStatic: true });
165 boundaries.push(topWall, bottomWall);
166
167 // Create spring paddle systems
168 createSpringPaddleSystem('left');
169 createSpringPaddleSystem('right');
170
171 // Create ball
172 resetBall();
173
174 // Add everything to the world
175 World.add(world, [
176 ...boundaries,
177 ball,
178 leftSupport, leftPaddle, leftSpring,
179 rightSupport, rightPaddle, rightSpring
180 ]);
181
182 console.log("Sprong Phase 5 Complete!");
183 console.log("Particle effects system");
184 console.log("Tuned physics for maximum bounce");
185 console.log("Faster, more responsive paddles");
186 }
187
188 function createSpringPaddleSystem(side) {
189 let supportX = side === 'left' ? 60 : width - 60;
190 let paddleX = side === 'left' ? 60 + SPRING_LENGTH : width - 60 - SPRING_LENGTH;
191 let startY = height / 2;
192
193 if (side === 'left') {
194 // Left support (invisible anchor point controlled by player)
195 leftSupport = Bodies.rectangle(supportX, startY, 10, 10, {
196 isStatic: true,
197 render: { visible: false }
198 });
199
200 // Left paddle (the actual hitting surface)
201 leftPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
202 mass: PADDLE_MASS,
203 restitution: 1.3, // Even bouncier!
204 friction: 0,
205 frictionAir: 0.005 // Less air resistance
206 });
207
208 // Spring constraint connecting support to paddle
209 leftSpring = Constraint.create({
210 bodyA: leftSupport,
211 bodyB: leftPaddle,
212 length: SPRING_LENGTH,
213 stiffness: SPRING_STIFFNESS,
214 damping: SPRING_DAMPING
215 });
216 } else {
217 // Right support (invisible anchor point controlled by player/AI)
218 rightSupport = Bodies.rectangle(supportX, startY, 10, 10, {
219 isStatic: true,
220 render: { visible: false }
221 });
222
223 // Right paddle (the actual hitting surface)
224 rightPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
225 mass: PADDLE_MASS,
226 restitution: 1.2, // Slightly toned down
227 friction: 0,
228 frictionAir: 0.008 // Bit more air resistance for stability
229 });
230
231 // Spring constraint connecting support to paddle
232 rightSpring = Constraint.create({
233 bodyA: rightSupport,
234 bodyB: rightPaddle,
235 length: SPRING_LENGTH,
236 stiffness: SPRING_STIFFNESS,
237 damping: SPRING_DAMPING
238 });
239 }
240 }
241
242 function draw() {
243 // Update physics
244 Engine.update(engine);
245
246 // Clear canvas
247 background(10, 10, 10);
248
249 if (gameState === 'menu') {
250 drawMenu();
251 } else {
252 // Handle enhanced player input
253 handleEnhancedInput();
254
255 // Update particle systems
256 updateParticles();
257 checkCollisions();
258
259 // Check for scoring
260 checkBallPosition();
261
262 // Draw particles behind everything
263 drawParticles();
264
265 // Draw game objects with enhanced visuals
266 drawSpringPaddleSystemsEnhanced();
267 drawBallEnhanced();
268 drawBoundaries();
269 drawCenterLine();
270
271 // Draw debug info
272 drawDebugInfo();
273
274 // Start message
275 if (!gameStarted) {
276 drawStartMessage();
277 }
278 }
279 }
280
281 function handleEnhancedInput() {
282 // Handle both keyboard and mouse/touch input
283 handleKeyboardInput();
284 handleMouseTouchInput();
285
286 // Handle AI if enabled
287 if (aiEnabled && gameStarted) {
288 handleAI();
289 }
290 }
291
292 function handleKeyboardInput() {
293 // Smooth input accumulation with acceleration
294 let leftInput = 0;
295 let rightInput = 0;
296
297 // Left paddle input (W/S keys) - always player controlled
298 if (keys['w'] || keys['W']) leftInput -= 1;
299 if (keys['s'] || keys['S']) leftInput += 1;
300
301 // Right paddle input (Arrow keys) - only if AI is disabled
302 if (!aiEnabled) {
303 if (keys['ArrowUp']) rightInput -= 1;
304 if (keys['ArrowDown']) rightInput += 1;
305 }
306
307 // Apply acceleration and smoothing for keyboard
308 inputBuffer.left = lerp(inputBuffer.left, leftInput, INPUT_SMOOTHING);
309 if (!aiEnabled) {
310 inputBuffer.right = lerp(inputBuffer.right, rightInput, INPUT_SMOOTHING);
311 }
312
313 // Move supports with enhanced physics (only if not using mouse)
314 if (!mouseInput.active) {
315 if (Math.abs(inputBuffer.left) > 0.01) {
316 moveSupportEnhanced(leftSupport, inputBuffer.left * SUPPORT_SPEED);
317 }
318 if (!aiEnabled && Math.abs(inputBuffer.right) > 0.01) {
319 moveSupportEnhanced(rightSupport, inputBuffer.right * SUPPORT_SPEED);
320 }
321 }
322 }
323
324 function handleMouseTouchInput() {
325 if (!mouseInput.active) return;
326
327 // Determine which paddle to control based on mouse X position
328 let controllingLeft = mouseX < width / 2;
329
330 // Don't allow mouse control of AI paddle
331 if (!controllingLeft && aiEnabled) return;
332
333 let targetSupport = controllingLeft ? leftSupport : rightSupport;
334
335 // Calculate target Y with dead zone
336 let currentY = targetSupport.position.y;
337 let targetY = mouseY;
338 let deltaY = targetY - currentY;
339
340 // Apply dead zone - don't move unless mouse is far enough
341 if (Math.abs(deltaY) < mouseInput.deadZone) {
342 return;
343 }
344
345 // Calculate movement with lag and speed limiting
346 let movement = deltaY * MOUSE_LAG_FACTOR * TOUCH_SENSITIVITY;
347
348 // Limit maximum speed to prevent snappy movement
349 movement = constrain(movement, -MOUSE_SPEED_LIMIT, MOUSE_SPEED_LIMIT);
350
351 // Apply the lagged movement
352 moveSupportEnhanced(targetSupport, movement);
353
354 // Visual feedback - update input buffer for particle effects
355 if (controllingLeft) {
356 inputBuffer.left = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
357 } else if (!aiEnabled) {
358 inputBuffer.right = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
359 }
360 }
361
362 function handleAI() {
363 let currentTime = millis();
364 let ballPos = ball.position;
365 let ballVel = ball.velocity;
366 let aiSettings = AI_SETTINGS[aiState.difficulty];
367
368 // Update aggression based on score difference and time
369 updateAIAggression();
370
371 // Add lifelike micro-movements
372 updateAILifelikeBehavior(currentTime);
373
374 // Advanced AI state machine
375 switch (aiState.mode) {
376 case 'TRACKING':
377 handleAITracking(currentTime, ballPos, ballVel, aiSettings);
378 break;
379 case 'WINDING_UP':
380 handleAIWindup(currentTime, ballPos, ballVel, aiSettings);
381 break;
382 case 'SWINGING':
383 handleAISwing(currentTime, ballPos, ballVel, aiSettings);
384 break;
385 case 'RECOVERING':
386 handleAIRecovery(currentTime, ballPos, ballVel, aiSettings);
387 break;
388 case 'ANTICIPATING':
389 handleAIAnticipation(currentTime, ballPos, ballVel, aiSettings);
390 break;
391 }
392
393 // Apply movement with spring physics awareness
394 executeAIMovement(aiSettings);
395 }
396
397 function updateAILifelikeBehavior(currentTime) {
398 // Subtle breathing-like motion when not actively engaged
399 aiState.breathingOffset = sin(currentTime * 0.003) * 3;
400
401 // Random micro-adjustments every few seconds
402 if (currentTime - aiState.lastMicroTime > 2000 + random(1000)) {
403 aiState.microAdjustment = (random() - 0.5) * 15;
404 aiState.lastMicroTime = currentTime;
405 }
406
407 // Gradually decay micro-adjustment
408 aiState.microAdjustment *= 0.98;
409
410 // Update idle target with slight wandering
411 if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
412 let centerY = height / 2;
413 let wanderRadius = 25;
414 aiState.idleTarget = centerY + sin(currentTime * 0.002) * wanderRadius;
415 }
416 }
417
418 function updateAIAggression() {
419 // Increase aggression when losing
420 let scoreDiff = leftScore - rightScore;
421 let baseAggression = AI_SETTINGS[aiState.difficulty].aggression;
422
423 // Rage mode when losing by 2+ points
424 if (scoreDiff >= 2) {
425 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.3);
426 } else if (scoreDiff >= 1) {
427 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.15);
428 } else {
429 aiState.aggressionLevel = baseAggression;
430 }
431 }
432
433 function handleAITracking(currentTime, ballPos, ballVel, aiSettings) {
434 // Always track ball position for more responsive movement
435 let ballApproaching = ballVel.x > 0;
436 let ballDistance = width - ballPos.x;
437 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
438
439 // Calculate where the AI paddle currently is (not the anchor)
440 let paddlePos = rightPaddle.position;
441 let anchorPos = rightSupport.position;
442
443 // Continuous ball tracking with paddle awareness
444 let trackingIntensity = ballApproaching ? 0.08 : 0.03;
445
446 // Calculate where anchor should be to position PADDLE at target Y
447 let desiredPaddleY = ballPos.y + aiState.microAdjustment;
448 desiredPaddleY = constrain(desiredPaddleY, 80, height - 80);
449
450 // Estimate anchor position needed to get paddle to desired position
451 // This is tricky because spring physics affects paddle position
452 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
453 let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
454
455 // Always apply some level of paddle-aware tracking
456 aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity);
457
458 // Only do advanced prediction and windup logic if enough time has passed (reaction delay)
459 if (currentTime - aiState.lastUpdateTime > aiSettings.reactionTime) {
460
461 if (ballApproaching && ballDistance < 300) {
462 // Calculate intercept point with advanced prediction
463 let timeToReach = ballDistance / Math.abs(ballVel.x);
464 let predictedBallY = ballPos.y + ballVel.y * timeToReach;
465
466 // Account for wall bounces
467 if (predictedBallY < 50) {
468 predictedBallY = 100 - predictedBallY;
469 } else if (predictedBallY > height - 50) {
470 predictedBallY = 2 * (height - 50) - predictedBallY;
471 }
472
473 // Add accuracy error
474 let error = (random() - 0.5) * 35 * (1 - aiSettings.accuracy);
475 predictedBallY += error;
476
477 // Calculate where PADDLE needs to be to hit the ball
478 aiState.interceptY = predictedBallY;
479
480 // Calculate where ANCHOR needs to be to position paddle correctly
481 let interceptAnchorOffset = calculateAnchorOffset(aiState.interceptY, paddlePos, anchorPos);
482 let targetAnchorForIntercept = aiState.interceptY + interceptAnchorOffset;
483
484 // VERY selective windup decision: only for very slow balls
485 let shouldWindUp = ballSpeed < 4.5 && // Much stricter - very slow balls only
486 ballDistance > 200 && // Lots of distance required
487 Math.abs(ballVel.y) < 3 && // Very limited vertical movement
488 Math.abs(ballVel.x) > 1 && // Ball must be actually moving toward AI
489 random() < aiSettings.oscillation * aiState.aggressionLevel * 0.3; // Much lower chance
490
491 if (shouldWindUp) {
492 // Start winding up for power shot
493 aiState.mode = 'WINDING_UP';
494 aiState.windupStartTime = currentTime;
495
496 // Determine windup direction (opposite of where paddle needs to be)
497 aiState.windupDirection = aiState.interceptY > paddlePos.y ? -1 : 1;
498
499 } else {
500 // Use paddle-aware intercept positioning
501 aiState.targetY = lerp(aiState.targetY, targetAnchorForIntercept, 0.3);
502 }
503
504 aiState.lastUpdateTime = currentTime;
505 }
506 }
507 }
508
509 // Helper function to estimate where anchor should be to position paddle at target Y
510 function calculateAnchorOffset(targetPaddleY, currentPaddlePos, currentAnchorPos) {
511 // Calculate current spring vector
512 let springVectorY = currentPaddlePos.y - currentAnchorPos.y;
513
514 // The paddle tends to lag behind the anchor due to spring physics
515 // We need to account for this offset when positioning
516
517 // Simple approximation: if spring is compressed/extended, paddle will be offset
518 let currentSpringLength = dist(currentAnchorPos.x, currentAnchorPos.y,
519 currentPaddlePos.x, currentPaddlePos.y);
520 let springCompression = SPRING_LENGTH - currentSpringLength;
521
522 // Estimate the Y offset the paddle will have relative to anchor
523 // This is a simplified physics approximation
524 let estimatedPaddleOffset = springVectorY * 0.8; // Paddle lags behind anchor
525
526 // Return the offset needed for anchor positioning
527 return -estimatedPaddleOffset;
528 }
529
530 function handleAIWindup(currentTime, ballPos, ballVel, aiSettings) {
531 let windupTime = currentTime - aiState.windupStartTime;
532 let maxWindupTime = 500 / Math.max(aiState.aggressionLevel, 0.3); // Longer windup for bigger movement
533
534 // Calculate windup target with much bigger distance
535 let paddlePos = rightPaddle.position;
536 let windupTarget = paddlePos.y + aiState.windupDirection * aiState.windupDistance * aiState.aggressionLevel;
537 windupTarget = constrain(windupTarget, 50, height - 50); // Allow closer to edges for big windup
538
539 // Convert paddle target to anchor target using paddle awareness
540 let anchorOffsetNeeded = calculateAnchorOffset(windupTarget, paddlePos, rightSupport.position);
541 aiState.targetY = windupTarget + anchorOffsetNeeded;
542
543 // Check if it's time to swing - allow more time for bigger windups
544 let ballDistance = width - ballPos.x;
545 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
546
547 let shouldSwing = windupTime > maxWindupTime ||
548 ballDistance < 120 ||
549 ballSpeed > 6; // Abort if ball speeds up even slightly
550
551 if (shouldSwing) {
552 aiState.mode = 'SWINGING';
553 aiState.swingStartTime = currentTime;
554 }
555 }
556
557 function handleAISwing(currentTime, ballPos, ballVel, aiSettings) {
558 // Aggressive swing toward intercept point - but position anchor for paddle placement
559 let paddlePos = rightPaddle.position;
560
561 // Calculate where anchor should be to get paddle to intercept point
562 let anchorOffsetNeeded = calculateAnchorOffset(aiState.interceptY, paddlePos, rightSupport.position);
563 aiState.targetY = aiState.interceptY + anchorOffsetNeeded;
564
565 let swingTime = currentTime - aiState.swingStartTime;
566 let maxSwingTime = aiState.timingWindow;
567
568 // Check if swing is complete
569 if (swingTime > maxSwingTime || Math.abs(ball.velocity.x) < 2) {
570 aiState.mode = 'RECOVERING';
571 aiState.lastHitTime = currentTime;
572 }
573 }
574
575 function handleAIRecovery(currentTime, ballPos, ballVel, aiSettings) {
576 // Return to idle position with lifelike movement
577 aiState.targetY = aiState.idleTarget + aiState.breathingOffset;
578
579 let recoveryTime = currentTime - aiState.lastHitTime;
580 if (recoveryTime > 400) { // Faster recovery
581 aiState.mode = 'ANTICIPATING';
582 }
583 }
584
585 function handleAIAnticipation(currentTime, ballPos, ballVel, aiSettings) {
586 // Stay near center with subtle lifelike movements, but use paddle-aware positioning
587 let baseTarget = aiState.idleTarget + aiState.breathingOffset + aiState.microAdjustment;
588 let ballTrackingTarget = ballPos.y;
589
590 // Blend idle position with gentle ball tracking
591 let desiredPaddleY = lerp(baseTarget, ballTrackingTarget, 0.15);
592
593 // Convert paddle target to anchor target
594 let paddlePos = rightPaddle.position;
595 let anchorPos = rightSupport.position;
596 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
597 aiState.targetY = desiredPaddleY + anchorOffsetNeeded;
598
599 // Switch back to tracking when ball changes direction
600 if (ballVel.x > 0) {
601 aiState.mode = 'TRACKING';
602 }
603 }
604
605 function executeAIMovement(aiSettings) {
606 // Move AI paddle toward target with speed limitation
607 let currentY = rightSupport.position.y;
608 let deltaY = aiState.targetY - currentY;
609
610 if (Math.abs(deltaY) > 1) { // Very small dead zone for responsive tracking
611 let baseSpeed = 0.08 * aiSettings.speed; // Back to more responsive speed
612
613 // Apply swing power during swing phase
614 if (aiState.mode === 'SWINGING') {
615 baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.3);
616 } else if (aiState.mode === 'WINDING_UP') {
617 baseSpeed *= 0.6; // Slower during windup for more deliberate movement
618 }
619
620 // Apply aggression multiplier
621 baseSpeed *= (1 + aiState.aggressionLevel * 0.2);
622
623 let movement = deltaY * baseSpeed;
624 movement = constrain(movement, -SUPPORT_SPEED * 0.9, SUPPORT_SPEED * 0.9);
625
626 moveSupportEnhanced(rightSupport, movement);
627
628 // Update input buffer for visual effects
629 inputBuffer.right = constrain(movement / (SUPPORT_SPEED * 0.9), -1, 1);
630 } else {
631 // Gradually reduce input buffer when AI is not moving
632 inputBuffer.right *= 0.95;
633 }
634 }
635
636 function moveSupportEnhanced(support, deltaY) {
637 let newY = support.position.y + deltaY;
638
639 // Keep support within reasonable bounds with smooth clamping
640 let minY = 50;
641 let maxY = height - 50;
642
643 if (newY < minY) {
644 newY = minY + (newY - minY) * 0.1; // Soft boundary
645 } else if (newY > maxY) {
646 newY = maxY + (newY - maxY) * 0.1; // Soft boundary
647 }
648
649 Body.setPosition(support, { x: support.position.x, y: newY });
650 }
651
652 function checkCollisions() {
653 let ballPos = ball.position;
654 let ballVel = ball.velocity;
655
656 // Check paddle collisions for particle effects
657 let leftDist = dist(ballPos.x, ballPos.y, leftPaddle.position.x, leftPaddle.position.y);
658 let rightDist = dist(ballPos.x, ballPos.y, rightPaddle.position.x, rightPaddle.position.y);
659
660 // Collision threshold
661 let collisionDist = BALL_RADIUS + PADDLE_WIDTH/2 + 5;
662
663 // Left paddle collision
664 if (leftDist < collisionDist && ballVel.x < 0) {
665 createImpactParticles(ballPos.x, ballPos.y, ballVel.x, ballVel.y);
666 }
667
668 // Right paddle collision
669 if (rightDist < collisionDist && ballVel.x > 0) {
670 createImpactParticles(ballPos.x, ballPos.y, ballVel.x, ballVel.y);
671 }
672 }
673
674 function createImpactParticles(x, y, velX, velY) {
675 for (let i = 0; i < IMPACT_PARTICLES; i++) {
676 let angle = random(TWO_PI);
677 let speed = random(2, 8);
678 let size = random(2, 6);
679
680 particles.push({
681 x: x + random(-5, 5),
682 y: y + random(-5, 5),
683 vx: cos(angle) * speed - velX * 0.2,
684 vy: sin(angle) * speed - velY * 0.2,
685 size: size,
686 life: PARTICLE_LIFE,
687 maxLife: PARTICLE_LIFE,
688 color: { r: 255, g: random(100, 255), b: random(100, 150) },
689 type: 'impact'
690 });
691 }
692 }
693
694 function createSpringParticles(springPos, compression) {
695 if (random() < SPRING_PARTICLE_RATE * compression) {
696 let angle = random(TWO_PI);
697 let speed = random(1, 3) * compression;
698
699 particles.push({
700 x: springPos.x + random(-10, 10),
701 y: springPos.y + random(-10, 10),
702 vx: cos(angle) * speed,
703 vy: sin(angle) * speed,
704 size: random(1, 3),
705 life: PARTICLE_LIFE * 0.5,
706 maxLife: PARTICLE_LIFE * 0.5,
707 color: { r: 0, g: 255, b: 136 },
708 type: 'spring'
709 });
710 }
711 }
712
713 function updateParticles() {
714 // Update and remove dead particles
715 for (let i = particles.length - 1; i >= 0; i--) {
716 let p = particles[i];
717
718 // Update position
719 p.x += p.vx;
720 p.y += p.vy;
721
722 // Apply drag
723 p.vx *= 0.98;
724 p.vy *= 0.98;
725
726 // Update life
727 p.life--;
728
729 // Remove dead particles
730 if (p.life <= 0) {
731 particles.splice(i, 1);
732 }
733 }
734
735 // Limit particle count
736 if (particles.length > MAX_PARTICLES) {
737 particles.splice(0, particles.length - MAX_PARTICLES);
738 }
739 }
740
741 function drawParticles() {
742 for (let p of particles) {
743 let alpha = map(p.life, 0, p.maxLife, 0, 255);
744
745 push();
746 translate(p.x, p.y);
747
748 if (p.type === 'impact') {
749 // Impact particles: bright sparks
750 fill(p.color.r, p.color.g, p.color.b, alpha);
751 noStroke();
752 ellipse(0, 0, p.size, p.size);
753
754 // Add glow
755 fill(p.color.r, p.color.g, p.color.b, alpha * 0.3);
756 ellipse(0, 0, p.size * 2, p.size * 2);
757
758 } else if (p.type === 'spring') {
759 // Spring particles: green energy
760 fill(p.color.r, p.color.g, p.color.b, alpha);
761 noStroke();
762 ellipse(0, 0, p.size, p.size);
763 }
764
765 pop();
766 }
767 }
768
769 function drawSpringPaddleSystemsEnhanced() {
770 // Draw springs with enhanced visuals and particles
771 drawSpringsEnhanced();
772
773 // Draw paddles with glow effects
774 drawPaddlesWithGlow();
775
776 // Draw support points with input feedback
777 drawSupportPointsEnhanced();
778 }
779
780 function drawSpringsEnhanced() {
781 // Left spring
782 let leftSupportPos = leftSupport.position;
783 let leftPaddlePos = leftPaddle.position;
784 let leftCompression = drawSpringLineEnhanced(leftSupportPos, leftPaddlePos);
785 createSpringParticles(leftPaddlePos, leftCompression);
786
787 // Right spring
788 let rightSupportPos = rightSupport.position;
789 let rightPaddlePos = rightPaddle.position;
790 let rightCompression = drawSpringLineEnhanced(rightSupportPos, rightPaddlePos);
791 createSpringParticles(rightPaddlePos, rightCompression);
792 }
793
794 function drawSpringLineEnhanced(startPos, endPos) {
795 let segments = 12; // More segments for smoother springs
796 let amplitude = 10; // Bigger amplitude for more dramatic effect
797
798 // Calculate spring compression for visual effects
799 let currentLength = dist(startPos.x, startPos.y, endPos.x, endPos.y);
800 let compression = SPRING_LENGTH / currentLength;
801 amplitude *= compression;
802
803 // Enhanced spring glow based on compression
804 let glowIntensity = 150 + compression * SPRING_GLOW_INTENSITY;
805 stroke(0, 255, 136, glowIntensity);
806 strokeWeight(3 + compression * 2); // Thicker when compressed
807
808 // Draw spring coil with smooth curves
809 beginShape();
810 noFill();
811
812 for (let i = 0; i <= segments; i++) {
813 let t = i / segments;
814 let x = lerp(startPos.x, endPos.x, t);
815 let y = lerp(startPos.y, endPos.y, t);
816
817 // Enhanced zigzag with smoother curves
818 if (i > 0 && i < segments) {
819 let perpX = -(endPos.y - startPos.y) / currentLength;
820 let perpY = (endPos.x - startPos.x) / currentLength;
821 let offset = sin(i * PI * 1.5) * amplitude; // More dramatic oscillation
822 x += perpX * offset;
823 y += perpY * offset;
824 }
825
826 vertex(x, y);
827 }
828
829 endShape();
830
831 // Add spring glow effect with pulsing
832 let pulse = sin(frameCount * 0.1) * 0.2 + 1;
833 stroke(0, 255, 136, glowIntensity * 0.4 * pulse);
834 strokeWeight(8 + compression * 3);
835 beginShape();
836 noFill();
837
838 for (let i = 0; i <= segments; i++) {
839 let t = i / segments;
840 let x = lerp(startPos.x, endPos.x, t);
841 let y = lerp(startPos.y, endPos.y, t);
842 vertex(x, y);
843 }
844
845 endShape();
846
847 return compression; // Return compression for particle effects
848 }
849
850 function drawPaddlesWithGlow() {
851 // Calculate ball distance for glow effects
852 let ballPos = ball.position;
853 let leftDist = dist(ballPos.x, ballPos.y, leftPaddle.position.x, leftPaddle.position.y);
854 let rightDist = dist(ballPos.x, ballPos.y, rightPaddle.position.x, rightPaddle.position.y);
855
856 // Enhanced paddle drawing
857 drawSinglePaddleEnhanced(leftPaddle, leftDist);
858 drawSinglePaddleEnhanced(rightPaddle, rightDist);
859 }
860
861 function drawSinglePaddleEnhanced(paddle, ballDistance) {
862 let pos = paddle.position;
863 let angle = paddle.angle;
864
865 // Check if this is the AI paddle
866 let isAI = aiEnabled && paddle === rightPaddle;
867
868 // Calculate glow intensity based on ball proximity
869 let glowIntensity = map(ballDistance, 0, PADDLE_GLOW_DISTANCE, 150, 0);
870 glowIntensity = constrain(glowIntensity, 0, 150);
871
872 // Add AI state-based effects
873 if (isAI) {
874 // Enhance glow during aggressive states
875 if (aiState.mode === 'WINDING_UP') {
876 glowIntensity += 50;
877 } else if (aiState.mode === 'SWINGING') {
878 glowIntensity += 100;
879 }
880
881 // Aggression-based glow
882 glowIntensity += aiState.aggressionLevel * 30;
883 }
884
885 push();
886 translate(pos.x, pos.y);
887 rotate(angle);
888
889 // Different color scheme for AI paddle
890 let paddleColor = isAI ? [255, 100, 100] : [0, 255, 136]; // Red for AI, green for player
891
892 // AI mode indicator colors
893 if (isAI && aiState.mode === 'WINDING_UP') {
894 paddleColor = [255, 150, 50]; // Orange during windup
895 } else if (isAI && aiState.mode === 'SWINGING') {
896 paddleColor = [255, 50, 50]; // Bright red during swing
897 }
898
899 // Draw enhanced glow effect first
900 if (glowIntensity > 0) {
901 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.6);
902 noStroke();
903 rectMode(CENTER);
904 rect(0, 0, PADDLE_WIDTH + 12, PADDLE_HEIGHT + 12);
905
906 // Add outer glow
907 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.3);
908 rect(0, 0, PADDLE_WIDTH + 20, PADDLE_HEIGHT + 20);
909 }
910
911 // Draw main paddle with enhanced visual
912 fill(paddleColor[0], paddleColor[1], paddleColor[2]);
913 stroke(paddleColor[0], paddleColor[1], paddleColor[2], 220 + glowIntensity * 0.5);
914 strokeWeight(3);
915 rectMode(CENTER);
916 rect(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT);
917
918 // Add core highlight
919 if (isAI) {
920 fill(255, 200, 200, 100); // Light red highlight for AI
921 } else {
922 fill(150, 255, 200, 100); // Light green highlight for player
923 }
924 noStroke();
925 rect(0, 0, PADDLE_WIDTH - 4, PADDLE_HEIGHT - 4);
926
927 pop();
928 }
929
930 function drawSupportPointsEnhanced() {
931 // Enhanced support indicators with input feedback
932 let leftActivity = Math.abs(inputBuffer.left) * 255;
933 let rightActivity = Math.abs(inputBuffer.right) * 255;
934
935 // Left support with pulsing effect
936 let leftPulse = sin(frameCount * 0.2) * 0.3 + 1;
937 fill(0, 255, 136, 100 + leftActivity * 0.6);
938 noStroke();
939 ellipse(leftSupport.position.x, leftSupport.position.y,
940 (8 + leftActivity * 0.15) * leftPulse,
941 (8 + leftActivity * 0.15) * leftPulse);
942
943 // Right support with pulsing effect
944 let rightPulse = sin(frameCount * 0.2 + PI) * 0.3 + 1;
945 fill(0, 255, 136, 100 + rightActivity * 0.6);
946 ellipse(rightSupport.position.x, rightSupport.position.y,
947 (8 + rightActivity * 0.15) * rightPulse,
948 (8 + rightActivity * 0.15) * rightPulse);
949 }
950
951 function drawBallEnhanced() {
952 let ballPos = ball.position;
953 let ballVel = ball.velocity;
954 let speed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
955
956 // Enhanced ball with speed-based effects
957 let speedIntensity = map(speed, 0, 15, 50, 255);
958
959 // Multi-layered trail effect
960 for (let i = 0; i < 3; i++) {
961 let offset = i * 3;
962 fill(255, 100, 100, 40 - i * 10);
963 noStroke();
964 ellipse(ballPos.x - ballVel.x * offset * 0.1,
965 ballPos.y - ballVel.y * offset * 0.1,
966 BALL_RADIUS * (4 - i), BALL_RADIUS * (4 - i));
967 }
968
969 // Main ball with enhanced glow
970 fill(255, 100, 100);
971 stroke(255, 200, 200, speedIntensity);
972 strokeWeight(3 + speed * 0.15);
973 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 2, BALL_RADIUS * 2);
974
975 // Speed indicator core
976 if (speed > 8) {
977 fill(255, 255, 255, speedIntensity * 0.8);
978 noStroke();
979 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 0.8, BALL_RADIUS * 0.8);
980 }
981
982 // Outer energy ring for high speeds
983 if (speed > 12) {
984 noFill();
985 stroke(255, 255, 255, speedIntensity * 0.5);
986 strokeWeight(2);
987 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 3, BALL_RADIUS * 3);
988 }
989 }
990
991 function drawBoundaries() {
992 stroke(0, 255, 136, 30);
993 strokeWeight(1);
994 noFill();
995 line(0, 0, width, 0);
996 line(0, height, width, height);
997 }
998
999 function drawCenterLine() {
1000 stroke(0, 255, 136, 50);
1001 strokeWeight(2);
1002
1003 for (let y = 0; y < height; y += 20) {
1004 line(width/2, y, width/2, y + 10);
1005 }
1006 }
1007
1008 function drawDebugInfo() {
1009 fill(255, 100);
1010 textAlign(LEFT);
1011 textSize(12);
1012 text(`FPS: ${Math.round(frameRate())}`, 10, 20);
1013 text(`Ball Speed: ${Math.round(getBallSpeed())}`, 10, 35);
1014 text(`Particles: ${particles.length}`, 10, 50);
1015 text(`Mode: ${aiEnabled ? 'vs CPU' : '2 Player'} | Difficulty: ${aiState.difficulty}`, 10, 65);
1016
1017 // Enhanced spring info
1018 let leftSpringLength = dist(leftSupport.position.x, leftSupport.position.y,
1019 leftPaddle.position.x, leftPaddle.position.y);
1020 let rightSpringLength = dist(rightSupport.position.x, rightSupport.position.y,
1021 rightPaddle.position.x, rightPaddle.position.y);
1022
1023 text(`L Spring: ${Math.round(leftSpringLength)}px (${((SPRING_LENGTH/leftSpringLength - 1) * 100).toFixed(0)}%)`, 10, 80);
1024 text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 95);
1025 text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 110);
1026
1027 // Advanced AI debug info
1028 if (aiEnabled) {
1029 text(`AI State: ${aiState.mode} | Aggression: ${aiState.aggressionLevel.toFixed(2)}`, 10, 125);
1030 text(`Target: ${Math.round(aiState.targetY)} | Intercept: ${Math.round(aiState.interceptY)}`, 10, 140);
1031 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);
1032
1033 // Show AI technique indicators
1034 if (aiState.mode === 'WINDING_UP') {
1035 fill(255, 150, 50, 200);
1036 text("🔄 AI WINDING UP FOR POWER SHOT", 10, 175);
1037 } else if (aiState.mode === 'SWINGING') {
1038 fill(255, 50, 50, 200);
1039 text("⚡ AI POWER SWING!", 10, 175);
1040 }
1041 }
1042
1043 // Mouse/touch input debug
1044 if (mouseInput.active) {
1045 text(`Mouse: Active | Side: ${mouseX < width/2 ? 'Left' : 'Right'} | Y: ${mouseY}`, 10, 190);
1046 }
1047 }
1048
1049 function drawMenu() {
1050 // Draw animated background
1051 drawMenuBackground();
1052
1053 // Main title
1054 push();
1055 let titlePulse = sin(frameCount * 0.05) * 0.2 + 1;
1056 fill(0, 255, 136);
1057 textAlign(CENTER);
1058 textSize(60 * titlePulse);
1059 text("SPRONG", width/2, 120);
1060
1061 // Subtitle
1062 fill(0, 255, 136, 150);
1063 textSize(16);
1064 text("Physics-based Pong with Spring Paddles", width/2, 150);
1065 pop();
1066
1067 // Menu options
1068 let startY = height/2 - 20;
1069 let spacing = 60;
1070
1071 for (let i = 0; i < menuState.options.length; i++) {
1072 let y = startY + i * spacing;
1073 let isSelected = i === menuState.selectedOption;
1074
1075 // Selection indicator
1076 if (isSelected) {
1077 push();
1078 let pulse = sin(frameCount * 0.15) * 0.3 + 1;
1079 fill(0, 255, 136, 100 * pulse);
1080 noStroke();
1081 rectMode(CENTER);
1082 rect(width/2, y, 300, 45);
1083 pop();
1084 }
1085
1086 // Option text
1087 fill(isSelected ? 255 : 200);
1088 textAlign(CENTER);
1089 textSize(isSelected ? 24 : 20);
1090 text(menuState.options[i], width/2, y + 8);
1091
1092 // Show difficulty selector for 1 Player option
1093 if (i === 0 && isSelected && menuState.showDifficulty) {
1094 fill(0, 255, 136, 180);
1095 textSize(14);
1096 text(`Difficulty: ${menuState.difficulties[menuState.difficultySelected]}`, width/2, y + 28);
1097 text("(Use ← → to change)", width/2, y + 45);
1098 }
1099 }
1100
1101 // Instructions
1102 fill(0, 255, 136, 120);
1103 textAlign(CENTER);
1104 textSize(14);
1105 text("Use ↑↓ to select, ENTER to confirm", width/2, height - 80);
1106 text("or click/touch to select", width/2, height - 60);
1107
1108 // Show controls preview
1109 textSize(12);
1110 fill(255, 100);
1111 if (menuState.selectedOption === 0) {
1112 text("Controls: W/S keys or Mouse/Touch to move paddle", width/2, height - 30);
1113 } else {
1114 text("Controls: Player 1 (W/S) | Player 2 (↑/↓) | Mouse/Touch", width/2, height - 30);
1115 }
1116 }
1117
1118 function drawMenuBackground() {
1119 // Draw subtle animated background elements
1120 push();
1121 stroke(0, 255, 136, 30);
1122 strokeWeight(1);
1123
1124 // Animated grid
1125 for (let x = 0; x < width; x += 40) {
1126 let offset = sin(frameCount * 0.01 + x * 0.01) * 5;
1127 line(x, 0, x, height + offset);
1128 }
1129
1130 for (let y = 0; y < height; y += 40) {
1131 let offset = cos(frameCount * 0.01 + y * 0.01) * 5;
1132 line(0, y, width + offset, y);
1133 }
1134
1135 // Floating particles
1136 for (let i = 0; i < 20; i++) {
1137 let x = (frameCount * 0.5 + i * 137) % width;
1138 let y = (sin(frameCount * 0.01 + i) * 50 + height/2);
1139 let alpha = sin(frameCount * 0.02 + i) * 30 + 50;
1140
1141 fill(0, 255, 136, alpha);
1142 noStroke();
1143 ellipse(x, y, 3, 3);
1144 }
1145 pop();
1146 }
1147
1148 function drawStartMessage() {
1149 fill(0, 255, 136, 200);
1150 textAlign(CENTER);
1151 textSize(20);
1152 text("Press any key to start!", width/2, height/2 + 100);
1153 textSize(14);
1154
1155 if (aiEnabled) {
1156 text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
1157 text(`AI Difficulty: ${aiState.difficulty.toUpperCase()}`, width/2, height/2 + 145);
1158 } else {
1159 text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
1160 }
1161
1162 textSize(12);
1163 fill(0, 255, 136, 120);
1164 text("Press ESC to return to menu", width/2, height/2 + 170);
1165 }
1166
1167 function resetBall() {
1168 if (ball) {
1169 World.remove(world, ball);
1170 }
1171
1172 // Create new ball at center
1173 ball = Bodies.circle(width/2, height/2, BALL_RADIUS, {
1174 restitution: 1,
1175 friction: 0,
1176 frictionAir: 0
1177 });
1178
1179 if (world) {
1180 World.add(world, ball);
1181 }
1182
1183 // Start ball moving after a short delay
1184 setTimeout(() => {
1185 let direction = random() > 0.5 ? 1 : -1;
1186 let angle = random(-PI/6, PI/6);
1187
1188 Body.setVelocity(ball, {
1189 x: direction * BALL_SPEED * cos(angle),
1190 y: BALL_SPEED * sin(angle)
1191 });
1192
1193 gameStarted = true;
1194 }, 1000);
1195 }
1196
1197 function checkBallPosition() {
1198 let ballX = ball.position.x;
1199
1200 if (ballX < -BALL_RADIUS) {
1201 rightScore++;
1202 updateScore();
1203 resetBall();
1204 gameStarted = false;
1205 }
1206
1207 if (ballX > width + BALL_RADIUS) {
1208 leftScore++;
1209 updateScore();
1210 resetBall();
1211 gameStarted = false;
1212 }
1213 }
1214
1215 function updateScore() {
1216 document.getElementById('leftScore').textContent = leftScore;
1217 document.getElementById('rightScore').textContent = rightScore;
1218 }
1219
1220 function getBallSpeed() {
1221 let velocity = ball.velocity;
1222 return Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
1223 }
1224
1225 // Input handling
1226 function keyPressed() {
1227 keys[key] = true;
1228 keys[keyCode] = true;
1229
1230 if (gameState === 'menu') {
1231 handleMenuInput();
1232 return;
1233 }
1234
1235 if (!gameStarted && key !== ' ') {
1236 gameStarted = true;
1237 }
1238
1239 // Toggle game mode (only during gameplay)
1240 if (key === 'm' || key === 'M') {
1241 aiEnabled = !aiEnabled;
1242 gameMode = aiEnabled ? 'vs-cpu' : 'vs-human';
1243 console.log("Switched to " + gameMode + " mode");
1244 }
1245
1246 // Change AI difficulty (only during gameplay)
1247 if (key === 'd' || key === 'D') {
1248 if (aiState.difficulty === 'easy') {
1249 aiState.difficulty = 'medium';
1250 } else if (aiState.difficulty === 'medium') {
1251 aiState.difficulty = 'hard';
1252 } else {
1253 aiState.difficulty = 'easy';
1254 }
1255 console.log("AI difficulty: " + aiState.difficulty);
1256 }
1257
1258 // Reset game with spacebar
1259 if (key === ' ') {
1260 leftScore = 0;
1261 rightScore = 0;
1262 updateScore();
1263 resetBall();
1264 gameStarted = false;
1265
1266 // Reset input buffers
1267 inputBuffer.left = 0;
1268 inputBuffer.right = 0;
1269
1270 // Reset mouse input
1271 mouseInput.active = false;
1272
1273 // Reset AI state
1274 aiState.targetY = height / 2;
1275 aiState.lastUpdateTime = 0;
1276 aiState.mode = 'ANTICIPATING';
1277 aiState.aggressionLevel = AI_SETTINGS[aiState.difficulty].aggression;
1278
1279 // Clear particles
1280 particles = [];
1281
1282 console.log("Game reset!");
1283 }
1284
1285 // Return to menu with ESC
1286 if (keyCode === 27) {
1287 gameState = 'menu';
1288 gameStarted = false;
1289 particles = [];
1290 console.log("Returned to menu");
1291 }
1292 }
1293
1294 function handleMenuInput() {
1295 // Navigate menu with arrow keys
1296 if (keyCode === UP_ARROW) {
1297 menuState.selectedOption = Math.max(0, menuState.selectedOption - 1);
1298 } else if (keyCode === DOWN_ARROW) {
1299 menuState.selectedOption = Math.min(menuState.options.length - 1, menuState.selectedOption + 1);
1300 }
1301
1302 // Change difficulty for 1 Player mode
1303 if (menuState.selectedOption === 0) {
1304 if (keyCode === LEFT_ARROW) {
1305 menuState.difficultySelected = Math.max(0, menuState.difficultySelected - 1);
1306 } else if (keyCode === RIGHT_ARROW) {
1307 menuState.difficultySelected = Math.min(menuState.difficulties.length - 1, menuState.difficultySelected + 1);
1308 }
1309 }
1310
1311 // Confirm selection with ENTER
1312 if (keyCode === ENTER || key === ' ') {
1313 startGameWithSelection();
1314 }
1315 }
1316
1317 function startGameWithSelection() {
1318 // Set game mode based on selection
1319 if (menuState.selectedOption === 0) {
1320 // 1 Player vs CPU
1321 aiEnabled = true;
1322 gameMode = 'vs-cpu';
1323 aiState.difficulty = menuState.difficulties[menuState.difficultySelected].toLowerCase();
1324 } else {
1325 // 2 Player
1326 aiEnabled = false;
1327 gameMode = 'vs-human';
1328 }
1329
1330 // Start the game
1331 gameState = 'playing';
1332 gameStarted = false; // Will start when user presses a key
1333
1334 // Reset game state
1335 leftScore = 0;
1336 rightScore = 0;
1337 updateScore();
1338 resetBall();
1339
1340 // Reset input buffers
1341 inputBuffer.left = 0;
1342 inputBuffer.right = 0;
1343 mouseInput.active = false;
1344
1345 // Reset AI state
1346 aiState.targetY = height / 2;
1347 aiState.lastUpdateTime = 0;
1348
1349 // Clear particles
1350 particles = [];
1351
1352 console.log("Started " + gameMode + " mode" + (aiEnabled ? " - Difficulty: " + aiState.difficulty : ""));
1353 }
1354
1355 function keyReleased() {
1356 keys[key] = false;
1357 keys[keyCode] = false;
1358 }
1359
1360 // Mouse/touch input handlers
1361 function mousePressed() {
1362 if (gameState === 'menu') {
1363 handleMenuClick();
1364 return false;
1365 }
1366
1367 // Start mouse/touch input when clicking in game area
1368 if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) {
1369 mouseInput.active = true;
1370
1371 // Start game if not started
1372 if (!gameStarted) {
1373 gameStarted = true;
1374 }
1375
1376 return false; // Prevent default behavior
1377 }
1378 }
1379
1380 function handleMenuClick() {
1381 let startY = height/2 - 20;
1382 let spacing = 60;
1383
1384 // Check if clicked on menu options
1385 for (let i = 0; i < menuState.options.length; i++) {
1386 let y = startY + i * spacing;
1387
1388 if (mouseY > y - 25 && mouseY < y + 25) {
1389 if (menuState.selectedOption === i) {
1390 // Double click or click on already selected - start game
1391 startGameWithSelection();
1392 } else {
1393 // Select this option
1394 menuState.selectedOption = i;
1395 }
1396 break;
1397 }
1398 }
1399
1400 // Check difficulty selection area for 1 Player mode
1401 if (menuState.selectedOption === 0) {
1402 let diffY = startY + 28;
1403 if (mouseY > diffY && mouseY < diffY + 20) {
1404 // Cycle through difficulties on click
1405 menuState.difficultySelected = (menuState.difficultySelected + 1) % menuState.difficulties.length;
1406 }
1407 }
1408 }
1409
1410 function mouseDragged() {
1411 // Continue mouse/touch input while dragging
1412 if (mouseInput.active) {
1413 return false; // Prevent default behavior
1414 }
1415 }
1416
1417 function mouseReleased() {
1418 // Stop mouse/touch input when releasing
1419 mouseInput.active = false;
1420
1421 // Gradually reduce input buffer when mouse is released
1422 inputBuffer.left *= 0.8;
1423 inputBuffer.right *= 0.8;
1424 }
1425
1426 function touchStarted() {
1427 // Handle touch events same as mouse
1428 return mousePressed();
1429 }
1430
1431 function touchMoved() {
1432 // Handle touch drag same as mouse
1433 return mouseDragged();
1434 }
1435
1436 function touchEnded() {
1437 // Handle touch end same as mouse
1438 mouseReleased();
1439 return false;
1440 }