JavaScript · 60765 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 // Canvas settings
10 const CANVAS_WIDTH = 800;
11 const CANVAS_HEIGHT = 400;
12
13 // Game constants
14 const BALL_SPEED = 6;
15 const BALL_RADIUS = 12;
16 const PADDLE_WIDTH = 20;
17 const PADDLE_HEIGHT = 80;
18
19 // Enhanced movement constants (tuned for faster response)
20 const SUPPORT_SPEED = 6.5; // Bumped up
21 const SUPPORT_ACCEL = 1.2; // Increased acceleration
22 const INPUT_SMOOTHING = 0.25; // More responsive
23 const SUPPORT_MAX_SPEED = 8; // Higher max speed
24
25 // Touch/mouse control constants
26 const MOUSE_SPEED_LIMIT = 4; // Max speed for mouse movement
27 const MOUSE_LAG_FACTOR = 0.12; // How much lag in mouse following
28 const TOUCH_SENSITIVITY = 1.2; // Touch movement multiplier
29
30 // Spring physics constants (using your current settings)
31 const PADDLE_MASS = 0.8;
32 const SPRING_LENGTH = 50;
33 const SPRING_DAMPING = 0.6;
34 const SPRING_STIFFNESS = 0.025;
35
36 // Visual enhancement constants
37 const TRAIL_SEGMENTS = 8;
38 const PADDLE_GLOW_DISTANCE = 25;
39 const SPRING_GLOW_INTENSITY = 120; // More intense glow
40
41 // Particle system constants
42 const MAX_PARTICLES = 100;
43 const PARTICLE_LIFE = 60;
44 const IMPACT_PARTICLES = 8;
45 const SPRING_PARTICLE_RATE = 0.3;
46
47 // Bop system constants
48 const BOP_FORCE = 1.0;
49 const BOP_DURATION = 300;
50 const BOP_COOLDOWN = 500;
51 const ANCHOR_RECOIL = 40; // How far the anchor moves backward during bop
52 const BOP_RANGE = 600; // How far the paddle can thrust forward
53 const BOP_VELOCITY_BOOST = 12; // Initial velocity boost for paddle
54
55 // Game variables
56 let ball;
57 let world;
58 let engine;
59
60 // Particle systems
61 let particles = [];
62 let impactParticles = [];
63
64 // Spring paddle system components
65 let boundaries = [];
66 let leftSupport, leftPaddle, leftSpring;
67 let rightSupport, rightPaddle, rightSpring;
68
69 // Game state
70 let leftScore = 0;
71 let rightScore = 0;
72 let aiEnabled = true;
73 let gameState = 'menu'; // 'menu', 'playing', 'paused'
74 let gameMode = 'vs-cpu'; // 'vs-cpu' or 'vs-human'
75 let gameStarted = false;
76
77 // Menu state
78 let menuState = {
79 selectedOption: 0, // 0 = 1 Player, 1 = 2 Player
80 options: ['1 Player vs CPU', '2 Player'],
81 difficultySelected: 1, // 0 = Easy, 1 = Medium, 2 = Hard
82 difficulties: ['Easy', 'Medium', 'Hard'],
83 showDifficulty: true
84 };
85
86 // AI system
87 let aiState = {
88 targetY: 200,
89 reactionDelay: 0,
90 difficulty: 'medium', // 'easy', 'medium', 'hard'
91 lastBallX: 0,
92 lastUpdateTime: 0,
93
94 // Advanced AI state machine
95 mode: 'TRACKING', // TRACKING, WINDING_UP, SWINGING, RECOVERING, ANTICIPATING
96 windupStartTime: 0,
97 swingStartTime: 0,
98 interceptY: 200,
99 windupDirection: 1, // 1 for up, -1 for down
100 aggressionLevel: 0.5, // 0 = defensive, 1 = maximum aggression
101 lastHitTime: 0,
102
103 // Oscillation parameters (increased for better windup)
104 windupDistance: 120, // Much bigger - about half canvas height
105 swingPower: 1.05, // Reduced from 1.1 for more control
106 timingWindow: 40, // Slightly longer execution window
107
108 // Lifelike movement
109 idleTarget: 200, // Where AI "wants" to be when idle
110 microAdjustment: 0, // Small random movements
111 breathingOffset: 0, // Subtle breathing-like motion
112 lastMicroTime: 0, // For micro-movement timing
113
114 // AI Bop system
115 consideringBop: false, // Is AI thinking about bopping?
116 bopDecisionTime: 0, // When AI decided to bop
117 bopTiming: 200 // How long before ball contact to bop (ms)
118 };
119
120 // Player input
121 let keys = {};
122 let inputBuffer = { left: 0, right: 0 };
123
124 // Bop system
125 let bopState = {
126 left: {
127 active: false,
128 startTime: 0,
129 duration: BOP_DURATION,
130 power: BOP_FORCE,
131 cooldown: BOP_COOLDOWN,
132 lastBopTime: 0,
133 originalPos: null
134 },
135 right: {
136 active: false,
137 startTime: 0,
138 duration: BOP_DURATION,
139 power: BOP_FORCE,
140 cooldown: BOP_COOLDOWN,
141 lastBopTime: 0,
142 originalPos: null
143 }
144 };
145
146 function handleBopInput() {
147 let currentTime = millis();
148
149 // Left player bop - use Left Shift for both modes
150 let leftBopPressed = keys['Shift'] && !keys['Control']; // Left shift (without Ctrl)
151
152 if (leftBopPressed && !bopState.left.active &&
153 currentTime - bopState.left.lastBopTime > bopState.left.cooldown) {
154 activateBop('left', currentTime);
155 }
156
157 // Right player bop (Enter - only in two player mode)
158 if (!aiEnabled) {
159 let rightBopPressed = keys['Enter'];
160
161 if (rightBopPressed && !bopState.right.active &&
162 currentTime - bopState.right.lastBopTime > bopState.right.cooldown) {
163 activateBop('right', currentTime);
164 }
165 }
166
167 // Update active bops
168 updateBopStates(currentTime);
169 }
170
171 function activateBop(side, currentTime) {
172 bopState[side].active = true;
173 bopState[side].startTime = currentTime;
174 bopState[side].lastBopTime = currentTime;
175
176 // Get the relevant bodies
177 let paddle = side === 'left' ? leftPaddle : rightPaddle;
178 let support = side === 'left' ? leftSupport : rightSupport;
179
180 // Calculate direction from support to paddle (this is the bop direction)
181 let dx = paddle.position.x - support.position.x;
182 let dy = paddle.position.y - support.position.y;
183
184 // Normalize direction
185 let magnitude = Math.sqrt(dx * dx + dy * dy);
186 if (magnitude > 0) {
187 dx /= magnitude;
188 dy /= magnitude;
189
190 // Calculate anchor recoil distance
191 let anchorRecoilDistance = ANCHOR_RECOIL * 0.4;
192
193 // Move the support BACKWARD (recoil effect)
194 let newSupportX = support.position.x - dx * anchorRecoilDistance;
195 let newSupportY = support.position.y - dy * anchorRecoilDistance;
196
197 // Apply the support movement
198 Body.setPosition(support, { x: newSupportX, y: newSupportY });
199
200 // Store original support position for recovery
201 bopState[side].originalPos = {
202 x: support.position.x + dx * anchorRecoilDistance,
203 y: support.position.y + dy * anchorRecoilDistance
204 };
205
206 // IMPORTANT: Set paddle velocity directly for immediate forward thrust
207 // This creates the "shooting forward" effect based on BOP_RANGE
208 let forwardSpeed = (BOP_RANGE / SPRING_LENGTH) * BOP_VELOCITY_BOOST;
209 Body.setVelocity(paddle, {
210 x: paddle.velocity.x + dx * forwardSpeed,
211 y: paddle.velocity.y + dy * forwardSpeed
212 });
213
214 // Also apply a strong forward force for continued acceleration
215 Body.applyForce(paddle, paddle.position, {
216 x: dx * bopState[side].power * BOP_RANGE * 0.1,
217 y: dy * bopState[side].power * BOP_RANGE * 0.1
218 });
219
220 // Create particle burst for visual feedback
221 for (let i = 0; i < 5; i++) {
222 let angle = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.5;
223 let speed = Math.random() * 4 + 2;
224 particles.push({
225 x: support.position.x,
226 y: support.position.y,
227 vx: Math.cos(angle) * speed * -1, // Particles go backward
228 vy: Math.sin(angle) * speed * -1,
229 size: Math.random() * 3 + 2,
230 life: 30,
231 maxLife: 30,
232 color: { r: 255, g: 255, b: 100 },
233 type: 'impact'
234 });
235 }
236
237 // Force collision detection update
238 Engine.update(engine, 0);
239 }
240
241 console.log(side + " player BOP!");
242 }
243
244 function updateBopStates(currentTime) {
245 // Update left bop
246 if (bopState.left.active) {
247 let elapsed = currentTime - bopState.left.startTime;
248 let progress = elapsed / bopState.left.duration;
249
250 if (progress >= 1.0) {
251 // Bop finished
252 bopState.left.active = false;
253 bopState.left.originalPos = null;
254 } else {
255 // Smoothly return support to original position
256 if (bopState.left.originalPos) {
257 let support = leftSupport;
258 let currentX = support.position.x;
259 let currentY = support.position.y;
260
261 // Ease back to original position
262 let returnSpeed = 0.15 * (1 - Math.pow(1 - progress, 3)); // Ease out cubic
263 let newX = currentX + (bopState.left.originalPos.x - currentX) * returnSpeed;
264 let newY = currentY + (bopState.left.originalPos.y - currentY) * returnSpeed;
265
266 Body.setPosition(support, { x: newX, y: newY });
267 }
268
269 // Apply range limiting during active bop
270 limitBopRange(leftSupport, leftPaddle);
271 }
272 }
273
274 // Update right bop
275 if (bopState.right.active) {
276 let elapsed = currentTime - bopState.right.startTime;
277 let progress = elapsed / bopState.right.duration;
278
279 if (progress >= 1.0) {
280 // Bop finished
281 bopState.right.active = false;
282 bopState.right.originalPos = null;
283 } else {
284 // Smoothly return support to original position
285 if (bopState.right.originalPos) {
286 let support = rightSupport;
287 let currentX = support.position.x;
288 let currentY = support.position.y;
289
290 // Ease back to original position
291 let returnSpeed = 0.15 * (1 - Math.pow(1 - progress, 3)); // Ease out cubic
292 let newX = currentX + (bopState.right.originalPos.x - currentX) * returnSpeed;
293 let newY = currentY + (bopState.right.originalPos.y - currentY) * returnSpeed;
294
295 Body.setPosition(support, { x: newX, y: newY });
296 }
297
298 // Apply range limiting during active bop
299 limitBopRange(rightSupport, rightPaddle);
300 }
301 }
302 }
303
304 function limitBopRange(support, paddle) {
305 // Calculate current distance
306 let currentDistance = dist(support.position.x, support.position.y,
307 paddle.position.x, paddle.position.y);
308
309 // If paddle is beyond max range (spring length + bop range), pull it back
310 let maxDistance = SPRING_LENGTH + BOP_RANGE;
311 if (currentDistance > maxDistance) {
312 // Calculate direction from support to paddle
313 let dx = paddle.position.x - support.position.x;
314 let dy = paddle.position.y - support.position.y;
315
316 // Normalize
317 let magnitude = Math.sqrt(dx * dx + dy * dy);
318 dx /= magnitude;
319 dy /= magnitude;
320
321 // Set paddle position at max distance
322 let newX = support.position.x + dx * maxDistance;
323 let newY = support.position.y + dy * maxDistance;
324
325 // Preserve some velocity but dampen it
326 let currentVel = paddle.velocity;
327 Body.setPosition(paddle, { x: newX, y: newY });
328 Body.setVelocity(paddle, {
329 x: currentVel.x * 0.7,
330 y: currentVel.y * 0.7
331 });
332 }
333 }
334
335 // Touch/mouse input
336 let mouseInput = {
337 active: false,
338 targetY: 0,
339 leftPaddleTarget: 0,
340 rightPaddleTarget: 0,
341 smoothing: 0.08, // Slower smoothing for deliberate lag
342 deadZone: 15 // Minimum distance before movement starts
343 };
344
345
346 // AI difficulty settings
347 const AI_SETTINGS = {
348 easy: {
349 reactionTime: 400, // ms delay
350 accuracy: 0.7, // 70% accuracy
351 speed: 0.8, // Increased from 0.6
352 prediction: 0.3, // 30% prediction vs reaction
353 aggression: 0.2, // Low aggression
354 oscillation: 0.3 // Minimal oscillation
355 },
356 medium: {
357 reactionTime: 250,
358 accuracy: 0.85,
359 speed: 1.0, // Increased from 0.8
360 prediction: 0.6,
361 aggression: 0.5, // Moderate aggression
362 oscillation: 0.7 // Good oscillation technique
363 },
364 hard: {
365 reactionTime: 150,
366 accuracy: 0.95,
367 speed: 1.2, // Increased from 1.0
368 prediction: 0.8,
369 aggression: 0.8, // High aggression
370 oscillation: 1.0 // Master-level oscillation
371 }
372 };
373
374 function setup() {
375 // Create p5.js canvas
376 let canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
377 canvas.parent('gameCanvas');
378
379 // Initialize Matter.js physics engine
380 engine = Engine.create();
381 world = engine.world;
382
383 // Disable gravity for classic Pong feel
384 engine.world.gravity.y = 0;
385 engine.world.gravity.x = 0;
386
387 // Create game boundaries (top and bottom walls)
388 let topWall = Bodies.rectangle(width/2, -10, width, 20, { isStatic: true });
389 let bottomWall = Bodies.rectangle(width/2, height + 10, width, 20, { isStatic: true });
390 boundaries.push(topWall, bottomWall);
391
392 // Create spring paddle systems
393 createSpringPaddleSystem('left');
394 createSpringPaddleSystem('right');
395
396 // Create ball
397 resetBall();
398
399 // Add everything to the world
400 World.add(world, [
401 ...boundaries,
402 ball,
403 leftSupport, leftPaddle, leftSpring,
404 rightSupport, rightPaddle, rightSpring
405 ]);
406
407 // Set up collision events
408 Matter.Events.on(engine, 'collisionStart', function(event) {
409 let pairs = event.pairs;
410
411 for (let i = 0; i < pairs.length; i++) {
412 let pair = pairs[i];
413
414 // Check if collision involves ball and paddle
415 if ((pair.bodyA === ball && (pair.bodyB === leftPaddle || pair.bodyB === rightPaddle)) ||
416 (pair.bodyB === ball && (pair.bodyA === leftPaddle || pair.bodyA === rightPaddle))) {
417
418 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
419 let isLeftPaddle = paddle === leftPaddle;
420
421 // Apply bop boost if paddle is currently bopping
422 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
423 // Get current velocities
424 let ballVel = ball.velocity;
425 let paddleVel = paddle.velocity;
426
427 // Calculate boost based on paddle velocity
428 let boostX = paddleVel.x * 0.5;
429 let boostY = paddleVel.y * 0.5;
430
431 // Apply extra velocity to ball
432 Body.setVelocity(ball, {
433 x: ballVel.x * 1.3 + boostX,
434 y: ballVel.y * 1.3 + boostY
435 });
436
437 // Create extra impact particles
438 createImpactParticles(ball.position.x, ball.position.y, ballVel.x, ballVel.y);
439 }
440 }
441 }
442 });
443 }
444
445 function createSpringPaddleSystem(side) {
446 let supportX = side === 'left' ? 60 : width - 60;
447 let paddleX = side === 'left' ? 60 + SPRING_LENGTH : width - 60 - SPRING_LENGTH;
448 let startY = height / 2;
449
450 if (side === 'left') {
451 // Left support (invisible anchor point controlled by player)
452 leftSupport = Bodies.rectangle(supportX, startY, 10, 10, {
453 isStatic: true,
454 render: { visible: false }
455 });
456
457 // Left paddle (the actual hitting surface)
458 leftPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
459 mass: PADDLE_MASS,
460 restitution: 1.3, // Even bouncier!
461 friction: 0,
462 frictionAir: 0.005, // Less air resistance
463 isSensor: false,
464 slop: 0.01, // Tighter collision detection
465 render: {
466 fillStyle: '#00ff88'
467 }
468 });
469
470 // Enable continuous collision detection for better bop collisions
471 leftPaddle.collisionFilter = {
472 category: 0x0002,
473 mask: 0xFFFF
474 };
475
476 // Spring constraint connecting support to paddle
477 leftSpring = Constraint.create({
478 bodyA: leftSupport,
479 bodyB: leftPaddle,
480 length: SPRING_LENGTH,
481 stiffness: SPRING_STIFFNESS,
482 damping: SPRING_DAMPING
483 });
484 } else {
485 // Right support (invisible anchor point controlled by player/AI)
486 rightSupport = Bodies.rectangle(supportX, startY, 10, 10, {
487 isStatic: true,
488 render: { visible: false }
489 });
490
491 // Right paddle (the actual hitting surface)
492 rightPaddle = Bodies.rectangle(paddleX, startY, PADDLE_WIDTH, PADDLE_HEIGHT, {
493 mass: PADDLE_MASS,
494 restitution: 1.2, // Slightly toned down
495 friction: 0,
496 frictionAir: 0.008, // Bit more air resistance for stability
497 isSensor: false,
498 slop: 0.01, // Tighter collision detection
499 render: {
500 fillStyle: '#ff6464'
501 }
502 });
503
504 // Enable continuous collision detection for better bop collisions
505 rightPaddle.collisionFilter = {
506 category: 0x0004,
507 mask: 0xFFFF
508 };
509
510 // Spring constraint connecting support to paddle
511 rightSpring = Constraint.create({
512 bodyA: rightSupport,
513 bodyB: rightPaddle,
514 length: SPRING_LENGTH,
515 stiffness: SPRING_STIFFNESS,
516 damping: SPRING_DAMPING
517 });
518 }
519 }
520
521 function draw() {
522 // Update physics
523 Engine.update(engine);
524
525 // Clear canvas
526 background(10, 10, 10);
527
528 if (gameState === 'menu') {
529 drawMenu();
530 } else {
531 // Handle enhanced player input
532 handleEnhancedInput();
533
534 // Update particle systems
535 updateParticles();
536 checkCollisions();
537
538 // Enhanced collision detection during bops - just more frequent updates
539 if (bopState.left.active || bopState.right.active) {
540 // Multiple smaller physics updates for better collision detection
541 Engine.update(engine, 8);
542 Engine.update(engine, 8);
543 }
544
545 // Check for scoring
546 checkBallPosition();
547
548 // Draw particles behind everything
549 drawParticles();
550
551 // Draw game objects with enhanced visuals
552 drawSpringPaddleSystemsEnhanced();
553 drawBallEnhanced();
554 drawBoundaries();
555 drawCenterLine();
556
557 // Draw debug info
558 drawDebugInfo();
559
560 // Start message
561 if (!gameStarted) {
562 drawStartMessage();
563 }
564 }
565 }
566
567 function handleEnhancedInput() {
568 // Handle both keyboard and mouse/touch input
569 handleKeyboardInput();
570 handleMouseTouchInput();
571
572 // Handle bop mechanics
573 handleBopInput();
574
575 // Handle AI if enabled
576 if (aiEnabled && gameStarted) {
577 handleAI();
578 }
579 }
580
581 function handleKeyboardInput() {
582 // Smooth input accumulation with acceleration
583 let leftInput = 0;
584 let rightInput = 0;
585
586 // Left paddle input (W/S keys) - always player controlled
587 if (keys['w'] || keys['W']) leftInput -= 1;
588 if (keys['s'] || keys['S']) leftInput += 1;
589
590 // Right paddle input (Arrow keys) - only if AI is disabled
591 if (!aiEnabled) {
592 if (keys['ArrowUp']) rightInput -= 1;
593 if (keys['ArrowDown']) rightInput += 1;
594 }
595
596 // Apply acceleration and smoothing for keyboard
597 inputBuffer.left = lerp(inputBuffer.left, leftInput, INPUT_SMOOTHING);
598 if (!aiEnabled) {
599 inputBuffer.right = lerp(inputBuffer.right, rightInput, INPUT_SMOOTHING);
600 }
601
602 // Move supports with enhanced physics (only if not using mouse)
603 if (!mouseInput.active) {
604 if (Math.abs(inputBuffer.left) > 0.01) {
605 moveSupportEnhanced(leftSupport, inputBuffer.left * SUPPORT_SPEED);
606 }
607 if (!aiEnabled && Math.abs(inputBuffer.right) > 0.01) {
608 moveSupportEnhanced(rightSupport, inputBuffer.right * SUPPORT_SPEED);
609 }
610 }
611 }
612
613 function handleMouseTouchInput() {
614 if (!mouseInput.active) return;
615
616 // Determine which paddle to control based on mouse X position
617 let controllingLeft = mouseX < width / 2;
618
619 // Don't allow mouse control of AI paddle
620 if (!controllingLeft && aiEnabled) return;
621
622 let targetSupport = controllingLeft ? leftSupport : rightSupport;
623
624 // Calculate target Y with dead zone
625 let currentY = targetSupport.position.y;
626 let targetY = mouseY;
627 let deltaY = targetY - currentY;
628
629 // Apply dead zone - don't move unless mouse is far enough
630 if (Math.abs(deltaY) < mouseInput.deadZone) {
631 return;
632 }
633
634 // Calculate movement with lag and speed limiting
635 let movement = deltaY * MOUSE_LAG_FACTOR * TOUCH_SENSITIVITY;
636
637 // Limit maximum speed to prevent snappy movement
638 movement = constrain(movement, -MOUSE_SPEED_LIMIT, MOUSE_SPEED_LIMIT);
639
640 // Apply the lagged movement
641 moveSupportEnhanced(targetSupport, movement);
642
643 // Visual feedback - update input buffer for particle effects
644 if (controllingLeft) {
645 inputBuffer.left = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
646 } else if (!aiEnabled) {
647 inputBuffer.right = constrain(movement / MOUSE_SPEED_LIMIT, -1, 1);
648 }
649 }
650
651 function handleAI() {
652 let currentTime = millis();
653 let ballPos = ball.position;
654 let ballVel = ball.velocity;
655 let aiSettings = AI_SETTINGS[aiState.difficulty];
656
657 // Update aggression based on score difference and time
658 updateAIAggression();
659
660 // Add lifelike micro-movements
661 updateAILifelikeBehavior(currentTime);
662
663 // Advanced AI state machine
664 switch (aiState.mode) {
665 case 'TRACKING':
666 handleAITracking(currentTime, ballPos, ballVel, aiSettings);
667 break;
668 case 'WINDING_UP':
669 handleAIWindup(currentTime, ballPos, ballVel, aiSettings);
670 break;
671 case 'SWINGING':
672 handleAISwing(currentTime, ballPos, ballVel, aiSettings);
673 break;
674 case 'RECOVERING':
675 handleAIRecovery(currentTime, ballPos, ballVel, aiSettings);
676 break;
677 case 'ANTICIPATING':
678 handleAIAnticipation(currentTime, ballPos, ballVel, aiSettings);
679 break;
680 }
681
682 // Apply movement with spring physics awareness
683 executeAIMovement(aiSettings);
684 }
685
686 function updateAILifelikeBehavior(currentTime) {
687 // Subtle breathing-like motion when not actively engaged
688 aiState.breathingOffset = sin(currentTime * 0.003) * 3;
689
690 // Random micro-adjustments every few seconds
691 if (currentTime - aiState.lastMicroTime > 2000 + random(1000)) {
692 aiState.microAdjustment = (random() - 0.5) * 15;
693 aiState.lastMicroTime = currentTime;
694 }
695
696 // Gradually decay micro-adjustment
697 aiState.microAdjustment *= 0.98;
698
699 // Update idle target with slight wandering
700 if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
701 let centerY = height / 2;
702 let wanderRadius = 25;
703 aiState.idleTarget = centerY + sin(currentTime * 0.002) * wanderRadius;
704 }
705 }
706
707 function updateAIAggression() {
708 // Increase aggression when losing
709 let scoreDiff = leftScore - rightScore;
710 let baseAggression = AI_SETTINGS[aiState.difficulty].aggression;
711
712 // Rage mode when losing by 2+ points
713 if (scoreDiff >= 2) {
714 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.3);
715 } else if (scoreDiff >= 1) {
716 aiState.aggressionLevel = Math.min(1.0, baseAggression + 0.15);
717 } else {
718 aiState.aggressionLevel = baseAggression;
719 }
720 }
721
722 function handleAITracking(currentTime, ballPos, ballVel, aiSettings) {
723 // Always track ball position for more responsive movement
724 let ballApproaching = ballVel.x > 0;
725 let ballDistance = width - ballPos.x;
726 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
727
728 // Calculate where the AI paddle currently is (not the anchor)
729 let paddlePos = rightPaddle.position;
730 let anchorPos = rightSupport.position;
731
732 // Continuous ball tracking with paddle awareness
733 let trackingIntensity = ballApproaching ? 0.08 : 0.03;
734
735 // Calculate where anchor should be to position PADDLE at target Y
736 let desiredPaddleY = ballPos.y + aiState.microAdjustment;
737 desiredPaddleY = constrain(desiredPaddleY, 80, height - 80);
738
739 // Estimate anchor position needed to get paddle to desired position
740 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
741 let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
742
743 // Always apply some level of paddle-aware tracking
744 aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity);
745
746 // AI Bop decision logic
747 if (ballApproaching && ballDistance < 150 && !aiState.consideringBop) {
748 // Consider bopping if ball is close and conditions are right
749 let shouldConsiderBop = ballSpeed > 8 && // Fast incoming ball
750 Math.abs(ballVel.y) < 4 && // Not too much vertical movement
751 random() < aiSettings.aggression * 0.4; // Chance based on aggression
752
753 if (shouldConsiderBop) {
754 aiState.consideringBop = true;
755 aiState.bopDecisionTime = currentTime;
756 }
757 }
758
759 // Execute bop at the right moment
760 if (aiState.consideringBop && ballApproaching) {
761 let timeToBop = currentTime - aiState.bopDecisionTime;
762 let shouldBop = timeToBop > aiState.bopTiming &&
763 ballDistance < 100 &&
764 !bopState.right.active &&
765 currentTime - bopState.right.lastBopTime > BOP_COOLDOWN;
766
767 if (shouldBop) {
768 activateBop('right', currentTime);
769 aiState.consideringBop = false;
770 }
771
772 // Cancel bop consideration if ball gets too far
773 if (ballDistance > 150) {
774 aiState.consideringBop = false;
775 }
776 }
777
778 // Only do advanced prediction and windup logic if enough time has passed (reaction delay)
779 if (currentTime - aiState.lastUpdateTime > aiSettings.reactionTime) {
780
781 if (ballApproaching && ballDistance < 300) {
782 // Calculate intercept point with advanced prediction
783 let timeToReach = ballDistance / Math.abs(ballVel.x);
784 let predictedBallY = ballPos.y + ballVel.y * timeToReach;
785
786 // Account for wall bounces
787 if (predictedBallY < 50) {
788 predictedBallY = 100 - predictedBallY;
789 } else if (predictedBallY > height - 50) {
790 predictedBallY = 2 * (height - 50) - predictedBallY;
791 }
792
793 // Add accuracy error
794 let error = (random() - 0.5) * 35 * (1 - aiSettings.accuracy);
795 predictedBallY += error;
796
797 // Calculate where PADDLE needs to be to hit the ball
798 aiState.interceptY = predictedBallY;
799
800 // Calculate where ANCHOR needs to be to position paddle correctly
801 let interceptAnchorOffset = calculateAnchorOffset(aiState.interceptY, paddlePos, anchorPos);
802 let targetAnchorForIntercept = aiState.interceptY + interceptAnchorOffset;
803
804 // VERY selective windup decision: only for very slow balls
805 let shouldWindUp = ballSpeed < 4.5 && // Much stricter - very slow balls only
806 ballDistance > 200 && // Lots of distance required
807 Math.abs(ballVel.y) < 3 && // Very limited vertical movement
808 Math.abs(ballVel.x) > 1 && // Ball must be actually moving toward AI
809 !aiState.consideringBop && // Don't windup if considering bop
810 random() < aiSettings.oscillation * aiState.aggressionLevel * 0.3; // Much lower chance
811
812 if (shouldWindUp) {
813 // Start winding up for power shot
814 aiState.mode = 'WINDING_UP';
815 aiState.windupStartTime = currentTime;
816
817 // Determine windup direction (opposite of where paddle needs to be)
818 aiState.windupDirection = aiState.interceptY > paddlePos.y ? -1 : 1;
819
820 } else {
821 // Use paddle-aware intercept positioning
822 aiState.targetY = lerp(aiState.targetY, targetAnchorForIntercept, 0.3);
823 }
824
825 aiState.lastUpdateTime = currentTime;
826 }
827 }
828 }
829
830 // Helper function to estimate where anchor should be to position paddle at target Y
831 function calculateAnchorOffset(targetPaddleY, currentPaddlePos, currentAnchorPos) {
832 // Calculate current spring vector
833 let springVectorY = currentPaddlePos.y - currentAnchorPos.y;
834
835 // The paddle tends to lag behind the anchor due to spring physics
836 // We need to account for this offset when positioning
837
838 // Simple approximation: if spring is compressed/extended, paddle will be offset
839 let currentSpringLength = dist(currentAnchorPos.x, currentAnchorPos.y,
840 currentPaddlePos.x, currentPaddlePos.y);
841 let springCompression = SPRING_LENGTH - currentSpringLength;
842
843 // Estimate the Y offset the paddle will have relative to anchor
844 // This is a simplified physics approximation
845 let estimatedPaddleOffset = springVectorY * 0.8; // Paddle lags behind anchor
846
847 // Return the offset needed for anchor positioning
848 return -estimatedPaddleOffset;
849 }
850
851 function handleAIWindup(currentTime, ballPos, ballVel, aiSettings) {
852 let windupTime = currentTime - aiState.windupStartTime;
853
854 // Smooth windup progression with easing
855 let maxWindupTime = 800; // Longer time for smooth movement
856 let timeProgress = Math.min(windupTime / maxWindupTime, 1.0);
857
858 // Ease-in-out for smooth acceleration/deceleration
859 let easedProgress = timeProgress < 0.5
860 ? 2 * timeProgress * timeProgress
861 : 1 - Math.pow(-2 * timeProgress + 2, 3) / 2;
862
863 aiState.windupProgress = easedProgress;
864
865 // Calculate smooth windup target based on intercept position
866 let windupTargetY = aiState.interceptY + aiState.windupDirection * aiState.windupDistance * aiState.aggressionLevel * easedProgress;
867 windupTargetY = constrain(windupTargetY, 50, height - 50);
868
869 // Convert paddle target to anchor target using paddle awareness
870 let anchorOffsetNeeded = calculateAnchorOffset(windupTargetY, rightPaddle.position, rightSupport.position);
871 aiState.targetY = windupTargetY + anchorOffsetNeeded;
872
873 // Check if it's time to swing
874 let ballDistance = width - ballPos.x;
875 let ballSpeed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
876
877 let shouldSwing = windupTime > maxWindupTime ||
878 ballDistance < 120 ||
879 ballSpeed > 6 ||
880 easedProgress > 0.85; // Swing when windup is mostly complete
881
882 if (shouldSwing) {
883 aiState.mode = 'SWINGING';
884 aiState.swingStartTime = currentTime;
885 aiState.windupProgress = 0; // Reset for next time
886 }
887 }
888
889 function handleAISwing(currentTime, ballPos, ballVel, aiSettings) {
890 // Aggressive swing toward intercept point - but position anchor for paddle placement
891 let paddlePos = rightPaddle.position;
892
893 // Calculate where anchor should be to get paddle to intercept point
894 let anchorOffsetNeeded = calculateAnchorOffset(aiState.interceptY, paddlePos, rightSupport.position);
895 aiState.targetY = aiState.interceptY + anchorOffsetNeeded;
896
897 let swingTime = currentTime - aiState.swingStartTime;
898 let maxSwingTime = aiState.timingWindow;
899
900 // Check if swing is complete
901 if (swingTime > maxSwingTime || Math.abs(ball.velocity.x) < 2) {
902 aiState.mode = 'RECOVERING';
903 aiState.lastHitTime = currentTime;
904 }
905 }
906
907 function handleAIRecovery(currentTime, ballPos, ballVel, aiSettings) {
908 // Return to idle position with lifelike movement
909 aiState.targetY = aiState.idleTarget + aiState.breathingOffset;
910
911 let recoveryTime = currentTime - aiState.lastHitTime;
912 if (recoveryTime > 400) { // Faster recovery
913 aiState.mode = 'ANTICIPATING';
914 }
915 }
916
917 function handleAIAnticipation(currentTime, ballPos, ballVel, aiSettings) {
918 // Stay near center with subtle lifelike movements, but use paddle-aware positioning
919 let baseTarget = aiState.idleTarget + aiState.breathingOffset + aiState.microAdjustment;
920 let ballTrackingTarget = ballPos.y;
921
922 // Blend idle position with gentle ball tracking
923 let desiredPaddleY = lerp(baseTarget, ballTrackingTarget, 0.15);
924
925 // Convert paddle target to anchor target
926 let paddlePos = rightPaddle.position;
927 let anchorPos = rightSupport.position;
928 let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
929 aiState.targetY = desiredPaddleY + anchorOffsetNeeded;
930
931 // Switch back to tracking when ball changes direction
932 if (ballVel.x > 0) {
933 aiState.mode = 'TRACKING';
934 }
935 }
936
937 function executeAIMovement(aiSettings) {
938 // Move AI paddle toward target with speed limitation
939 let currentY = rightSupport.position.y;
940 let deltaY = aiState.targetY - currentY;
941
942 if (Math.abs(deltaY) > 1) { // Very small dead zone for responsive tracking
943 let baseSpeed = 0.12 * aiSettings.speed; // Increased base speed significantly
944
945 // Apply swing power during swing phase
946 if (aiState.mode === 'SWINGING') {
947 baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.3);
948 } else if (aiState.mode === 'WINDING_UP') {
949 // Slower at start of windup, faster as it progresses
950 let windupSpeedMultiplier = 0.3 + (aiState.windupProgress * 0.4);
951 baseSpeed *= windupSpeedMultiplier;
952 }
953
954 // Apply aggression multiplier
955 baseSpeed *= (1 + aiState.aggressionLevel * 0.3);
956
957 let movement = deltaY * baseSpeed;
958 movement = constrain(movement, -SUPPORT_SPEED * 1.1, SUPPORT_SPEED * 1.1); // Allow slightly faster than player
959
960 moveSupportEnhanced(rightSupport, movement);
961
962 // Update input buffer for visual effects
963 inputBuffer.right = constrain(movement / (SUPPORT_SPEED * 1.1), -1, 1);
964 } else {
965 // Gradually reduce input buffer when AI is not moving
966 inputBuffer.right *= 0.95;
967 }
968 }
969
970 function moveSupportEnhanced(support, deltaY) {
971 let newY = support.position.y + deltaY;
972
973 // Keep support within reasonable bounds with smooth clamping
974 let minY = 50;
975 let maxY = height - 50;
976
977 if (newY < minY) {
978 newY = minY + (newY - minY) * 0.1; // Soft boundary
979 } else if (newY > maxY) {
980 newY = maxY + (newY - maxY) * 0.1; // Soft boundary
981 }
982
983 Body.setPosition(support, { x: support.position.x, y: newY });
984 }
985
986 function checkCollisions() {
987 let ballPos = ball.position;
988 let ballVel = ball.velocity;
989
990 // Check paddle collisions for particle effects
991 let leftDist = dist(ballPos.x, ballPos.y, leftPaddle.position.x, leftPaddle.position.y);
992 let rightDist = dist(ballPos.x, ballPos.y, rightPaddle.position.x, rightPaddle.position.y);
993
994 // Collision threshold
995 let collisionDist = BALL_RADIUS + PADDLE_WIDTH/2 + 5;
996
997 // Left paddle collision
998 if (leftDist < collisionDist && ballVel.x < 0) {
999 createImpactParticles(ballPos.x, ballPos.y, ballVel.x, ballVel.y);
1000 }
1001
1002 // Right paddle collision
1003 if (rightDist < collisionDist && ballVel.x > 0) {
1004 createImpactParticles(ballPos.x, ballPos.y, ballVel.x, ballVel.y);
1005 }
1006 }
1007
1008 function createImpactParticles(x, y, velX, velY) {
1009 for (let i = 0; i < IMPACT_PARTICLES; i++) {
1010 let angle = random(TWO_PI);
1011 let speed = random(2, 8);
1012 let size = random(2, 6);
1013
1014 particles.push({
1015 x: x + random(-5, 5),
1016 y: y + random(-5, 5),
1017 vx: cos(angle) * speed - velX * 0.2,
1018 vy: sin(angle) * speed - velY * 0.2,
1019 size: size,
1020 life: PARTICLE_LIFE,
1021 maxLife: PARTICLE_LIFE,
1022 color: { r: 255, g: random(100, 255), b: random(100, 150) },
1023 type: 'impact'
1024 });
1025 }
1026 }
1027
1028 function createSpringParticles(springPos, compression) {
1029 if (random() < SPRING_PARTICLE_RATE * compression) {
1030 let angle = random(TWO_PI);
1031 let speed = random(1, 3) * compression;
1032
1033 particles.push({
1034 x: springPos.x + random(-10, 10),
1035 y: springPos.y + random(-10, 10),
1036 vx: cos(angle) * speed,
1037 vy: sin(angle) * speed,
1038 size: random(1, 3),
1039 life: PARTICLE_LIFE * 0.5,
1040 maxLife: PARTICLE_LIFE * 0.5,
1041 color: { r: 0, g: 255, b: 136 },
1042 type: 'spring'
1043 });
1044 }
1045 }
1046
1047 function updateParticles() {
1048 // Update and remove dead particles
1049 for (let i = particles.length - 1; i >= 0; i--) {
1050 let p = particles[i];
1051
1052 // Update position
1053 p.x += p.vx;
1054 p.y += p.vy;
1055
1056 // Apply drag
1057 p.vx *= 0.98;
1058 p.vy *= 0.98;
1059
1060 // Update life
1061 p.life--;
1062
1063 // Remove dead particles
1064 if (p.life <= 0) {
1065 particles.splice(i, 1);
1066 }
1067 }
1068
1069 // Limit particle count
1070 if (particles.length > MAX_PARTICLES) {
1071 particles.splice(0, particles.length - MAX_PARTICLES);
1072 }
1073 }
1074
1075 function drawParticles() {
1076 for (let p of particles) {
1077 let alpha = map(p.life, 0, p.maxLife, 0, 255);
1078
1079 push();
1080 translate(p.x, p.y);
1081
1082 if (p.type === 'impact') {
1083 // Impact particles: bright sparks
1084 fill(p.color.r, p.color.g, p.color.b, alpha);
1085 noStroke();
1086 ellipse(0, 0, p.size, p.size);
1087
1088 // Add glow
1089 fill(p.color.r, p.color.g, p.color.b, alpha * 0.3);
1090 ellipse(0, 0, p.size * 2, p.size * 2);
1091
1092 } else if (p.type === 'spring') {
1093 // Spring particles: green energy
1094 fill(p.color.r, p.color.g, p.color.b, alpha);
1095 noStroke();
1096 ellipse(0, 0, p.size, p.size);
1097 }
1098
1099 pop();
1100 }
1101 }
1102
1103 function drawSpringPaddleSystemsEnhanced() {
1104 // Draw springs with enhanced visuals and particles
1105 drawSpringsEnhanced();
1106
1107 // Draw paddles with glow effects
1108 drawPaddlesWithGlow();
1109
1110 // Draw support points with input feedback
1111 drawSupportPointsEnhanced();
1112 }
1113
1114 function drawSpringsEnhanced() {
1115 // Left spring
1116 let leftSupportPos = leftSupport.position;
1117 let leftPaddlePos = leftPaddle.position;
1118 let leftCompression = drawSpringLineEnhanced(leftSupportPos, leftPaddlePos);
1119 createSpringParticles(leftPaddlePos, leftCompression);
1120
1121 // Right spring
1122 let rightSupportPos = rightSupport.position;
1123 let rightPaddlePos = rightPaddle.position;
1124 let rightCompression = drawSpringLineEnhanced(rightSupportPos, rightPaddlePos);
1125 createSpringParticles(rightPaddlePos, rightCompression);
1126 }
1127
1128 function drawSpringLineEnhanced(startPos, endPos) {
1129 let segments = 12; // More segments for smoother springs
1130 let amplitude = 10; // Bigger amplitude for more dramatic effect
1131
1132 // Calculate spring compression for visual effects
1133 let currentLength = dist(startPos.x, startPos.y, endPos.x, endPos.y);
1134 let compression = SPRING_LENGTH / currentLength;
1135 amplitude *= compression;
1136
1137 // Enhanced spring glow based on compression
1138 let glowIntensity = 150 + compression * SPRING_GLOW_INTENSITY;
1139 stroke(0, 255, 136, glowIntensity);
1140 strokeWeight(3 + compression * 2); // Thicker when compressed
1141
1142 // Draw spring coil with smooth curves
1143 beginShape();
1144 noFill();
1145
1146 for (let i = 0; i <= segments; i++) {
1147 let t = i / segments;
1148 let x = lerp(startPos.x, endPos.x, t);
1149 let y = lerp(startPos.y, endPos.y, t);
1150
1151 // Enhanced zigzag with smoother curves
1152 if (i > 0 && i < segments) {
1153 let perpX = -(endPos.y - startPos.y) / currentLength;
1154 let perpY = (endPos.x - startPos.x) / currentLength;
1155 let offset = sin(i * PI * 1.5) * amplitude; // More dramatic oscillation
1156 x += perpX * offset;
1157 y += perpY * offset;
1158 }
1159
1160 vertex(x, y);
1161 }
1162
1163 endShape();
1164
1165 // Add spring glow effect with pulsing
1166 let pulse = sin(frameCount * 0.1) * 0.2 + 1;
1167 stroke(0, 255, 136, glowIntensity * 0.4 * pulse);
1168 strokeWeight(8 + compression * 3);
1169 beginShape();
1170 noFill();
1171
1172 for (let i = 0; i <= segments; i++) {
1173 let t = i / segments;
1174 let x = lerp(startPos.x, endPos.x, t);
1175 let y = lerp(startPos.y, endPos.y, t);
1176 vertex(x, y);
1177 }
1178
1179 endShape();
1180
1181 return compression; // Return compression for particle effects
1182 }
1183
1184 function drawPaddlesWithGlow() {
1185 // Calculate ball distance for glow effects
1186 let ballPos = ball.position;
1187 let leftDist = dist(ballPos.x, ballPos.y, leftPaddle.position.x, leftPaddle.position.y);
1188 let rightDist = dist(ballPos.x, ballPos.y, rightPaddle.position.x, rightPaddle.position.y);
1189
1190 // Enhanced paddle drawing
1191 drawSinglePaddleEnhanced(leftPaddle, leftDist);
1192 drawSinglePaddleEnhanced(rightPaddle, rightDist);
1193 }
1194
1195 function drawSinglePaddleEnhanced(paddle, ballDistance) {
1196 let pos = paddle.position;
1197 let angle = paddle.angle;
1198
1199 // Check if this is the AI paddle
1200 let isAI = aiEnabled && paddle === rightPaddle;
1201 let isLeft = paddle === leftPaddle;
1202
1203 // Calculate glow intensity based on ball proximity
1204 let glowIntensity = map(ballDistance, 0, PADDLE_GLOW_DISTANCE, 150, 0);
1205 glowIntensity = constrain(glowIntensity, 0, 150);
1206
1207 // Add bop glow effect
1208 let bopGlow = 0;
1209 if (isLeft && bopState.left.active) {
1210 let bopProgress = (millis() - bopState.left.startTime) / bopState.left.duration;
1211 bopGlow = (1 - bopProgress) * 100; // Fade out over bop duration
1212 } else if (!isLeft && !isAI && bopState.right.active) {
1213 let bopProgress = (millis() - bopState.right.startTime) / bopState.right.duration;
1214 bopGlow = (1 - bopProgress) * 100;
1215 }
1216
1217 glowIntensity += bopGlow;
1218
1219 // Add AI state-based effects
1220 if (isAI) {
1221 // Enhance glow during aggressive states
1222 if (aiState.mode === 'WINDING_UP') {
1223 glowIntensity += 50;
1224 } else if (aiState.mode === 'SWINGING') {
1225 glowIntensity += 100;
1226 }
1227
1228 // Aggression-based glow
1229 glowIntensity += aiState.aggressionLevel * 30;
1230 }
1231
1232 push();
1233 translate(pos.x, pos.y);
1234 rotate(angle);
1235
1236 // Different color scheme for AI paddle
1237 let paddleColor = isAI ? [255, 100, 100] : [0, 255, 136]; // Red for AI, green for player
1238
1239 // AI mode indicator colors
1240 if (isAI && aiState.mode === 'WINDING_UP') {
1241 paddleColor = [255, 150, 50]; // Orange during windup
1242 } else if (isAI && aiState.mode === 'SWINGING') {
1243 paddleColor = [255, 50, 50]; // Bright red during swing
1244 }
1245
1246 // Bop color override
1247 if ((isLeft && bopState.left.active) || (!isLeft && !isAI && bopState.right.active)) {
1248 paddleColor = [255, 255, 100]; // Bright yellow during bop
1249 }
1250
1251 // Draw enhanced glow effect first
1252 if (glowIntensity > 0) {
1253 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.6);
1254 noStroke();
1255 rectMode(CENTER);
1256 rect(0, 0, PADDLE_WIDTH + 12, PADDLE_HEIGHT + 12);
1257
1258 // Add outer glow
1259 fill(paddleColor[0], paddleColor[1], paddleColor[2], glowIntensity * 0.3);
1260 rect(0, 0, PADDLE_WIDTH + 20, PADDLE_HEIGHT + 20);
1261 }
1262
1263 // Draw main paddle with enhanced visual
1264 fill(paddleColor[0], paddleColor[1], paddleColor[2]);
1265 stroke(paddleColor[0], paddleColor[1], paddleColor[2], 220 + glowIntensity * 0.5);
1266 strokeWeight(3);
1267 rectMode(CENTER);
1268 rect(0, 0, PADDLE_WIDTH, PADDLE_HEIGHT);
1269
1270 // Add core highlight
1271 if (isAI) {
1272 fill(255, 200, 200, 100); // Light red highlight for AI
1273 } else {
1274 fill(150, 255, 200, 100); // Light green highlight for player
1275 }
1276 noStroke();
1277 rect(0, 0, PADDLE_WIDTH - 4, PADDLE_HEIGHT - 4);
1278
1279 pop();
1280 }
1281
1282 function drawSupportPointsEnhanced() {
1283 // Enhanced support indicators with input feedback
1284 let leftActivity = Math.abs(inputBuffer.left) * 255;
1285 let rightActivity = Math.abs(inputBuffer.right) * 255;
1286
1287 // Left support with pulsing effect
1288 let leftPulse = sin(frameCount * 0.2) * 0.3 + 1;
1289 fill(0, 255, 136, 100 + leftActivity * 0.6);
1290 noStroke();
1291 ellipse(leftSupport.position.x, leftSupport.position.y,
1292 (8 + leftActivity * 0.15) * leftPulse,
1293 (8 + leftActivity * 0.15) * leftPulse);
1294
1295 // Right support with pulsing effect
1296 let rightPulse = sin(frameCount * 0.2 + PI) * 0.3 + 1;
1297 fill(0, 255, 136, 100 + rightActivity * 0.6);
1298 ellipse(rightSupport.position.x, rightSupport.position.y,
1299 (8 + rightActivity * 0.15) * rightPulse,
1300 (8 + rightActivity * 0.15) * rightPulse);
1301 }
1302
1303 function drawBallEnhanced() {
1304 let ballPos = ball.position;
1305 let ballVel = ball.velocity;
1306 let speed = Math.sqrt(ballVel.x * ballVel.x + ballVel.y * ballVel.y);
1307
1308 // Enhanced ball with speed-based effects
1309 let speedIntensity = map(speed, 0, 15, 50, 255);
1310
1311 // Multi-layered trail effect
1312 for (let i = 0; i < 3; i++) {
1313 let offset = i * 3;
1314 fill(255, 100, 100, 40 - i * 10);
1315 noStroke();
1316 ellipse(ballPos.x - ballVel.x * offset * 0.1,
1317 ballPos.y - ballVel.y * offset * 0.1,
1318 BALL_RADIUS * (4 - i), BALL_RADIUS * (4 - i));
1319 }
1320
1321 // Main ball with enhanced glow
1322 fill(255, 100, 100);
1323 stroke(255, 200, 200, speedIntensity);
1324 strokeWeight(3 + speed * 0.15);
1325 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 2, BALL_RADIUS * 2);
1326
1327 // Speed indicator core
1328 if (speed > 8) {
1329 fill(255, 255, 255, speedIntensity * 0.8);
1330 noStroke();
1331 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 0.8, BALL_RADIUS * 0.8);
1332 }
1333
1334 // Outer energy ring for high speeds
1335 if (speed > 12) {
1336 noFill();
1337 stroke(255, 255, 255, speedIntensity * 0.5);
1338 strokeWeight(2);
1339 ellipse(ballPos.x, ballPos.y, BALL_RADIUS * 3, BALL_RADIUS * 3);
1340 }
1341 }
1342
1343 function drawBoundaries() {
1344 stroke(0, 255, 136, 30);
1345 strokeWeight(1);
1346 noFill();
1347 line(0, 0, width, 0);
1348 line(0, height, width, height);
1349 }
1350
1351 function drawCenterLine() {
1352 stroke(0, 255, 136, 50);
1353 strokeWeight(2);
1354
1355 for (let y = 0; y < height; y += 20) {
1356 line(width/2, y, width/2, y + 10);
1357 }
1358 }
1359
1360 function drawDebugInfo() {
1361 fill(255, 100);
1362 textAlign(LEFT);
1363 textSize(12);
1364 text(`FPS: ${Math.round(frameRate())}`, 10, 20);
1365 text(`Ball Speed: ${Math.round(getBallSpeed())}`, 10, 35);
1366 text(`Particles: ${particles.length}`, 10, 50);
1367 text(`Mode: ${aiEnabled ? 'vs CPU' : '2 Player'} | Difficulty: ${aiState.difficulty}`, 10, 65);
1368
1369 // Enhanced spring info
1370 let leftSpringLength = dist(leftSupport.position.x, leftSupport.position.y,
1371 leftPaddle.position.x, leftPaddle.position.y);
1372 let rightSpringLength = dist(rightSupport.position.x, rightSupport.position.y,
1373 rightPaddle.position.x, rightPaddle.position.y);
1374
1375 text(`L Spring: ${Math.round(leftSpringLength)}px (${((SPRING_LENGTH/leftSpringLength - 1) * 100).toFixed(0)}%)`, 10, 80);
1376 text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 95);
1377 text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 110);
1378
1379 // Advanced AI debug info
1380 if (aiEnabled) {
1381 text(`AI State: ${aiState.mode} | Aggression: ${aiState.aggressionLevel.toFixed(2)}`, 10, 125);
1382 text(`Target: ${Math.round(aiState.targetY)} | Intercept: ${Math.round(aiState.interceptY)}`, 10, 140);
1383 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);
1384
1385 // Show AI technique indicators
1386 if (aiState.mode === 'WINDING_UP') {
1387 fill(255, 150, 50, 200);
1388 text("🔄 AI WINDING UP FOR POWER SHOT", 10, 175);
1389 } else if (aiState.mode === 'SWINGING') {
1390 fill(255, 50, 50, 200);
1391 text("⚡ AI POWER SWING!", 10, 175);
1392 }
1393 }
1394
1395 // Mouse/touch input debug
1396 if (mouseInput.active) {
1397 text(`Mouse: Active | Side: ${mouseX < width/2 ? 'Left' : 'Right'} | Y: ${mouseY}`, 10, 190);
1398 }
1399 }
1400
1401 function drawMenu() {
1402 // Draw animated background
1403 drawMenuBackground();
1404
1405 // Main title
1406 push();
1407 let titlePulse = sin(frameCount * 0.05) * 0.2 + 1;
1408 fill(0, 255, 136);
1409 textAlign(CENTER);
1410 textSize(60 * titlePulse);
1411 text("SPRONG", width/2, 120);
1412
1413 // Subtitle
1414 fill(0, 255, 136, 150);
1415 textSize(16);
1416 text("Physics-based Pong with Spring Paddles", width/2, 150);
1417 pop();
1418
1419 // Menu options
1420 let startY = height/2 - 20;
1421 let spacing = 60;
1422
1423 for (let i = 0; i < menuState.options.length; i++) {
1424 let y = startY + i * spacing;
1425 let isSelected = i === menuState.selectedOption;
1426
1427 // Selection indicator
1428 if (isSelected) {
1429 push();
1430 let pulse = sin(frameCount * 0.15) * 0.3 + 1;
1431 fill(0, 255, 136, 100 * pulse);
1432 noStroke();
1433 rectMode(CENTER);
1434 rect(width/2, y, 300, 45);
1435 pop();
1436 }
1437
1438 // Option text
1439 fill(isSelected ? 255 : 200);
1440 textAlign(CENTER);
1441 textSize(isSelected ? 24 : 20);
1442 text(menuState.options[i], width/2, y + 8);
1443
1444 // Show difficulty selector for 1 Player option
1445 if (i === 0 && isSelected && menuState.showDifficulty) {
1446 fill(0, 255, 136, 180);
1447 textSize(14);
1448 text(`Difficulty: ${menuState.difficulties[menuState.difficultySelected]}`, width/2, y + 28);
1449 text("(Use ← → to change)", width/2, y + 45);
1450 }
1451 }
1452
1453 // Instructions
1454 fill(0, 255, 136, 120);
1455 textAlign(CENTER);
1456 textSize(14);
1457 text("Use ↑↓ to select, ENTER to confirm", width/2, height - 80);
1458 text("or click/touch to select", width/2, height - 60);
1459
1460 // Show controls preview
1461 textSize(12);
1462 fill(255, 100);
1463 if (menuState.selectedOption === 0) {
1464 text("Controls: W/S keys + LEFT SHIFT (bop) or Mouse/Touch", width/2, height - 30);
1465 } else {
1466 text("Controls: P1 (W/S + L.Shift) | P2 (↑/↓ + Enter) | Mouse/Touch", width/2, height - 30);
1467 }
1468 }
1469
1470 function drawMenuBackground() {
1471 // Draw subtle animated background elements
1472 push();
1473 stroke(0, 255, 136, 30);
1474 strokeWeight(1);
1475
1476 // Animated grid
1477 for (let x = 0; x < width; x += 40) {
1478 let offset = sin(frameCount * 0.01 + x * 0.01) * 5;
1479 line(x, 0, x, height + offset);
1480 }
1481
1482 for (let y = 0; y < height; y += 40) {
1483 let offset = cos(frameCount * 0.01 + y * 0.01) * 5;
1484 line(0, y, width + offset, y);
1485 }
1486
1487 // Floating particles
1488 for (let i = 0; i < 20; i++) {
1489 let x = (frameCount * 0.5 + i * 137) % width;
1490 let y = (sin(frameCount * 0.01 + i) * 50 + height/2);
1491 let alpha = sin(frameCount * 0.02 + i) * 30 + 50;
1492
1493 fill(0, 255, 136, alpha);
1494 noStroke();
1495 ellipse(x, y, 3, 3);
1496 }
1497 pop();
1498 }
1499
1500 function drawStartMessage() {
1501 fill(0, 255, 136, 200);
1502 textAlign(CENTER);
1503 textSize(20);
1504 text("Press any key to start!", width/2, height/2 + 100);
1505 textSize(14);
1506
1507 if (aiEnabled) {
1508 text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
1509 text(`AI Difficulty: ${aiState.difficulty.toUpperCase()}`, width/2, height/2 + 145);
1510 } else {
1511 text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
1512 }
1513
1514 textSize(12);
1515 fill(0, 255, 136, 120);
1516 text("Press ESC to return to menu", width/2, height/2 + 170);
1517 }
1518
1519 function resetBall() {
1520 if (ball) {
1521 World.remove(world, ball);
1522 }
1523
1524 // Create new ball at center with collision filter
1525 ball = Bodies.circle(width/2, height/2, BALL_RADIUS, {
1526 restitution: 1,
1527 friction: 0,
1528 frictionAir: 0,
1529 slop: 0.01, // Tighter collision detection
1530 collisionFilter: {
1531 category: 0x0001,
1532 mask: 0xFFFF
1533 },
1534 render: {
1535 fillStyle: '#ff6464'
1536 }
1537 });
1538
1539 if (world) {
1540 World.add(world, ball);
1541 }
1542
1543 // Start ball moving after a short delay
1544 setTimeout(() => {
1545 let direction = random() > 0.5 ? 1 : -1;
1546 let angle = random(-PI/6, PI/6);
1547
1548 Body.setVelocity(ball, {
1549 x: direction * BALL_SPEED * cos(angle),
1550 y: BALL_SPEED * sin(angle)
1551 });
1552
1553 gameStarted = true;
1554 }, 1000);
1555 }
1556
1557 function checkBallPosition() {
1558 let ballX = ball.position.x;
1559
1560 if (ballX < -BALL_RADIUS) {
1561 rightScore++;
1562 updateScore();
1563 resetBall();
1564 gameStarted = false;
1565 }
1566
1567 if (ballX > width + BALL_RADIUS) {
1568 leftScore++;
1569 updateScore();
1570 resetBall();
1571 gameStarted = false;
1572 }
1573 }
1574
1575 function updateScore() {
1576 document.getElementById('leftScore').textContent = leftScore;
1577 document.getElementById('rightScore').textContent = rightScore;
1578 }
1579
1580 function getBallSpeed() {
1581 let velocity = ball.velocity;
1582 return Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y);
1583 }
1584
1585 // Input handling
1586 function keyPressed() {
1587 keys[key] = true;
1588 keys[keyCode] = true;
1589
1590 if (gameState === 'menu') {
1591 handleMenuInput();
1592 return;
1593 }
1594
1595 if (!gameStarted && key !== ' ') {
1596 gameStarted = true;
1597 }
1598
1599 // Toggle game mode (only during gameplay)
1600 if (key === 'm' || key === 'M') {
1601 aiEnabled = !aiEnabled;
1602 gameMode = aiEnabled ? 'vs-cpu' : 'vs-human';
1603 console.log("Switched to " + gameMode + " mode");
1604 }
1605
1606 // Change AI difficulty (only during gameplay)
1607 if (key === 'd' || key === 'D') {
1608 if (aiState.difficulty === 'easy') {
1609 aiState.difficulty = 'medium';
1610 } else if (aiState.difficulty === 'medium') {
1611 aiState.difficulty = 'hard';
1612 } else {
1613 aiState.difficulty = 'easy';
1614 }
1615 console.log("AI difficulty: " + aiState.difficulty);
1616 }
1617
1618 // Reset game with spacebar
1619 if (key === ' ') {
1620 leftScore = 0;
1621 rightScore = 0;
1622 updateScore();
1623 resetBall();
1624 gameStarted = false;
1625
1626 // Reset input buffers
1627 inputBuffer.left = 0;
1628 inputBuffer.right = 0;
1629
1630 // Reset mouse input
1631 mouseInput.active = false;
1632
1633 // Reset AI state
1634 aiState.targetY = height / 2;
1635 aiState.lastUpdateTime = 0;
1636 aiState.mode = 'ANTICIPATING';
1637 aiState.aggressionLevel = AI_SETTINGS[aiState.difficulty].aggression;
1638 aiState.windupProgress = 0;
1639
1640 // Clear particles
1641 particles = [];
1642
1643 console.log("Game reset!");
1644 }
1645
1646 // Return to menu with ESC
1647 if (keyCode === 27) { // ESC key
1648 gameState = 'menu';
1649 gameStarted = false;
1650 particles = [];
1651 console.log("Returned to menu");
1652 }
1653 }
1654
1655 function handleMenuInput() {
1656 // Navigate menu with arrow keys
1657 if (keyCode === UP_ARROW) {
1658 menuState.selectedOption = Math.max(0, menuState.selectedOption - 1);
1659 } else if (keyCode === DOWN_ARROW) {
1660 menuState.selectedOption = Math.min(menuState.options.length - 1, menuState.selectedOption + 1);
1661 }
1662
1663 // Change difficulty for 1 Player mode
1664 if (menuState.selectedOption === 0) {
1665 if (keyCode === LEFT_ARROW) {
1666 menuState.difficultySelected = Math.max(0, menuState.difficultySelected - 1);
1667 } else if (keyCode === RIGHT_ARROW) {
1668 menuState.difficultySelected = Math.min(menuState.difficulties.length - 1, menuState.difficultySelected + 1);
1669 }
1670 }
1671
1672 // Confirm selection with ENTER
1673 if (keyCode === ENTER || key === ' ') {
1674 startGameWithSelection();
1675 }
1676 }
1677
1678 function startGameWithSelection() {
1679 // Set game mode based on selection
1680 if (menuState.selectedOption === 0) {
1681 // 1 Player vs CPU
1682 aiEnabled = true;
1683 gameMode = 'vs-cpu';
1684 aiState.difficulty = menuState.difficulties[menuState.difficultySelected].toLowerCase();
1685 } else {
1686 // 2 Player
1687 aiEnabled = false;
1688 gameMode = 'vs-human';
1689 }
1690
1691 // Start the game
1692 gameState = 'playing';
1693 gameStarted = false; // Will start when user presses a key
1694
1695 // Reset game state
1696 leftScore = 0;
1697 rightScore = 0;
1698 updateScore();
1699 resetBall();
1700
1701 // Reset input buffers
1702 inputBuffer.left = 0;
1703 inputBuffer.right = 0;
1704 mouseInput.active = false;
1705
1706 // Reset AI state
1707 aiState.targetY = height / 2;
1708 aiState.lastUpdateTime = 0;
1709
1710 // Clear particles
1711 particles = [];
1712
1713 console.log("Started " + gameMode + " mode" + (aiEnabled ? " - Difficulty: " + aiState.difficulty : ""));
1714 }
1715
1716 function keyReleased() {
1717 keys[key] = false;
1718 keys[keyCode] = false;
1719 }
1720
1721 // Mouse/touch input handlers
1722 function mousePressed() {
1723 if (gameState === 'menu') {
1724 handleMenuClick();
1725 return false;
1726 }
1727
1728 // Start mouse/touch input when clicking in game area
1729 if (mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height) {
1730 mouseInput.active = true;
1731
1732 // Start game if not started
1733 if (!gameStarted) {
1734 gameStarted = true;
1735 }
1736
1737 return false; // Prevent default behavior
1738 }
1739 }
1740
1741 function handleMenuClick() {
1742 let startY = height/2 - 20;
1743 let spacing = 60;
1744
1745 // Check if clicked on menu options
1746 for (let i = 0; i < menuState.options.length; i++) {
1747 let y = startY + i * spacing;
1748
1749 if (mouseY > y - 25 && mouseY < y + 25) {
1750 if (menuState.selectedOption === i) {
1751 // Double click or click on already selected - start game
1752 startGameWithSelection();
1753 } else {
1754 // Select this option
1755 menuState.selectedOption = i;
1756 }
1757 break;
1758 }
1759 }
1760
1761 // Check difficulty selection area for 1 Player mode
1762 if (menuState.selectedOption === 0) {
1763 let diffY = startY + 28;
1764 if (mouseY > diffY && mouseY < diffY + 20) {
1765 // Cycle through difficulties on click
1766 menuState.difficultySelected = (menuState.difficultySelected + 1) % menuState.difficulties.length;
1767 }
1768 }
1769 }
1770
1771 function mouseDragged() {
1772 // Continue mouse/touch input while dragging
1773 if (mouseInput.active) {
1774 return false; // Prevent default behavior
1775 }
1776 }
1777
1778 function mouseReleased() {
1779 // Stop mouse/touch input when releasing
1780 mouseInput.active = false;
1781
1782 // Gradually reduce input buffer when mouse is released
1783 inputBuffer.left *= 0.8;
1784 inputBuffer.right *= 0.8;
1785 }
1786
1787 function touchStarted() {
1788 // Handle touch events same as mouse
1789 return mousePressed();
1790 }
1791
1792 function touchMoved() {
1793 // Handle touch drag same as mouse
1794 return mouseDragged();
1795 }
1796
1797 function touchEnded() {
1798 // Handle touch end same as mouse
1799 mouseReleased();
1800 return false;
1801 }