Comparing changes

Choose two branches to see what's changed or to start a new pull request.

base: refactor
Choose a base ref
compare: rotation
Choose a head ref
Create pull request
Able to merge. These branches can be automatically merged.
7 commits 6 files changed 3 contributors

Commits on rotation

.DS_Storeadded
Binary file changed.
index.htmlmodified
@@ -3,7 +3,7 @@
33
 <head>
44
     <meta charset="UTF-8">
55
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
-    <title>Sprong :: Physics-based Pong</title>
6
+    <title>Sprong :: Pong with Physick</title>
77
     <link rel="stylesheet" href="sprong.css">
88
 </head>
99
 <body>
js/ai.jsmodified
@@ -1,62 +1,93 @@
11
 // ai.js - AI logic and behavior
22
 
33
 // ============= AI TECHNIQUE CONSTANTS =============
4
-const AI_WINDUP_SPEED = 0.15;        // Base oscillation speed
5
-const AI_WINDUP_SMOOTHNESS = 0.92;   // Smooth transitions
6
-const AI_WINDUP_RADIUS = 40;         // Circular motion radius
7
-const AI_WINDUP_MIN_TIME = 300;      // Minimum windup duration
8
-const AI_WINDUP_MAX_TIME = 600;      // Maximum windup duration
9
-const AI_BOP_AT_PEAK_CHANCE = 0.4;   // Chance to bop at windup peak
10
-const AI_CIRCULAR_MOTION = 0.7;      // How circular vs linear the motion is
11
-const AI_MOMENTUM_CARRY = 0.85;      // How much momentum carries between moves
12
-const AI_PHASE_SPEED = 0.08;         // Speed of phase progression (radians per frame)
4
+const AI_WINDUP_SPEED       = 0.15;   // Base oscillation speed
5
+const AI_WINDUP_SMOOTHNESS  = 1.00;   // Smooth transitions
6
+const AI_WINDUP_RADIUS      = 240;    // Circular motion radius
7
+const AI_WINDUP_MIN_TIME    = 300;    // Minimum windup duration
8
+const AI_WINDUP_MAX_TIME    = 1200;   // Maximum windup duration
9
+const AI_BOP_AT_PEAK_CHANCE = 0.4;    // Chance to bop at windup peak
10
+const AI_CIRCULAR_MOTION    = 0.9;    // How circular vs linear the motion is
11
+const AI_MOMENTUM_CARRY     = 0.85;   // How much momentum carries between moves
12
+const AI_PHASE_SPEED        = 0.1;    // Speed of phase progression (radians per frame)
13
+
14
+const AI_IDLE_MOVEMENT  = 0.8;        // How much AI moves when idle
15
+const AI_NERVOUS_ENERGY = 0.5;        // Random fidgeting energy
16
+const AI_PREDICTIVE_MOVEMENT = 0.7;   // How much AI moves based on prediction
17
+const AI_TRACKING_AGGRESSION = 0.15;  // How aggressively AI tracks the ball
18
+
19
+// AI Rotation constants
20
+const AI_ROTATION_UPDATE_RATE = 150;  // How often AI reconsiders rotation (ms)
21
+const AI_ROTATION_SMOOTHING = 0.08;   // How smoothly AI rotates
22
+const AI_DEFENSIVE_ANGLE = 0.2;       // Slight angle for defensive shots
23
+const AI_OFFENSIVE_ANGLE = 0.4;       // Larger angle for offensive shots
24
+const AI_TRICK_SHOT_ANGLE = 0.6;      // Maximum angle for trick shots
25
+const AI_ROTATION_PREDICTION = 1.2;   // How far ahead AI predicts for rotation
1326
 
1427
 // ============= AI SETTINGS =============
1528
 const AI_SETTINGS = {
1629
     easy: {
17
-        reactionTime: 400,
30
+        reactionTime: 350,
1831
         accuracy: 0.7,
1932
         speed: 0.8,
2033
         prediction: 0.3,
21
-        aggression: 0.2,
34
+        aggression: 0.4,
2235
         oscillation: 0.3,
2336
         bopChance: 0.25,
24
-        // New windup parameters
2537
         windupSpeed: 0.1,
2638
         windupRadius: 30,
2739
         comboBopChance: 0.1,
2840
         circularMotion: 0.4,
29
-        phaseSpeed: 0.06
41
+        phaseSpeed: 0.06,
42
+        idleMovement: 0.3,
43
+        trackingAggression: 0.1,
44
+        // Rotation settings
45
+        rotationUse: 0.2,           // How often AI uses rotation
46
+        rotationAccuracy: 0.5,       // How accurately AI calculates angle
47
+        rotationSpeed: 0.6,          // How fast AI rotates
48
+        rotationAnticipation: 0.3    // How well AI predicts needed angle
3049
     },
3150
     medium: {
32
-        reactionTime: 250,
51
+        reactionTime: 200,
3352
         accuracy: 0.85,
3453
         speed: 1.0,
3554
         prediction: 0.6,
36
-        aggression: 0.5,
55
+        aggression: 0.7,
3756
         oscillation: 0.7,
3857
         bopChance: 0.55,
39
-        // New windup parameters
4058
         windupSpeed: 0.15,
4159
         windupRadius: 40,
4260
         comboBopChance: 0.3,
4361
         circularMotion: 0.6,
44
-        phaseSpeed: 0.08
62
+        phaseSpeed: 0.08,
63
+        idleMovement: 0.5,
64
+        trackingAggression: 0.15,
65
+        // Rotation settings
66
+        rotationUse: 0.5,
67
+        rotationAccuracy: 0.7,
68
+        rotationSpeed: 0.8,
69
+        rotationAnticipation: 0.6
4570
     },
4671
     hard: {
47
-        reactionTime: 150,
72
+        reactionTime: 100,
4873
         accuracy: 0.95,
49
-        speed: 1.2,
50
-        prediction: 0.8,
51
-        aggression: 0.8,
74
+        speed: 1.5,
75
+        prediction: 0.85,
76
+        aggression: 1.0,
5277
         oscillation: 1.0,
5378
         bopChance: 0.85,
54
-        // New windup parameters
5579
         windupSpeed: 0.2,
56
-        windupRadius: 50,
80
+        windupRadius: 60,
5781
         comboBopChance: 0.5,
5882
         circularMotion: 0.8,
59
-        phaseSpeed: 0.1
83
+        phaseSpeed: 0.12,
84
+        idleMovement: 0.8,
85
+        trackingAggression: 0.25,
86
+        // Rotation settings
87
+        rotationUse: 0.8,
88
+        rotationAccuracy: 0.9,
89
+        rotationSpeed: 1.0,
90
+        rotationAnticipation: 0.85
6091
     }
6192
 };
6293
 
@@ -107,7 +138,14 @@ let aiState = {
107138
     // AI Bop system
108139
     consideringBop: false,
109140
     bopDecisionTime: 0,
110
-    bopTiming: 200
141
+    bopTiming: 200,
142
+    
143
+    // AI Rotation system
144
+    targetRotation: 0,       // Desired rotation angle
145
+    rotationMode: 'NEUTRAL', // NEUTRAL, OFFENSIVE, DEFENSIVE, TRICK_SHOT
146
+    lastRotationUpdate: 0,   // Time of last rotation decision
147
+    plannedShotAngle: 0,     // Angle AI wants to send ball
148
+    rotationConfidence: 0    // How confident AI is in its rotation choice
111149
 };
112150
 
113151
 // ============= MAIN AI HANDLER =============
@@ -120,6 +158,7 @@ function handleAI(currentTime, ball, rightPaddle, rightSupport,
120158
     
121159
     updateAIAggression(leftScore, rightScore);
122160
     updateAILifelikeBehavior(currentTime, height);
161
+    updateAIRotation(currentTime, ball, rightPaddle, width, height, aiSettings);
123162
     
124163
     switch (aiState.mode) {
125164
         case 'TRACKING':
@@ -148,19 +187,32 @@ function handleAI(currentTime, ball, rightPaddle, rightSupport,
148187
 
149188
 // ============= AI BEHAVIORS =============
150189
 function updateAILifelikeBehavior(currentTime, height) {
151
-    aiState.breathingOffset = Math.sin(currentTime * 0.003) * 3;
190
+    let aiSettings = AI_SETTINGS[aiState.difficulty];
191
+    
192
+    // More pronounced breathing motion
193
+    aiState.breathingOffset = Math.sin(currentTime * 0.005) * 5 * aiSettings.idleMovement;
152194
     
153
-    if (currentTime - aiState.lastMicroTime > 2000 + Math.random() * 1000) {
154
-        aiState.microAdjustment = (Math.random() - 0.5) * 15;
195
+    // More frequent micro-adjustments
196
+    if (currentTime - aiState.lastMicroTime > 1000 + Math.random() * 500) {
197
+        aiState.microAdjustment = (Math.random() - 0.5) * 30 * aiSettings.idleMovement;
155198
         aiState.lastMicroTime = currentTime;
156199
     }
157200
     
158
-    aiState.microAdjustment *= 0.98;
201
+    // Slower decay for more persistent movement
202
+    aiState.microAdjustment *= 0.95;
203
+    
204
+    // Add "nervous energy" - small random movements
205
+    let nervousEnergy = (Math.random() - 0.5) * AI_NERVOUS_ENERGY * aiSettings.aggression;
206
+    aiState.microAdjustment += nervousEnergy;
159207
     
160208
     if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
161209
         let centerY = height / 2;
162
-        let wanderRadius = 25;
163
-        aiState.idleTarget = centerY + Math.sin(currentTime * 0.002) * wanderRadius;
210
+        let wanderRadius = 40 * aiSettings.idleMovement; // Larger wander radius
211
+        aiState.idleTarget = centerY + Math.sin(currentTime * 0.003) * wanderRadius;
212
+        
213
+        // Add vertical patrol behavior
214
+        let patrolOffset = Math.sin(currentTime * 0.002) * 30 * aiSettings.idleMovement;
215
+        aiState.idleTarget += patrolOffset;
164216
     }
165217
 }
166218
 
@@ -187,15 +239,33 @@ function handleAITracking(currentTime, ballPos, ballVel, aiSettings,
187239
     let paddlePos = rightPaddle.position;
188240
     let anchorPos = rightSupport.position;
189241
     
190
-    let trackingIntensity = ballApproaching ? 0.08 : 0.03;
242
+    // More aggressive tracking based on difficulty
243
+    let trackingIntensity = ballApproaching ? 
244
+        (0.08 + aiSettings.trackingAggression) : 
245
+        (0.03 + aiSettings.trackingAggression * 0.5);
246
+    
247
+    // Predict where ball will be and move preemptively
248
+    let futureTime = 0.5; // Look ahead 0.5 seconds
249
+    let futureBallY = ballPos.y + ballVel.y * futureTime * 60; // 60 fps assumption
191250
     
192
-    let desiredPaddleY = ballPos.y + aiState.microAdjustment;
251
+    // Blend current and future position based on difficulty
252
+    let targetBallY = lerp(ballPos.y, futureBallY, aiSettings.prediction);
253
+    
254
+    let desiredPaddleY = targetBallY + aiState.microAdjustment + aiState.breathingOffset;
193255
     desiredPaddleY = Math.max(80, Math.min(height - 80, desiredPaddleY));
194256
     
257
+    // Add aggressive positioning - AI tries to "cut off" the ball
258
+    if (ballApproaching && aiSettings.aggression > 0.5) {
259
+        let aggressiveOffset = (ballVel.y > 0 ? 1 : -1) * 20 * aiSettings.aggression;
260
+        desiredPaddleY += aggressiveOffset;
261
+    }
262
+    
195263
     let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
196264
     let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
197265
     
198
-    aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity);
266
+    // Faster interpolation for more responsive movement
267
+    aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity * 1.5);
268
+    
199269
     
200270
     // AI Bop decision logic
201271
     if (ballApproaching && !aiState.consideringBop && !bopState.right.active) {
@@ -461,48 +531,186 @@ function executeAIMovement(aiSettings, rightSupport) {
461531
     let currentY = rightSupport.position.y;
462532
     let deltaY = aiState.targetY - currentY;
463533
     
464
-    if (Math.abs(deltaY) > 1) {
465
-        let baseSpeed = 0.12 * aiSettings.speed;
534
+    // Lower threshold for more constant movement
535
+    if (Math.abs(deltaY) > 0.5) { // Reduced from 1
536
+        let baseSpeed = 0.15 * aiSettings.speed; // Increased from 0.12
466537
         
467538
         // Apply swing power during swing phase
468539
         if (aiState.mode === 'SWINGING') {
469
-            baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.3);
540
+            baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.5); // Increased multiplier
470541
             
471542
             // Add momentum from windup if available
472543
             if (aiState.targetVelocity > 0) {
473
-                baseSpeed *= (1 + aiState.targetVelocity * 0.1);
474
-                aiState.targetVelocity *= 0.9; // Decay momentum
544
+                baseSpeed *= (1 + aiState.targetVelocity * 0.15); // Increased from 0.1
545
+                aiState.targetVelocity *= 0.85; // Slower decay
475546
             }
476547
         } else if (aiState.mode === 'WINDING_UP') {
477548
             // Enhanced windup speed based on phase and settings
478549
             let windupSpeedMultiplier = aiSettings.windupSpeed / AI_WINDUP_SPEED;
479
-            baseSpeed *= (1.5 + windupSpeedMultiplier);
550
+            baseSpeed *= (2.0 + windupSpeedMultiplier); // Increased from 1.5
480551
             
481552
             // Add extra speed at peak velocity points
482553
             if (aiState.peakReached) {
483
-                baseSpeed *= 1.3;
554
+                baseSpeed *= 1.5; // Increased from 1.3
555
+            }
556
+        } else if (aiState.mode === 'TRACKING' || aiState.mode === 'ANTICIPATING') {
557
+            // Add movement urgency based on ball position
558
+            let ball = window.ball; // Access global ball
559
+            if (ball && ball.velocity.x > 0) {
560
+                let urgency = 1 + (1 - (ball.position.x / window.width)) * aiSettings.aggression;
561
+                baseSpeed *= urgency;
484562
             }
485563
         }
486564
         
487
-        // Apply aggression multiplier
488
-        baseSpeed *= (1 + aiState.aggressionLevel * 0.3);
565
+        // Apply aggression multiplier with higher impact
566
+        baseSpeed *= (1 + aiState.aggressionLevel * 0.5); // Increased from 0.3
489567
         
490568
         let movement = deltaY * baseSpeed;
491
-        movement = Math.max(-SUPPORT_SPEED * 1.1, Math.min(SUPPORT_SPEED * 1.1, movement));
569
+        
570
+        // Allow faster movement for hard AI
571
+        let maxSpeed = SUPPORT_SPEED * (1.2 + aiSettings.aggression * 0.3);
572
+        movement = Math.max(-maxSpeed, Math.min(maxSpeed, movement));
492573
         
493574
         // Import moveSupportEnhanced from game-systems
494575
         const moveSupportEnhanced = window.moveSupportEnhanced;
495576
         moveSupportEnhanced(rightSupport, movement, window.height);
496577
         
497578
         // Update input buffer for visual effects
498
-        window.inputBuffer.right = movement / (SUPPORT_SPEED * 1.1);
579
+        window.inputBuffer.right = movement / maxSpeed;
499580
     } else {
500
-        // Gradually reduce input buffer when AI is not moving
501
-        window.inputBuffer.right *= 0.95;
581
+        // Even when close to target, add small movements for liveliness
582
+        if (aiSettings.idleMovement > 0.5) {
583
+            let tinyMovement = (Math.random() - 0.5) * aiSettings.idleMovement;
584
+            const moveSupportEnhanced = window.moveSupportEnhanced;
585
+            moveSupportEnhanced(rightSupport, tinyMovement, window.height);
586
+            window.inputBuffer.right = tinyMovement / SUPPORT_SPEED;
587
+        } else {
588
+            // Gradually reduce input buffer when AI is not moving
589
+            window.inputBuffer.right *= 0.9; // Slower decay for more visible movement
590
+        }
502591
     }
503592
 }
504593
 
505
-// Utility function - should match p5.js lerp
506
-function lerp(start, stop, amt) {
507
-    return amt * (stop - start) + start;
508
-}
594
+// ============= AI ROTATION SYSTEM =============
595
+function updateAIRotation(currentTime, ball, rightPaddle, width, height, aiSettings) {
596
+    // Only update rotation decision periodically
597
+    if (currentTime - aiState.lastRotationUpdate < AI_ROTATION_UPDATE_RATE) {
598
+        // Still apply rotation smoothly even if not updating decision
599
+        applyAIRotation(aiSettings);
600
+        return;
601
+    }
602
+    
603
+    aiState.lastRotationUpdate = currentTime;
604
+    
605
+    let ballPos = ball.position;
606
+    let ballVel = ball.velocity;
607
+    let paddlePos = rightPaddle.position;
608
+    
609
+    // Check if AI should use rotation
610
+    if (Math.random() > aiSettings.rotationUse) {
611
+        aiState.rotationMode = 'NEUTRAL';
612
+        aiState.targetRotation = 0;
613
+        applyAIRotation(aiSettings);
614
+        return;
615
+    }
616
+    
617
+    // Calculate ball approach
618
+    let ballApproaching = ballVel.x > 0;
619
+    let ballDistance = width - ballPos.x;
620
+    let timeToReach = ballDistance / Math.abs(ballVel.x);
621
+    
622
+    // Predict where ball will be
623
+    let predictedBallY = ballPos.y + ballVel.y * timeToReach * AI_ROTATION_PREDICTION * aiSettings.rotationAnticipation;
624
+    
625
+    // Bounce prediction
626
+    if (predictedBallY < 50) {
627
+        predictedBallY = 100 - predictedBallY;
628
+    } else if (predictedBallY > height - 50) {
629
+        predictedBallY = 2 * (height - 50) - predictedBallY;
630
+    }
631
+    
632
+    // Decide rotation strategy
633
+    if (!ballApproaching || ballDistance > 300) {
634
+        // Return to neutral when ball is far
635
+        aiState.rotationMode = 'NEUTRAL';
636
+        aiState.targetRotation = 0;
637
+    } else if (aiState.mode === 'WINDING_UP' || aiState.mode === 'SWINGING') {
638
+        // Offensive rotation during power shots
639
+        aiState.rotationMode = 'OFFENSIVE';
640
+        
641
+        // Calculate desired shot angle
642
+        let targetY = height / 2; // Aim for center by default
643
+        
644
+        // Try to aim away from player
645
+        if (paddlePos.y < height / 2) {
646
+            targetY = height - 80; // Aim down
647
+        } else {
648
+            targetY = 80; // Aim up
649
+        }
650
+        
651
+        // Calculate required angle
652
+        let deltaY = targetY - paddlePos.y;
653
+        let desiredAngle = Math.atan2(deltaY, width - paddlePos.x) * AI_OFFENSIVE_ANGLE;
654
+        
655
+        // Add some inaccuracy based on difficulty
656
+        let error = (Math.random() - 0.5) * (1 - aiSettings.rotationAccuracy) * 0.5;
657
+        desiredAngle += error;
658
+        
659
+        // Clamp angle
660
+        aiState.targetRotation = Math.max(-ROTATION_MAX_ANGLE, Math.min(ROTATION_MAX_ANGLE, desiredAngle));
661
+        
662
+    } else if (aiState.consideringBop || bopState.right.active) {
663
+        // Trick shot rotation during bop
664
+        aiState.rotationMode = 'TRICK_SHOT';
665
+        
666
+        // More extreme angles for bop shots
667
+        let trickDirection = (paddlePos.y < height / 2) ? 1 : -1;
668
+        aiState.targetRotation = trickDirection * AI_TRICK_SHOT_ANGLE * aiSettings.rotationAccuracy;
669
+        
670
+    } else if (Math.abs(predictedBallY - paddlePos.y) < 30) {
671
+        // Defensive slight angle when ball is coming straight
672
+        aiState.rotationMode = 'DEFENSIVE';
673
+        
674
+        // Slight angle to control return
675
+        let defensiveDirection = (predictedBallY < height / 2) ? -1 : 1;
676
+        aiState.targetRotation = defensiveDirection * AI_DEFENSIVE_ANGLE * aiSettings.rotationAccuracy;
677
+        
678
+    } else {
679
+        // Normal tracking
680
+        aiState.rotationMode = 'NEUTRAL';
681
+        aiState.targetRotation = 0;
682
+    }
683
+    
684
+    // Apply rotation confidence based on AI state
685
+    aiState.rotationConfidence = aiSettings.rotationAccuracy;
686
+    if (aiState.mode === 'RECOVERING') {
687
+        aiState.rotationConfidence *= 0.5; // Less confident when recovering
688
+    }
689
+    
690
+    applyAIRotation(aiSettings);
691
+}
692
+
693
+function applyAIRotation(aiSettings) {
694
+    // Get current rotation state
695
+    let rotState = window.rotationState.right;
696
+    
697
+    // Calculate rotation input needed
698
+    let angleDiff = aiState.targetRotation - rotState.currentAngle;
699
+    let rotationInput = 0;
700
+    
701
+    if (Math.abs(angleDiff) > 0.05) {
702
+        // Determine rotation direction
703
+        rotationInput = Math.sign(angleDiff);
704
+        
705
+        // Scale by AI rotation speed and confidence
706
+        rotationInput *= aiSettings.rotationSpeed * aiState.rotationConfidence;
707
+        
708
+        // Add some imperfection for easier difficulties
709
+        if (aiSettings.rotationAccuracy < 0.8) {
710
+            rotationInput += (Math.random() - 0.5) * 0.2 * (1 - aiSettings.rotationAccuracy);
711
+        }
712
+    }
713
+    
714
+    // Update rotation physics for AI paddle
715
+    updateRotationPhysics('right', rotationInput, window.rightPaddle);
716
+}
js/game-systems.jsmodified
@@ -1,5 +1,4 @@
11
 // game-systems.js - Core game mechanics and physics
2
-// TODO redocument
32
 
43
 // ============= CONSTANTS =============
54
 // Canvas settings
@@ -7,7 +6,7 @@ const CANVAS_WIDTH = 800;
76
 const CANVAS_HEIGHT = 400;
87
 
98
 // Game constants
10
-const BALL_SPEED    = 6;
9
+const BALL_SPEED    = 5;
1110
 const BALL_RADIUS   = 12;
1211
 const PADDLE_WIDTH  = 20;
1312
 const PADDLE_HEIGHT = 80;
@@ -42,11 +41,23 @@ const SPRING_PARTICLE_RATE = 0.3;
4241
 
4342
 // Bop system constants
4443
 const BOP_FORCE             = 1.0;  // self explanatory.      BOP   it.
45
-const BOP_RANGE             = 500;  // also self explanatory. TWIST it.
46
-const BOP_DURATION          = 300;  // traversal duration.    SHAKE it.
47
-const BOP_COOLDOWN          = 500;  // also also self expl.   PULL  it.
48
-const ANCHOR_RECOIL         = 40;   // How far the anchor moves backward during bop
49
-const BOP_VELOCITY_BOOST    = 12;   // Initial velocity boost for paddle
44
+const BOP_RANGE             = 500;   // also self explanatory. TWIST it.
45
+const BOP_DURATION          = 1000; // traversal duration.    SHAKE it.
46
+const BOP_COOLDOWN          = 0;  // also also self expl.   PULL  it.
47
+const ANCHOR_RECOIL         = 60;   // How far the anchor moves backward during bop
48
+const BOP_VELOCITY_BOOST    = 5;    // Initial velocity boost for paddle
49
+
50
+// Rotation control constants
51
+const ROTATION_SPEED        = 0.05;  // Base rotation speed (radians per frame)
52
+const ROTATION_SMOOTHING    = 0.15;  // Input smoothing for rotation (0-1)
53
+const ROTATION_DAMPING      = 0.92;  // Angular velocity damping (0-1)
54
+const ROTATION_MAX_SPEED    = 0.2;   // Maximum angular velocity (radians per frame)
55
+const ROTATION_RESISTANCE   = 3.0;   // How much harder it is to rotate against momentum
56
+const ROTATION_MOMENTUM     = 0.85;  // How much angular momentum is preserved (0-1)
57
+const ROTATION_RETURN_FORCE = 0.02;  // Force returning paddle to neutral position
58
+const ROTATION_MAX_ANGLE    = Math.PI / 4; // Maximum rotation angle (45 degrees)
59
+const ROTATION_WITH_MOMENTUM_BOOST = 1.5;  // Speed multiplier when rotating with momentum
60
+const ROTATION_AGAINST_MOMENTUM_LAG = 0.05; // Lag multiplier when rotating against momentum
5061
 
5162
 // ============= BOP SYSTEM =============
5263
 let bopState = {
@@ -70,12 +81,33 @@ let bopState = {
7081
     }
7182
 };
7283
 
84
+// ============= ROTATION SYSTEM =============
85
+let rotationState = {
86
+    left: {
87
+        targetAngle: 0,           // Target angle based on input
88
+        currentAngle: 0,          // Current visual angle
89
+        angularVelocity: 0,       // Current angular velocity
90
+        angularMomentum: 0,       // Angular momentum
91
+        inputBuffer: 0,           // Smoothed rotation input (-1 to 1)
92
+        lastDirection: 0          // Last input direction for momentum checks
93
+    },
94
+    right: {
95
+        targetAngle: 0,
96
+        currentAngle: 0,
97
+        angularVelocity: 0,
98
+        angularMomentum: 0,
99
+        inputBuffer: 0,
100
+        lastDirection: 0
101
+    }
102
+};
103
+
73104
 function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, leftSupport, rightSupport, engine, particles) {
74105
     // Left player bop - use Left Shift for both modes
75106
     let leftBopPressed = keys['Shift'] && !keys['Control'];
76107
     
108
+    // BOP_COOLDOWN controls the minimum time between bops
77109
     if (leftBopPressed && !bopState.left.active && 
78
-        currentTime - bopState.left.lastBopTime > bopState.left.cooldown) {
110
+        currentTime - bopState.left.lastBopTime > BOP_COOLDOWN) {
79111
         activateBop('left', currentTime, leftPaddle, leftSupport, engine, particles);
80112
     }
81113
     
@@ -83,8 +115,9 @@ function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, l
83115
     if (!aiEnabled) {
84116
         let rightBopPressed = keys['Enter'];
85117
         
118
+        // BOP_COOLDOWN controls the minimum time between bops
86119
         if (rightBopPressed && !bopState.right.active && 
87
-            currentTime - bopState.right.lastBopTime > bopState.right.cooldown) {
120
+            currentTime - bopState.right.lastBopTime > BOP_COOLDOWN) {
88121
             activateBop('right', currentTime, rightPaddle, rightSupport, engine, particles);
89122
         }
90123
     }
@@ -95,7 +128,6 @@ function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, l
95128
 
96129
 function activateBop(side, currentTime, paddle, support, engine, particles) {
97130
     const Body = Matter.Body;
98
-    const Engine = Matter.Engine;
99131
     
100132
     bopState[side].active = true;
101133
     bopState[side].startTime = currentTime;
@@ -104,41 +136,40 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
104136
     // Calculate direction from support to paddle
105137
     let dx = paddle.position.x - support.position.x;
106138
     let dy = paddle.position.y - support.position.y;
107
-    
139
+
108140
     // Normalize direction
109141
     let magnitude = Math.sqrt(dx * dx + dy * dy);
110142
     if (magnitude > 0) {
111143
         dx /= magnitude;
112144
         dy /= magnitude;
113145
         
114
-        // Calculate anchor recoil distance
115
-        let anchorRecoilDistance = ANCHOR_RECOIL * 0.4;
116
-        
117
-        // Move the support BACKWARD (recoil effect)
118
-        let newSupportX = support.position.x - dx * anchorRecoilDistance;
119
-        let newSupportY = support.position.y - dy * anchorRecoilDistance;
120
-        
121
-        Body.setPosition(support, { x: newSupportX, y: newSupportY });
122
-        
123
-        // Store original support position for recovery
146
+        // ANCHOR_RECOIL now properly controls the recoil distance
147
+        let anchorRecoilDistance = ANCHOR_RECOIL * 0.3; // Can adjust the 0.3 multiplier
148
+
149
+        // Move support and paddle backward together
150
+        Body.translate(support, { x: -dx * anchorRecoilDistance, y: -dy * anchorRecoilDistance });
151
+        Body.translate(paddle,  { x: -dx * anchorRecoilDistance, y: -dy * anchorRecoilDistance });
152
+
153
+        // Remember where the support started so we can ease it back later
124154
         bopState[side].originalPos = { 
125
-            x: support.position.x + dx * anchorRecoilDistance, 
155
+            x: support.position.x + dx * anchorRecoilDistance,
126156
             y: support.position.y + dy * anchorRecoilDistance 
127157
         };
128158
         
129
-        // Set paddle velocity directly for immediate forward thrust
130
-        let forwardSpeed = (BOP_RANGE / SPRING_LENGTH) * BOP_VELOCITY_BOOST;
159
+        // BOP_VELOCITY_BOOST controls the initial forward velocity
160
+        let forwardSpeed = BOP_VELOCITY_BOOST;
131161
         Body.setVelocity(paddle, {
132162
             x: paddle.velocity.x + dx * forwardSpeed,
133163
             y: paddle.velocity.y + dy * forwardSpeed
134164
         });
135
-        
136
-        // Apply a strong forward force for continued acceleration
165
+
166
+        // BOP_FORCE controls the sustained forward thrust
167
+        // Combined with BOP_RANGE for the total force applied
137168
         Body.applyForce(paddle, paddle.position, {
138
-            x: dx * bopState[side].power * BOP_RANGE * 0.1,
139
-            y: dy * bopState[side].power * BOP_RANGE * 0.1
169
+            x: dx * BOP_FORCE * BOP_RANGE * 0.002,  // Scaled down for Matter.js
170
+            y: dy * BOP_FORCE * BOP_RANGE * 0.002
140171
         });
141
-        
172
+
142173
         // Create particle burst for visual feedback
143174
         for (let i = 0; i < 5; i++) {
144175
             let angle = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.5;
@@ -155,12 +186,9 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
155186
                 type: 'impact'
156187
             });
157188
         }
158
-        
159
-        // Force collision detection update
160
-        Engine.update(engine, 0);
161189
     }
162190
     
163
-    console.log(side + " player BOP!");
191
+    console.log(`${side} player BOP! Duration: ${BOP_DURATION}ms, Cooldown: ${BOP_COOLDOWN}ms`);
164192
 }
165193
 
166194
 function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rightPaddle) {
@@ -169,17 +197,19 @@ function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rig
169197
     // Update left bop
170198
     if (bopState.left.active) {
171199
         let elapsed = currentTime - bopState.left.startTime;
172
-        let progress = elapsed / bopState.left.duration;
200
+        let progress = elapsed / BOP_DURATION;  // BOP_DURATION controls how long the effect lasts
173201
         
174202
         if (progress >= 1.0) {
175203
             bopState.left.active = false;
176204
             bopState.left.originalPos = null;
205
+            console.log("Left bop ended after", elapsed, "ms");
177206
         } else {
178207
             if (bopState.left.originalPos) {
179208
                 let support = leftSupport;
180209
                 let currentX = support.position.x;
181210
                 let currentY = support.position.y;
182211
                 
212
+                // Smooth return motion with easing
183213
                 let returnSpeed = 0.15 * (1 - Math.pow(1 - progress, 3));
184214
                 let newX = currentX + (bopState.left.originalPos.x - currentX) * returnSpeed;
185215
                 let newY = currentY + (bopState.left.originalPos.y - currentY) * returnSpeed;
@@ -191,14 +221,15 @@ function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rig
191221
         }
192222
     }
193223
     
194
-    // Update right bop
224
+    // Update right bop (same logic)
195225
     if (bopState.right.active) {
196226
         let elapsed = currentTime - bopState.right.startTime;
197
-        let progress = elapsed / bopState.right.duration;
227
+        let progress = elapsed / BOP_DURATION;  // BOP_DURATION controls how long the effect lasts
198228
         
199229
         if (progress >= 1.0) {
200230
             bopState.right.active = false;
201231
             bopState.right.originalPos = null;
232
+            console.log("Right bop ended after", elapsed, "ms");
202233
         } else {
203234
             if (bopState.right.originalPos) {
204235
                 let support = rightSupport;
@@ -223,6 +254,7 @@ function limitBopRange(support, paddle) {
223254
     let currentDistance = dist(support.position.x, support.position.y,
224255
                               paddle.position.x, paddle.position.y);
225256
     
257
+    // BOP_RANGE controls the maximum extension allowed
226258
     let maxDistance = SPRING_LENGTH + BOP_RANGE;
227259
     if (currentDistance > maxDistance) {
228260
         let dx = paddle.position.x - support.position.x;
@@ -232,11 +264,14 @@ function limitBopRange(support, paddle) {
232264
         dx /= magnitude;
233265
         dy /= magnitude;
234266
         
267
+        // Clamp paddle position to max range
235268
         let newX = support.position.x + dx * maxDistance;
236269
         let newY = support.position.y + dy * maxDistance;
237270
         
238271
         let currentVel = paddle.velocity;
239272
         Body.setPosition(paddle, { x: newX, y: newY });
273
+        
274
+        // Dampen velocity when hitting the range limit
240275
         Body.setVelocity(paddle, { 
241276
             x: currentVel.x * 0.7, 
242277
             y: currentVel.y * 0.7 
@@ -265,6 +300,9 @@ function createSpringPaddleSystem(side, width, height) {
265300
         frictionAir: side === 'left' ? 0.005 : 0.008,
266301
         isSensor: false,
267302
         slop: 0.01,
303
+        // Add rotational physics
304
+        inertia: PADDLE_MASS * 200,  // Lower inertia = more rotation
305
+        frictionAngular: 0.02,       // Slight angular damping
268306
         render: {
269307
             fillStyle: side === 'left' ? '#00ff88' : '#ff6464'
270308
         }
@@ -282,7 +320,10 @@ function createSpringPaddleSystem(side, width, height) {
282320
         bodyB: paddle,
283321
         length: SPRING_LENGTH,
284322
         stiffness: SPRING_STIFFNESS,
285
-        damping: SPRING_DAMPING
323
+        damping: SPRING_DAMPING,
324
+        // Add angular stiffness to create torque from movement
325
+        angularStiffness: 0.01,  // Allows paddle to rotate based on spring tension
326
+        render: { visible: false }
286327
     });
287328
     
288329
     return { support, paddle, spring };
@@ -350,6 +391,12 @@ function resetBall(ball, world, width, height) {
350391
 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
351392
     const Body = Matter.Body;
352393
     
394
+    // Track collision state to prevent double-triggering
395
+    let collisionState = {
396
+        left: { inCollision: false, lastCollisionTime: 0 },
397
+        right: { inCollision: false, lastCollisionTime: 0 }
398
+    };
399
+    
353400
     Matter.Events.on(engine, 'collisionStart', function(event) {
354401
         let pairs = event.pairs;
355402
         
@@ -361,38 +408,217 @@ function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles
361408
                 
362409
                 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
363410
                 let isLeftPaddle = paddle === leftPaddle;
411
+                let side = isLeftPaddle ? 'left' : 'right';
364412
                 
413
+                // Prevent multiple collisions in quick succession
414
+                let currentTime = Date.now();
415
+                if (currentTime - collisionState[side].lastCollisionTime < 100) {
416
+                    continue; // Skip if we just had a collision
417
+                }
418
+                
419
+                collisionState[side].lastCollisionTime = currentTime;
420
+                
421
+                // Apply bop boost if paddle is currently bopping
365422
                 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
366
-                    let ballVel = ball.velocity;
367
-                    let paddleVel = paddle.velocity;
423
+                    // Get actual collision point from Matter.js
424
+                    let collision = pair.collision;
425
+                    let contactPoint = collision.supports[0];
368426
                     
369
-                    let boostX = paddleVel.x * 0.5;
370
-                    let boostY = paddleVel.y * 0.5;
427
+                    // Check if this is a valid collision
428
+                    let isValidCollision = false;
371429
                     
372
-                    Body.setVelocity(ball, {
373
-                        x: ballVel.x * 1.3 + boostX,
374
-                        y: ballVel.y * 1.3 + boostY
375
-                    });
430
+                    if (contactPoint) {
431
+                        isValidCollision = true;
432
+                    } else {
433
+                        // Use penetration depth as fallback to avoid false positives
434
+                        const depth = collision.depth || 0;
435
+                        isValidCollision = depth > 0.5;
436
+                    }
376437
                     
377
-                    // Create impact particles
378
-                    for (let j = 0; j < IMPACT_PARTICLES; j++) {
379
-                        let angle = Math.random() * Math.PI * 2;
380
-                        let speed = Math.random() * 6 + 2;
438
+                    if (isValidCollision) {
439
+                        // During bop, ALWAYS apply boost
440
+                        let ballVel = ball.velocity;
441
+                        let paddleVel = paddle.velocity;
442
+                        
443
+                        // Calculate the normal collision response first
444
+                        let normal = collision.normal;
445
+                        let relativeVelocity = {
446
+                            x: ballVel.x - paddleVel.x,
447
+                            y: ballVel.y - paddleVel.y
448
+                        };
381449
                         
382
-                        particles.push({
383
-                            x: ball.position.x,
384
-                            y: ball.position.y,
385
-                            vx: Math.cos(angle) * speed - ballVel.x * 0.2,
386
-                            vy: Math.sin(angle) * speed - ballVel.y * 0.2,
387
-                            size: Math.random() * 4 + 2,
388
-                            life: PARTICLE_LIFE,
389
-                            maxLife: PARTICLE_LIFE,
390
-                            color: { r: 255, g: Math.random() * 155 + 100, b: Math.random() * 50 + 100 },
391
-                            type: 'impact'
392
-                        });
450
+                        // Calculate the velocity along the collision normal
451
+                        let velocityAlongNormal = relativeVelocity.x * normal.x + relativeVelocity.y * normal.y;
452
+                        
453
+                        // Only apply boost if ball is approaching paddle
454
+                        if (velocityAlongNormal < 0) {
455
+                            // Base reflection velocity (enhanced during bop)
456
+                            let restitution = 1.5; // Higher restitution during bop
457
+                            let impulse = 2 * velocityAlongNormal * restitution;
458
+                            
459
+                            // Apply the impulse
460
+                            let newVelX = ballVel.x - impulse * normal.x;
461
+                            let newVelY = ballVel.y - impulse * normal.y;
462
+                            
463
+                            // Add paddle velocity influence (more during bop)
464
+                            let paddleInfluence = 0.6; // Higher influence during bop
465
+                            newVelX += paddleVel.x * paddleInfluence;
466
+                            newVelY += paddleVel.y * paddleInfluence;
467
+                            
468
+                            // Add bop boost in the direction of the normal
469
+                            let bopBoostMagnitude = 3; // Extra boost during bop
470
+                            newVelX += normal.x * bopBoostMagnitude;
471
+                            newVelY += normal.y * bopBoostMagnitude;
472
+                            
473
+                            // Ensure minimum speed after bop
474
+                            let newSpeed = Math.sqrt(newVelX * newVelX + newVelY * newVelY);
475
+                            let minSpeed = BALL_SPEED * 1.3; // At least 30% faster than normal
476
+                            
477
+                            if (newSpeed < minSpeed) {
478
+                                let scale = minSpeed / newSpeed;
479
+                                newVelX *= scale;
480
+                                newVelY *= scale;
481
+                            }
482
+                            
483
+                            // Apply the new velocity
484
+                            Body.setVelocity(ball, {
485
+                                x: newVelX,
486
+                                y: newVelY
487
+                            });
488
+                            
489
+                            // Move ball slightly away from paddle to prevent sticking
490
+                            let separation = 2; // pixels
491
+                            Body.setPosition(ball, {
492
+                                x: ball.position.x + normal.x * separation,
493
+                                y: ball.position.y + normal.y * separation
494
+                            });
495
+                            
496
+                            // Create impact particles
497
+                            for (let j = 0; j < IMPACT_PARTICLES * 2; j++) { // More particles for bop
498
+                                let angle = Math.atan2(normal.y, normal.x) + (Math.random() - 0.5) * Math.PI;
499
+                                let speed = Math.random() * 8 + 4;
500
+                                
501
+                                particles.push({
502
+                                    x: contactPoint ? contactPoint.x : ball.position.x,
503
+                                    y: contactPoint ? contactPoint.y : ball.position.y,
504
+                                    vx: Math.cos(angle) * speed,
505
+                                    vy: Math.sin(angle) * speed,
506
+                                    size: Math.random() * 5 + 3,
507
+                                    life: PARTICLE_LIFE,
508
+                                    maxLife: PARTICLE_LIFE,
509
+                                    color: { r: 255, g: Math.random() * 155 + 100, b: 50 },
510
+                                    type: 'impact'
511
+                                });
512
+                            }
513
+                            
514
+                            console.log(`BOP HIT! Clean bounce. New speed: ${newSpeed.toFixed(1)}, Side: ${side}`);
515
+                        }
393516
                     }
394517
                 }
395518
             }
396519
         }
397520
     });
521
+    
522
+    // Reset collision state on collision end
523
+    Matter.Events.on(engine, 'collisionEnd', function(event) {
524
+        let pairs = event.pairs;
525
+        
526
+        for (let i = 0; i < pairs.length; i++) {
527
+            let pair = pairs[i];
528
+            
529
+            if ((pair.bodyA === ball && (pair.bodyB === leftPaddle || pair.bodyB === rightPaddle)) ||
530
+                (pair.bodyB === ball && (pair.bodyA === leftPaddle || pair.bodyA === rightPaddle))) {
531
+                
532
+                let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
533
+                let isLeftPaddle = paddle === leftPaddle;
534
+                let side = isLeftPaddle ? 'left' : 'right';
535
+                
536
+                collisionState[side].inCollision = false;
537
+            }
538
+        }
539
+    });
540
+}
541
+
542
+// ============= ROTATION SYSTEM =============
543
+function handleRotationInput() {
544
+    // Left paddle rotation (A/D keys)
545
+    let leftRotationInput = 0;
546
+    if (keys['a'] || keys['A']) leftRotationInput -= 1;
547
+    if (keys['d'] || keys['D']) leftRotationInput += 1;
548
+    
549
+    // Right paddle rotation (Left/Right arrows, only if AI disabled)
550
+    let rightRotationInput = 0;
551
+    if (!aiEnabled) {
552
+        if (keys['ArrowLeft']) rightRotationInput -= 1;
553
+        if (keys['ArrowRight']) rightRotationInput += 1;
554
+    }
555
+    
556
+    // Update rotation states
557
+    updateRotationPhysics('left', leftRotationInput, leftPaddle);
558
+    if (!aiEnabled) {
559
+        updateRotationPhysics('right', rightRotationInput, rightPaddle);
560
+    }
561
+}
562
+
563
+function updateRotationPhysics(side, input, paddle) {
564
+    const Body = Matter.Body;
565
+    let state = rotationState[side];
566
+    
567
+    // Smooth the input
568
+    state.inputBuffer = lerp(state.inputBuffer, input, ROTATION_SMOOTHING);
569
+    
570
+    // Calculate resistance based on momentum
571
+    let rotatingWithMomentum = (state.inputBuffer * state.angularVelocity) > 0;
572
+    let effectiveInput = state.inputBuffer;
573
+    
574
+    if (rotatingWithMomentum && Math.abs(state.inputBuffer) > 0.1) {
575
+        // Easier to rotate with momentum
576
+        effectiveInput *= ROTATION_WITH_MOMENTUM_BOOST;
577
+    } else if (!rotatingWithMomentum && Math.abs(state.inputBuffer) > 0.1) {
578
+        // Harder to rotate against momentum
579
+        effectiveInput *= ROTATION_AGAINST_MOMENTUM_LAG;
580
+    }
581
+    
582
+    // Apply torque based on input
583
+    let torque = effectiveInput * ROTATION_SPEED;
584
+    
585
+    // Update angular velocity with torque
586
+    state.angularVelocity += torque;
587
+    
588
+    // Apply damping
589
+    state.angularVelocity *= ROTATION_DAMPING;
590
+    
591
+    // Limit maximum angular velocity
592
+    state.angularVelocity = Math.max(-ROTATION_MAX_SPEED, 
593
+                                    Math.min(ROTATION_MAX_SPEED, state.angularVelocity));
594
+    
595
+    // Apply return-to-center force when no input
596
+    if (Math.abs(state.inputBuffer) < 0.1) {
597
+        let returnForce = -state.currentAngle * ROTATION_RETURN_FORCE;
598
+        state.angularVelocity += returnForce;
599
+    }
600
+    
601
+    // Update current angle
602
+    state.currentAngle += state.angularVelocity;
603
+    
604
+    // Limit maximum rotation angle
605
+    state.currentAngle = Math.max(-ROTATION_MAX_ANGLE, 
606
+                                 Math.min(ROTATION_MAX_ANGLE, state.currentAngle));
607
+    
608
+    // Apply rotation to the paddle body
609
+    Body.setAngle(paddle, state.currentAngle);
610
+    
611
+    // Update angular momentum for next frame
612
+    state.angularMomentum = state.angularMomentum * ROTATION_MOMENTUM + 
613
+                           state.angularVelocity * (1 - ROTATION_MOMENTUM);
614
+    
615
+    // Track last direction for momentum calculations
616
+    if (Math.abs(input) > 0.1) {
617
+        state.lastDirection = Math.sign(input);
618
+    }
619
+}
620
+
621
+// ============= HELPER FUNCTIONS =============
622
+function lerp(start, stop, amt) {
623
+    return amt * (stop - start) + start;
398624
 }
js/rendering.jsmodified
@@ -344,32 +344,44 @@ function drawDebugInfo(ball, leftSupport, leftPaddle, rightSupport, rightPaddle,
344344
     text(`R Spring: ${Math.round(rightSpringLength)}px (${((SPRING_LENGTH/rightSpringLength - 1) * 100).toFixed(0)}%)`, 10, 95);
345345
     text(`Input: L=${inputBuffer.left.toFixed(2)} R=${inputBuffer.right.toFixed(2)}`, 10, 110);
346346
     
347
+    // Rotation info
348
+    if (window.rotationState) {
349
+        text(`L Rot: ${(window.rotationState.left.currentAngle * 180 / Math.PI).toFixed(1)}° (vel: ${window.rotationState.left.angularVelocity.toFixed(3)})`, 10, 125);
350
+        text(`R Rot: ${(window.rotationState.right.currentAngle * 180 / Math.PI).toFixed(1)}° (vel: ${window.rotationState.right.angularVelocity.toFixed(3)})`, 10, 140);
351
+    }
352
+    
347353
     // AI debug info
348354
     if (aiEnabled) {
349
-        text(`AI State: ${aiState.mode} | Aggression: ${aiState.aggressionLevel.toFixed(2)}`, 10, 125);
350
-        text(`Target: ${Math.round(aiState.targetY)} | Intercept: ${Math.round(aiState.interceptY)}`, 10, 140);
351
-        text(`Ball: (${Math.round(ball.position.x)}, ${Math.round(ball.position.y)}) Vel: (${ball.velocity.x.toFixed(1)}, ${ball.velocity.y.toFixed(1)})`, 10, 155);
355
+        text(`AI State: ${aiState.mode} | Aggression: ${aiState.aggressionLevel.toFixed(2)}`, 10, 155);
356
+        text(`Target: ${Math.round(aiState.targetY)} | Intercept: ${Math.round(aiState.interceptY)}`, 10, 170);
357
+        text(`Ball: (${Math.round(ball.position.x)}, ${Math.round(ball.position.y)}) Vel: (${ball.velocity.x.toFixed(1)}, ${ball.velocity.y.toFixed(1)})`, 10, 185);
358
+        
359
+        // AI rotation info
360
+        if (aiState.rotationMode !== 'NEUTRAL') {
361
+            fill(150, 200, 255, 200);
362
+            text(`AI Rotation: ${aiState.rotationMode} | Target: ${(aiState.targetRotation * 180 / Math.PI).toFixed(1)}°`, 10, 200);
363
+        }
352364
         
353365
         // AI technique indicators
354366
         if (aiState.mode === 'WINDING_UP') {
355367
             fill(255, 150, 50, 200);
356
-            text(`AI WINDING UP | Phase: ${(aiState.windupPhase % (Math.PI * 2)).toFixed(2)} | Velocity: ${aiState.currentVelocity.toFixed(1)}`, 10, 175);
368
+            text(`AI WINDING UP | Phase: ${(aiState.windupPhase % (Math.PI * 2)).toFixed(2)} | Velocity: ${aiState.currentVelocity.toFixed(1)}`, 10, 215);
357369
             
358370
             if (aiState.comboBop) {
359371
                 fill(255, 50, 255, 200);
360
-                text("⚡ COMBO PLANNED!", 10, 190);
372
+                text("⚡ COMBO PLANNED!", 10, 230);
361373
             }
362374
         } else if (aiState.mode === 'SWINGING') {
363375
             fill(255, 50, 50, 200);
364
-            text("AI POWER SWING!", 10, 175);
376
+            text("AI POWER SWING!", 10, 215);
365377
         } else if (aiState.consideringBop) {
366378
             fill(255, 255, 100, 200);
367
-            text("AI PREPARING BOP!", 10, 175);
379
+            text("AI PREPARING BOP!", 10, 215);
368380
         }
369381
         
370382
         if (bopState.right.active) {
371383
             fill(255, 255, 0, 255);
372
-            text("AI BOPPING!", 10, 190);
384
+            text("AI BOPPING!", 10, 230);
373385
         }
374386
     }
375387
 }
@@ -382,10 +394,11 @@ function drawStartMessage(aiEnabled, aiDifficulty) {
382394
     textSize(14);
383395
     
384396
     if (aiEnabled) {
385
-        text("Player vs CPU | Left paddle: W/S or Mouse/Touch", width/2, height/2 + 125);
397
+        text("Player vs CPU | Move: W/S or Mouse/Touch | Rotate: A/D | Bop: Left Shift", width/2, height/2 + 125);
386398
         text(`AI Difficulty: ${aiDifficulty.toUpperCase()}`, width/2, height/2 + 145);
387399
     } else {
388
-        text("2 Player Mode | P1: W/S | P2: ↑/↓ | Mouse/Touch: Drag paddles", width/2, height/2 + 125);
400
+        text("2 Player Mode | P1: W/S + A/D + L.Shift | P2: ↑/↓ + ←/→ + Enter", width/2, height/2 + 125);
401
+        text("Mouse/Touch: Drag paddles", width/2, height/2 + 145);
389402
     }
390403
     
391404
     textSize(12);
@@ -451,9 +464,9 @@ function drawMenu(menuState) {
451464
     textSize(12);
452465
     fill(255, 100);
453466
     if (menuState.selectedOption === 0) {
454
-        text("Controls: W/S keys + LEFT SHIFT (bop) or Mouse/Touch", width/2, height - 30);
467
+        text("Controls: W/S (move) + A/D (rotate) + LEFT SHIFT (bop) or Mouse/Touch", width/2, height - 30);
455468
     } else {
456
-        text("Controls: P1 (W/S + L.Shift) | P2 (↑/↓ + Enter) | Mouse/Touch", width/2, height - 30);
469
+        text("P1: W/S + A/D + L.Shift | P2: ↑/↓ + ←/→ + Enter | Mouse/Touch", width/2, height - 30);
457470
     }
458471
 }
459472
 
js/sprong.jsmodified
@@ -48,6 +48,11 @@ let mouseInput = {
4848
 // Make necessary variables globally accessible for other scripts
4949
 window.inputBuffer = inputBuffer;
5050
 window.moveSupportEnhanced = moveSupportEnhanced;
51
+window.ball = null;
52
+window.rotationState = rotationState;
53
+window.rightPaddle = null;
54
+window.width = CANVAS_WIDTH;
55
+window.height = CANVAS_HEIGHT;
5156
 
5257
 function setup() {
5358
     let canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
@@ -77,6 +82,8 @@ function setup() {
7782
     
7883
     // Create ball
7984
     ball = resetBall(null, world, width, height);
85
+    window.ball = ball;
86
+    window.rightPaddle = rightPaddle;
8087
     
8188
     // Add everything to the world
8289
     World.add(world, [
@@ -101,11 +108,7 @@ function draw() {
101108
         updateParticles();
102109
         checkBallPosition();
103110
         
104
-        // Enhanced collision detection during bops
105
-        if (bopState.left.active || bopState.right.active) {
106
-            Engine.update(engine, 8);
107
-            Engine.update(engine, 8);
108
-        }
111
+        
109112
         
110113
         // Draw everything
111114
         drawParticles();
@@ -127,6 +130,7 @@ function draw() {
127130
 function handleEnhancedInput() {
128131
     handleKeyboardInput();
129132
     handleMouseTouchInput();
133
+    handleRotationInput();  // Add rotation handling
130134
     handleBopInput(keys, aiEnabled, millis(), leftPaddle, rightPaddle, 
131135
                    leftSupport, rightSupport, engine, particles);
132136
     
@@ -198,6 +202,7 @@ function checkBallPosition() {
198202
         rightScore++;
199203
         updateScore();
200204
         ball = resetBall(ball, world, width, height);
205
+        window.ball = ball;  // ADD THIS LINE - Update global reference
201206
         gameStarted = false;
202207
     }
203208
     
@@ -205,6 +210,7 @@ function checkBallPosition() {
205210
         leftScore++;
206211
         updateScore();
207212
         ball = resetBall(ball, world, width, height);
213
+        window.ball = ball;  // ADD THIS LINE - Update global reference
208214
         gameStarted = false;
209215
     }
210216
 }
@@ -250,6 +256,7 @@ function keyPressed() {
250256
         rightScore = 0;
251257
         updateScore();
252258
         ball = resetBall(ball, world, width, height);
259
+        window.ball = ball;
253260
         gameStarted = false;
254261
         
255262
         inputBuffer.left = 0;