Comparing changes

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

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

Commits on trunk

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,71 @@
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
+
1319
 
1420
 // ============= AI SETTINGS =============
1521
 const AI_SETTINGS = {
1622
     easy: {
17
-        reactionTime: 400,
23
+        reactionTime: 350,
1824
         accuracy: 0.7,
1925
         speed: 0.8,
2026
         prediction: 0.3,
21
-        aggression: 0.2,
27
+        aggression: 0.4,
2228
         oscillation: 0.3,
2329
         bopChance: 0.25,
24
-        // New windup parameters
2530
         windupSpeed: 0.1,
2631
         windupRadius: 30,
2732
         comboBopChance: 0.1,
2833
         circularMotion: 0.4,
29
-        phaseSpeed: 0.06
34
+        phaseSpeed: 0.06,
35
+        idleMovement: 0.3,
36
+        trackingAggression: 0.1
3037
     },
3138
     medium: {
32
-        reactionTime: 250,
39
+        reactionTime: 200,
3340
         accuracy: 0.85,
3441
         speed: 1.0,
3542
         prediction: 0.6,
36
-        aggression: 0.5,
43
+        aggression: 0.7,
3744
         oscillation: 0.7,
3845
         bopChance: 0.55,
39
-        // New windup parameters
4046
         windupSpeed: 0.15,
4147
         windupRadius: 40,
4248
         comboBopChance: 0.3,
4349
         circularMotion: 0.6,
44
-        phaseSpeed: 0.08
50
+        phaseSpeed: 0.08,
51
+        idleMovement: 0.5,
52
+        trackingAggression: 0.15
4553
     },
4654
     hard: {
47
-        reactionTime: 150,
55
+        reactionTime: 100,
4856
         accuracy: 0.95,
49
-        speed: 1.2,
50
-        prediction: 0.8,
51
-        aggression: 0.8,
57
+        speed: 1.5,
58
+        prediction: 0.85,
59
+        aggression: 1.0,
5260
         oscillation: 1.0,
5361
         bopChance: 0.85,
54
-        // New windup parameters
5562
         windupSpeed: 0.2,
56
-        windupRadius: 50,
63
+        windupRadius: 60,
5764
         comboBopChance: 0.5,
5865
         circularMotion: 0.8,
59
-        phaseSpeed: 0.1
66
+        phaseSpeed: 0.12,
67
+        idleMovement: 0.8,
68
+        trackingAggression: 0.25
6069
     }
6170
 };
6271
 
@@ -148,19 +157,32 @@ function handleAI(currentTime, ball, rightPaddle, rightSupport,
148157
 
149158
 // ============= AI BEHAVIORS =============
150159
 function updateAILifelikeBehavior(currentTime, height) {
151
-    aiState.breathingOffset = Math.sin(currentTime * 0.003) * 3;
160
+    let aiSettings = AI_SETTINGS[aiState.difficulty];
161
+    
162
+    // More pronounced breathing motion
163
+    aiState.breathingOffset = Math.sin(currentTime * 0.005) * 5 * aiSettings.idleMovement;
152164
     
153
-    if (currentTime - aiState.lastMicroTime > 2000 + Math.random() * 1000) {
154
-        aiState.microAdjustment = (Math.random() - 0.5) * 15;
165
+    // More frequent micro-adjustments
166
+    if (currentTime - aiState.lastMicroTime > 1000 + Math.random() * 500) {
167
+        aiState.microAdjustment = (Math.random() - 0.5) * 30 * aiSettings.idleMovement;
155168
         aiState.lastMicroTime = currentTime;
156169
     }
157170
     
158
-    aiState.microAdjustment *= 0.98;
171
+    // Slower decay for more persistent movement
172
+    aiState.microAdjustment *= 0.95;
173
+    
174
+    // Add "nervous energy" - small random movements
175
+    let nervousEnergy = (Math.random() - 0.5) * AI_NERVOUS_ENERGY * aiSettings.aggression;
176
+    aiState.microAdjustment += nervousEnergy;
159177
     
160178
     if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
161179
         let centerY = height / 2;
162
-        let wanderRadius = 25;
163
-        aiState.idleTarget = centerY + Math.sin(currentTime * 0.002) * wanderRadius;
180
+        let wanderRadius = 40 * aiSettings.idleMovement; // Larger wander radius
181
+        aiState.idleTarget = centerY + Math.sin(currentTime * 0.003) * wanderRadius;
182
+        
183
+        // Add vertical patrol behavior
184
+        let patrolOffset = Math.sin(currentTime * 0.002) * 30 * aiSettings.idleMovement;
185
+        aiState.idleTarget += patrolOffset;
164186
     }
165187
 }
166188
 
@@ -187,15 +209,33 @@ function handleAITracking(currentTime, ballPos, ballVel, aiSettings,
187209
     let paddlePos = rightPaddle.position;
188210
     let anchorPos = rightSupport.position;
189211
     
190
-    let trackingIntensity = ballApproaching ? 0.08 : 0.03;
212
+    // More aggressive tracking based on difficulty
213
+    let trackingIntensity = ballApproaching ? 
214
+        (0.08 + aiSettings.trackingAggression) : 
215
+        (0.03 + aiSettings.trackingAggression * 0.5);
216
+    
217
+    // Predict where ball will be and move preemptively
218
+    let futureTime = 0.5; // Look ahead 0.5 seconds
219
+    let futureBallY = ballPos.y + ballVel.y * futureTime * 60; // 60 fps assumption
191220
     
192
-    let desiredPaddleY = ballPos.y + aiState.microAdjustment;
221
+    // Blend current and future position based on difficulty
222
+    let targetBallY = lerp(ballPos.y, futureBallY, aiSettings.prediction);
223
+    
224
+    let desiredPaddleY = targetBallY + aiState.microAdjustment + aiState.breathingOffset;
193225
     desiredPaddleY = Math.max(80, Math.min(height - 80, desiredPaddleY));
194226
     
227
+    // Add aggressive positioning - AI tries to "cut off" the ball
228
+    if (ballApproaching && aiSettings.aggression > 0.5) {
229
+        let aggressiveOffset = (ballVel.y > 0 ? 1 : -1) * 20 * aiSettings.aggression;
230
+        desiredPaddleY += aggressiveOffset;
231
+    }
232
+    
195233
     let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
196234
     let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
197235
     
198
-    aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity);
236
+    // Faster interpolation for more responsive movement
237
+    aiState.targetY = lerp(aiState.targetY, targetAnchorY, trackingIntensity * 1.5);
238
+    
199239
     
200240
     // AI Bop decision logic
201241
     if (ballApproaching && !aiState.consideringBop && !bopState.right.active) {
@@ -461,44 +501,63 @@ function executeAIMovement(aiSettings, rightSupport) {
461501
     let currentY = rightSupport.position.y;
462502
     let deltaY = aiState.targetY - currentY;
463503
     
464
-    if (Math.abs(deltaY) > 1) {
465
-        let baseSpeed = 0.12 * aiSettings.speed;
504
+    // Lower threshold for more constant movement
505
+    if (Math.abs(deltaY) > 0.5) { // Reduced from 1
506
+        let baseSpeed = 0.15 * aiSettings.speed; // Increased from 0.12
466507
         
467508
         // Apply swing power during swing phase
468509
         if (aiState.mode === 'SWINGING') {
469
-            baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.3);
510
+            baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.5); // Increased multiplier
470511
             
471512
             // Add momentum from windup if available
472513
             if (aiState.targetVelocity > 0) {
473
-                baseSpeed *= (1 + aiState.targetVelocity * 0.1);
474
-                aiState.targetVelocity *= 0.9; // Decay momentum
514
+                baseSpeed *= (1 + aiState.targetVelocity * 0.15); // Increased from 0.1
515
+                aiState.targetVelocity *= 0.85; // Slower decay
475516
             }
476517
         } else if (aiState.mode === 'WINDING_UP') {
477518
             // Enhanced windup speed based on phase and settings
478519
             let windupSpeedMultiplier = aiSettings.windupSpeed / AI_WINDUP_SPEED;
479
-            baseSpeed *= (1.5 + windupSpeedMultiplier);
520
+            baseSpeed *= (2.0 + windupSpeedMultiplier); // Increased from 1.5
480521
             
481522
             // Add extra speed at peak velocity points
482523
             if (aiState.peakReached) {
483
-                baseSpeed *= 1.3;
524
+                baseSpeed *= 1.5; // Increased from 1.3
525
+            }
526
+        } else if (aiState.mode === 'TRACKING' || aiState.mode === 'ANTICIPATING') {
527
+            // Add movement urgency based on ball position
528
+            let ball = window.ball; // Access global ball
529
+            if (ball && ball.velocity.x > 0) {
530
+                let urgency = 1 + (1 - (ball.position.x / window.width)) * aiSettings.aggression;
531
+                baseSpeed *= urgency;
484532
             }
485533
         }
486534
         
487
-        // Apply aggression multiplier
488
-        baseSpeed *= (1 + aiState.aggressionLevel * 0.3);
535
+        // Apply aggression multiplier with higher impact
536
+        baseSpeed *= (1 + aiState.aggressionLevel * 0.5); // Increased from 0.3
489537
         
490538
         let movement = deltaY * baseSpeed;
491
-        movement = Math.max(-SUPPORT_SPEED * 1.1, Math.min(SUPPORT_SPEED * 1.1, movement));
539
+        
540
+        // Allow faster movement for hard AI
541
+        let maxSpeed = SUPPORT_SPEED * (1.2 + aiSettings.aggression * 0.3);
542
+        movement = Math.max(-maxSpeed, Math.min(maxSpeed, movement));
492543
         
493544
         // Import moveSupportEnhanced from game-systems
494545
         const moveSupportEnhanced = window.moveSupportEnhanced;
495546
         moveSupportEnhanced(rightSupport, movement, window.height);
496547
         
497548
         // Update input buffer for visual effects
498
-        window.inputBuffer.right = movement / (SUPPORT_SPEED * 1.1);
549
+        window.inputBuffer.right = movement / maxSpeed;
499550
     } else {
500
-        // Gradually reduce input buffer when AI is not moving
501
-        window.inputBuffer.right *= 0.95;
551
+        // Even when close to target, add small movements for liveliness
552
+        if (aiSettings.idleMovement > 0.5) {
553
+            let tinyMovement = (Math.random() - 0.5) * aiSettings.idleMovement;
554
+            const moveSupportEnhanced = window.moveSupportEnhanced;
555
+            moveSupportEnhanced(rightSupport, tinyMovement, window.height);
556
+            window.inputBuffer.right = tinyMovement / SUPPORT_SPEED;
557
+        } else {
558
+            // Gradually reduce input buffer when AI is not moving
559
+            window.inputBuffer.right *= 0.9; // Slower decay for more visible movement
560
+        }
502561
     }
503562
 }
504563
 
js/game-systems.jsmodified
@@ -7,7 +7,7 @@ const CANVAS_WIDTH = 800;
77
 const CANVAS_HEIGHT = 400;
88
 
99
 // Game constants
10
-const BALL_SPEED    = 6;
10
+const BALL_SPEED    = 5;
1111
 const BALL_RADIUS   = 12;
1212
 const PADDLE_WIDTH  = 20;
1313
 const PADDLE_HEIGHT = 80;
@@ -42,11 +42,11 @@ const SPRING_PARTICLE_RATE = 0.3;
4242
 
4343
 // Bop system constants
4444
 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
45
+const BOP_RANGE             = 500;   // also self explanatory. TWIST it.
46
+const BOP_DURATION          = 1000; // traversal duration.    SHAKE it.
47
+const BOP_COOLDOWN          = 0;  // also also self expl.   PULL  it.
48
+const ANCHOR_RECOIL         = 60;   // How far the anchor moves backward during bop
49
+const BOP_VELOCITY_BOOST    = 5;    // Initial velocity boost for paddle
5050
 
5151
 // ============= BOP SYSTEM =============
5252
 let bopState = {
@@ -74,8 +74,9 @@ function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, l
7474
     // Left player bop - use Left Shift for both modes
7575
     let leftBopPressed = keys['Shift'] && !keys['Control'];
7676
     
77
+    // BOP_COOLDOWN controls the minimum time between bops
7778
     if (leftBopPressed && !bopState.left.active && 
78
-        currentTime - bopState.left.lastBopTime > bopState.left.cooldown) {
79
+        currentTime - bopState.left.lastBopTime > BOP_COOLDOWN) {
7980
         activateBop('left', currentTime, leftPaddle, leftSupport, engine, particles);
8081
     }
8182
     
@@ -83,8 +84,9 @@ function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, l
8384
     if (!aiEnabled) {
8485
         let rightBopPressed = keys['Enter'];
8586
         
87
+        // BOP_COOLDOWN controls the minimum time between bops
8688
         if (rightBopPressed && !bopState.right.active && 
87
-            currentTime - bopState.right.lastBopTime > bopState.right.cooldown) {
89
+            currentTime - bopState.right.lastBopTime > BOP_COOLDOWN) {
8890
             activateBop('right', currentTime, rightPaddle, rightSupport, engine, particles);
8991
         }
9092
     }
@@ -95,7 +97,6 @@ function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, l
9597
 
9698
 function activateBop(side, currentTime, paddle, support, engine, particles) {
9799
     const Body = Matter.Body;
98
-    const Engine = Matter.Engine;
99100
     
100101
     bopState[side].active = true;
101102
     bopState[side].startTime = currentTime;
@@ -104,41 +105,40 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
104105
     // Calculate direction from support to paddle
105106
     let dx = paddle.position.x - support.position.x;
106107
     let dy = paddle.position.y - support.position.y;
107
-    
108
+
108109
     // Normalize direction
109110
     let magnitude = Math.sqrt(dx * dx + dy * dy);
110111
     if (magnitude > 0) {
111112
         dx /= magnitude;
112113
         dy /= magnitude;
113114
         
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
115
+        // ANCHOR_RECOIL now properly controls the recoil distance
116
+        let anchorRecoilDistance = ANCHOR_RECOIL * 0.3; // Can adjust the 0.3 multiplier
117
+
118
+        // Move support and paddle backward together
119
+        Body.translate(support, { x: -dx * anchorRecoilDistance, y: -dy * anchorRecoilDistance });
120
+        Body.translate(paddle,  { x: -dx * anchorRecoilDistance, y: -dy * anchorRecoilDistance });
121
+
122
+        // Remember where the support started so we can ease it back later
124123
         bopState[side].originalPos = { 
125
-            x: support.position.x + dx * anchorRecoilDistance, 
124
+            x: support.position.x + dx * anchorRecoilDistance,
126125
             y: support.position.y + dy * anchorRecoilDistance 
127126
         };
128127
         
129
-        // Set paddle velocity directly for immediate forward thrust
130
-        let forwardSpeed = (BOP_RANGE / SPRING_LENGTH) * BOP_VELOCITY_BOOST;
128
+        // BOP_VELOCITY_BOOST controls the initial forward velocity
129
+        let forwardSpeed = BOP_VELOCITY_BOOST;
131130
         Body.setVelocity(paddle, {
132131
             x: paddle.velocity.x + dx * forwardSpeed,
133132
             y: paddle.velocity.y + dy * forwardSpeed
134133
         });
135
-        
136
-        // Apply a strong forward force for continued acceleration
134
+
135
+        // BOP_FORCE controls the sustained forward thrust
136
+        // Combined with BOP_RANGE for the total force applied
137137
         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
138
+            x: dx * BOP_FORCE * BOP_RANGE * 0.002,  // Scaled down for Matter.js
139
+            y: dy * BOP_FORCE * BOP_RANGE * 0.002
140140
         });
141
-        
141
+
142142
         // Create particle burst for visual feedback
143143
         for (let i = 0; i < 5; i++) {
144144
             let angle = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.5;
@@ -155,12 +155,9 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
155155
                 type: 'impact'
156156
             });
157157
         }
158
-        
159
-        // Force collision detection update
160
-        Engine.update(engine, 0);
161158
     }
162159
     
163
-    console.log(side + " player BOP!");
160
+    console.log(`${side} player BOP! Duration: ${BOP_DURATION}ms, Cooldown: ${BOP_COOLDOWN}ms`);
164161
 }
165162
 
166163
 function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rightPaddle) {
@@ -169,17 +166,19 @@ function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rig
169166
     // Update left bop
170167
     if (bopState.left.active) {
171168
         let elapsed = currentTime - bopState.left.startTime;
172
-        let progress = elapsed / bopState.left.duration;
169
+        let progress = elapsed / BOP_DURATION;  // BOP_DURATION controls how long the effect lasts
173170
         
174171
         if (progress >= 1.0) {
175172
             bopState.left.active = false;
176173
             bopState.left.originalPos = null;
174
+            console.log("Left bop ended after", elapsed, "ms");
177175
         } else {
178176
             if (bopState.left.originalPos) {
179177
                 let support = leftSupport;
180178
                 let currentX = support.position.x;
181179
                 let currentY = support.position.y;
182180
                 
181
+                // Smooth return motion with easing
183182
                 let returnSpeed = 0.15 * (1 - Math.pow(1 - progress, 3));
184183
                 let newX = currentX + (bopState.left.originalPos.x - currentX) * returnSpeed;
185184
                 let newY = currentY + (bopState.left.originalPos.y - currentY) * returnSpeed;
@@ -191,14 +190,15 @@ function updateBopStates(currentTime, leftSupport, rightSupport, leftPaddle, rig
191190
         }
192191
     }
193192
     
194
-    // Update right bop
193
+    // Update right bop (same logic)
195194
     if (bopState.right.active) {
196195
         let elapsed = currentTime - bopState.right.startTime;
197
-        let progress = elapsed / bopState.right.duration;
196
+        let progress = elapsed / BOP_DURATION;  // BOP_DURATION controls how long the effect lasts
198197
         
199198
         if (progress >= 1.0) {
200199
             bopState.right.active = false;
201200
             bopState.right.originalPos = null;
201
+            console.log("Right bop ended after", elapsed, "ms");
202202
         } else {
203203
             if (bopState.right.originalPos) {
204204
                 let support = rightSupport;
@@ -223,6 +223,7 @@ function limitBopRange(support, paddle) {
223223
     let currentDistance = dist(support.position.x, support.position.y,
224224
                               paddle.position.x, paddle.position.y);
225225
     
226
+    // BOP_RANGE controls the maximum extension allowed
226227
     let maxDistance = SPRING_LENGTH + BOP_RANGE;
227228
     if (currentDistance > maxDistance) {
228229
         let dx = paddle.position.x - support.position.x;
@@ -232,11 +233,14 @@ function limitBopRange(support, paddle) {
232233
         dx /= magnitude;
233234
         dy /= magnitude;
234235
         
236
+        // Clamp paddle position to max range
235237
         let newX = support.position.x + dx * maxDistance;
236238
         let newY = support.position.y + dy * maxDistance;
237239
         
238240
         let currentVel = paddle.velocity;
239241
         Body.setPosition(paddle, { x: newX, y: newY });
242
+        
243
+        // Dampen velocity when hitting the range limit
240244
         Body.setVelocity(paddle, { 
241245
             x: currentVel.x * 0.7, 
242246
             y: currentVel.y * 0.7 
@@ -265,6 +269,9 @@ function createSpringPaddleSystem(side, width, height) {
265269
         frictionAir: side === 'left' ? 0.005 : 0.008,
266270
         isSensor: false,
267271
         slop: 0.01,
272
+        // Add rotational physics
273
+        inertia: PADDLE_MASS * 200,  // Lower inertia = more rotation
274
+        frictionAngular: 0.02,       // Slight angular damping
268275
         render: {
269276
             fillStyle: side === 'left' ? '#00ff88' : '#ff6464'
270277
         }
@@ -282,7 +289,10 @@ function createSpringPaddleSystem(side, width, height) {
282289
         bodyB: paddle,
283290
         length: SPRING_LENGTH,
284291
         stiffness: SPRING_STIFFNESS,
285
-        damping: SPRING_DAMPING
292
+        damping: SPRING_DAMPING,
293
+        // Add angular stiffness to create torque from movement
294
+        angularStiffness: 0.01,  // Allows paddle to rotate based on spring tension
295
+        render: { visible: false }
286296
     });
287297
     
288298
     return { support, paddle, spring };
@@ -346,10 +356,17 @@ function resetBall(ball, world, width, height) {
346356
     return ball;
347357
 }
348358
 
359
+// ============= COLLISION =============
349360
 // ============= COLLISION =============
350361
 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
351362
     const Body = Matter.Body;
352363
     
364
+    // Track collision state to prevent double-triggering
365
+    let collisionState = {
366
+        left: { inCollision: false, lastCollisionTime: 0 },
367
+        right: { inCollision: false, lastCollisionTime: 0 }
368
+    };
369
+    
353370
     Matter.Events.on(engine, 'collisionStart', function(event) {
354371
         let pairs = event.pairs;
355372
         
@@ -361,38 +378,133 @@ function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles
361378
                 
362379
                 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
363380
                 let isLeftPaddle = paddle === leftPaddle;
381
+                let side = isLeftPaddle ? 'left' : 'right';
382
+                
383
+                // Prevent multiple collisions in quick succession
384
+                let currentTime = Date.now();
385
+                if (currentTime - collisionState[side].lastCollisionTime < 100) {
386
+                    continue; // Skip if we just had a collision
387
+                }
364388
                 
389
+                collisionState[side].lastCollisionTime = currentTime;
390
+                
391
+                // Apply bop boost if paddle is currently bopping
365392
                 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
366
-                    let ballVel = ball.velocity;
367
-                    let paddleVel = paddle.velocity;
393
+                    // Get actual collision point from Matter.js
394
+                    let collision = pair.collision;
395
+                    let contactPoint = collision.supports[0];
368396
                     
369
-                    let boostX = paddleVel.x * 0.5;
370
-                    let boostY = paddleVel.y * 0.5;
397
+                    // Check if this is a valid collision
398
+                    let isValidCollision = false;
371399
                     
372
-                    Body.setVelocity(ball, {
373
-                        x: ballVel.x * 1.3 + boostX,
374
-                        y: ballVel.y * 1.3 + boostY
375
-                    });
400
+                    if (contactPoint) {
401
+                        isValidCollision = true;
402
+                    } else {
403
+                        // Use penetration depth as fallback to avoid false positives
404
+                        const depth = collision.depth || 0;
405
+                        isValidCollision = depth > 0.5;
406
+                    }
376407
                     
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;
408
+                    if (isValidCollision) {
409
+                        // During bop, ALWAYS apply boost
410
+                        let ballVel = ball.velocity;
411
+                        let paddleVel = paddle.velocity;
412
+                        
413
+                        // Calculate the normal collision response first
414
+                        let normal = collision.normal;
415
+                        let relativeVelocity = {
416
+                            x: ballVel.x - paddleVel.x,
417
+                            y: ballVel.y - paddleVel.y
418
+                        };
381419
                         
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
-                        });
420
+                        // Calculate the velocity along the collision normal
421
+                        let velocityAlongNormal = relativeVelocity.x * normal.x + relativeVelocity.y * normal.y;
422
+                        
423
+                        // Only apply boost if ball is approaching paddle
424
+                        if (velocityAlongNormal < 0) {
425
+                            // Base reflection velocity (enhanced during bop)
426
+                            let restitution = 1.5; // Higher restitution during bop
427
+                            let impulse = 2 * velocityAlongNormal * restitution;
428
+                            
429
+                            // Apply the impulse
430
+                            let newVelX = ballVel.x - impulse * normal.x;
431
+                            let newVelY = ballVel.y - impulse * normal.y;
432
+                            
433
+                            // Add paddle velocity influence (more during bop)
434
+                            let paddleInfluence = 0.6; // Higher influence during bop
435
+                            newVelX += paddleVel.x * paddleInfluence;
436
+                            newVelY += paddleVel.y * paddleInfluence;
437
+                            
438
+                            // Add bop boost in the direction of the normal
439
+                            let bopBoostMagnitude = 3; // Extra boost during bop
440
+                            newVelX += normal.x * bopBoostMagnitude;
441
+                            newVelY += normal.y * bopBoostMagnitude;
442
+                            
443
+                            // Ensure minimum speed after bop
444
+                            let newSpeed = Math.sqrt(newVelX * newVelX + newVelY * newVelY);
445
+                            let minSpeed = BALL_SPEED * 1.3; // At least 30% faster than normal
446
+                            
447
+                            if (newSpeed < minSpeed) {
448
+                                let scale = minSpeed / newSpeed;
449
+                                newVelX *= scale;
450
+                                newVelY *= scale;
451
+                            }
452
+                            
453
+                            // Apply the new velocity
454
+                            Body.setVelocity(ball, {
455
+                                x: newVelX,
456
+                                y: newVelY
457
+                            });
458
+                            
459
+                            // Move ball slightly away from paddle to prevent sticking
460
+                            let separation = 2; // pixels
461
+                            Body.setPosition(ball, {
462
+                                x: ball.position.x + normal.x * separation,
463
+                                y: ball.position.y + normal.y * separation
464
+                            });
465
+                            
466
+                            // Create impact particles
467
+                            for (let j = 0; j < IMPACT_PARTICLES * 2; j++) { // More particles for bop
468
+                                let angle = Math.atan2(normal.y, normal.x) + (Math.random() - 0.5) * Math.PI;
469
+                                let speed = Math.random() * 8 + 4;
470
+                                
471
+                                particles.push({
472
+                                    x: contactPoint ? contactPoint.x : ball.position.x,
473
+                                    y: contactPoint ? contactPoint.y : ball.position.y,
474
+                                    vx: Math.cos(angle) * speed,
475
+                                    vy: Math.sin(angle) * speed,
476
+                                    size: Math.random() * 5 + 3,
477
+                                    life: PARTICLE_LIFE,
478
+                                    maxLife: PARTICLE_LIFE,
479
+                                    color: { r: 255, g: Math.random() * 155 + 100, b: 50 },
480
+                                    type: 'impact'
481
+                                });
482
+                            }
483
+                            
484
+                            console.log(`BOP HIT! Clean bounce. New speed: ${newSpeed.toFixed(1)}, Side: ${side}`);
485
+                        }
393486
                     }
394487
                 }
395488
             }
396489
         }
397490
     });
491
+    
492
+    // Reset collision state on collision end
493
+    Matter.Events.on(engine, 'collisionEnd', function(event) {
494
+        let pairs = event.pairs;
495
+        
496
+        for (let i = 0; i < pairs.length; i++) {
497
+            let pair = pairs[i];
498
+            
499
+            if ((pair.bodyA === ball && (pair.bodyB === leftPaddle || pair.bodyB === rightPaddle)) ||
500
+                (pair.bodyB === ball && (pair.bodyA === leftPaddle || pair.bodyA === rightPaddle))) {
501
+                
502
+                let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
503
+                let isLeftPaddle = paddle === leftPaddle;
504
+                let side = isLeftPaddle ? 'left' : 'right';
505
+                
506
+                collisionState[side].inCollision = false;
507
+            }
508
+        }
509
+    });
398510
 }
js/sprong.jsmodified
@@ -48,6 +48,7 @@ let mouseInput = {
4848
 // Make necessary variables globally accessible for other scripts
4949
 window.inputBuffer = inputBuffer;
5050
 window.moveSupportEnhanced = moveSupportEnhanced;
51
+window.ball = null;
5152
 
5253
 function setup() {
5354
     let canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
@@ -77,6 +78,7 @@ function setup() {
7778
     
7879
     // Create ball
7980
     ball = resetBall(null, world, width, height);
81
+    window.ball = ball;
8082
     
8183
     // Add everything to the world
8284
     World.add(world, [
@@ -101,11 +103,7 @@ function draw() {
101103
         updateParticles();
102104
         checkBallPosition();
103105
         
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
-        }
106
+        
109107
         
110108
         // Draw everything
111109
         drawParticles();
@@ -198,6 +196,7 @@ function checkBallPosition() {
198196
         rightScore++;
199197
         updateScore();
200198
         ball = resetBall(ball, world, width, height);
199
+        window.ball = ball;  // ADD THIS LINE - Update global reference
201200
         gameStarted = false;
202201
     }
203202
     
@@ -205,6 +204,7 @@ function checkBallPosition() {
205204
         leftScore++;
206205
         updateScore();
207206
         ball = resetBall(ball, world, width, height);
207
+        window.ball = ball;  // ADD THIS LINE - Update global reference
208208
         gameStarted = false;
209209
     }
210210
 }
@@ -250,6 +250,7 @@ function keyPressed() {
250250
         rightScore = 0;
251251
         updateScore();
252252
         ball = resetBall(ball, world, width, height);
253
+        window.ball = ball;
253254
         gameStarted = false;
254255
         
255256
         inputBuffer.left = 0;