zeroed-some/sprong / 09335cd

Browse files

tweaks to ai, collision

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
09335cd0db0576b2e51c544829e3f5ef91d7f192
Parents
ea02d88
Tree
578de96

3 changed files

StatusFile+-
M js/ai.js 100 41
M js/game-systems.js 147 40
M js/sprong.js 0 1
js/ai.jsmodified
@@ -2,14 +2,20 @@
2
 
2
 
3
 // ============= AI TECHNIQUE CONSTANTS =============
3
 // ============= AI TECHNIQUE CONSTANTS =============
4
 const AI_WINDUP_SPEED       = 0.15;   // Base oscillation speed
4
 const AI_WINDUP_SPEED       = 0.15;   // Base oscillation speed
5
-const AI_WINDUP_SMOOTHNESS = 0.92;   // Smooth transitions
5
+const AI_WINDUP_SMOOTHNESS  = 1.00;   // Smooth transitions
6
-const AI_WINDUP_RADIUS = 40;         // Circular motion radius
6
+const AI_WINDUP_RADIUS      = 240;    // Circular motion radius
7
 const AI_WINDUP_MIN_TIME    = 300;    // Minimum windup duration
7
 const AI_WINDUP_MIN_TIME    = 300;    // Minimum windup duration
8
-const AI_WINDUP_MAX_TIME = 600;      // Maximum 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
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
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
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)
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
+
13
 
19
 
14
 // ============= AI SETTINGS =============
20
 // ============= AI SETTINGS =============
15
 const AI_SETTINGS = {
21
 const AI_SETTINGS = {
@@ -21,12 +27,13 @@ const AI_SETTINGS = {
21
         aggression: 0.2,
27
         aggression: 0.2,
22
         oscillation: 0.3,
28
         oscillation: 0.3,
23
         bopChance: 0.25,
29
         bopChance: 0.25,
24
-        // New windup parameters
25
         windupSpeed: 0.1,
30
         windupSpeed: 0.1,
26
         windupRadius: 30,
31
         windupRadius: 30,
27
         comboBopChance: 0.1,
32
         comboBopChance: 0.1,
28
         circularMotion: 0.4,
33
         circularMotion: 0.4,
29
-        phaseSpeed: 0.06
34
+        phaseSpeed: 0.06,
35
+        idleMovement: 0.3,
36
+        trackingAggression: 0.1
30
     },
37
     },
31
     medium: {
38
     medium: {
32
         reactionTime: 250,
39
         reactionTime: 250,
@@ -36,27 +43,29 @@ const AI_SETTINGS = {
36
         aggression: 0.5,
43
         aggression: 0.5,
37
         oscillation: 0.7,
44
         oscillation: 0.7,
38
         bopChance: 0.55,
45
         bopChance: 0.55,
39
-        // New windup parameters
40
         windupSpeed: 0.15,
46
         windupSpeed: 0.15,
41
         windupRadius: 40,
47
         windupRadius: 40,
42
         comboBopChance: 0.3,
48
         comboBopChance: 0.3,
43
         circularMotion: 0.6,
49
         circularMotion: 0.6,
44
-        phaseSpeed: 0.08
50
+        phaseSpeed: 0.08,
51
+        idleMovement: 0.5,
52
+        trackingAggression: 0.15
45
     },
53
     },
46
     hard: {
54
     hard: {
47
         reactionTime: 150,
55
         reactionTime: 150,
48
         accuracy: 0.95,
56
         accuracy: 0.95,
49
-        speed: 1.2,
57
+        speed: 1.3,  // Increased from 1.2
50
-        prediction: 0.8,
58
+        prediction: 0.85, // Increased from 0.8
51
-        aggression: 0.8,
59
+        aggression: 0.9,  // Increased from 0.8
52
         oscillation: 1.0,
60
         oscillation: 1.0,
53
         bopChance: 0.85,
61
         bopChance: 0.85,
54
-        // New windup parameters
55
         windupSpeed: 0.2,
62
         windupSpeed: 0.2,
56
-        windupRadius: 50,
63
+        windupRadius: 60,  // Increased from 50
57
         comboBopChance: 0.5,
64
         comboBopChance: 0.5,
58
         circularMotion: 0.8,
65
         circularMotion: 0.8,
59
-        phaseSpeed: 0.1
66
+        phaseSpeed: 0.12,  // Increased from 0.1
67
+        idleMovement: 0.8,
68
+        trackingAggression: 0.25
60
     }
69
     }
61
 };
70
 };
62
 
71
 
@@ -148,19 +157,32 @@ function handleAI(currentTime, ball, rightPaddle, rightSupport,
148
 
157
 
149
 // ============= AI BEHAVIORS =============
158
 // ============= AI BEHAVIORS =============
150
 function updateAILifelikeBehavior(currentTime, height) {
159
 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;
152
     
164
     
153
-    if (currentTime - aiState.lastMicroTime > 2000 + Math.random() * 1000) {
165
+    // More frequent micro-adjustments
154
-        aiState.microAdjustment = (Math.random() - 0.5) * 15;
166
+    if (currentTime - aiState.lastMicroTime > 1000 + Math.random() * 500) {
167
+        aiState.microAdjustment = (Math.random() - 0.5) * 30 * aiSettings.idleMovement;
155
         aiState.lastMicroTime = currentTime;
168
         aiState.lastMicroTime = currentTime;
156
     }
169
     }
157
     
170
     
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;
159
     
177
     
160
     if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
178
     if (aiState.mode === 'ANTICIPATING' || aiState.mode === 'RECOVERING') {
161
         let centerY = height / 2;
179
         let centerY = height / 2;
162
-        let wanderRadius = 25;
180
+        let wanderRadius = 40 * aiSettings.idleMovement; // Larger wander radius
163
-        aiState.idleTarget = centerY + Math.sin(currentTime * 0.002) * wanderRadius;
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;
164
     }
186
     }
165
 }
187
 }
166
 
188
 
@@ -187,15 +209,33 @@ function handleAITracking(currentTime, ballPos, ballVel, aiSettings,
187
     let paddlePos = rightPaddle.position;
209
     let paddlePos = rightPaddle.position;
188
     let anchorPos = rightSupport.position;
210
     let anchorPos = rightSupport.position;
189
     
211
     
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
220
+    
221
+    // Blend current and future position based on difficulty
222
+    let targetBallY = lerp(ballPos.y, futureBallY, aiSettings.prediction);
191
     
223
     
192
-    let desiredPaddleY = ballPos.y + aiState.microAdjustment;
224
+    let desiredPaddleY = targetBallY + aiState.microAdjustment + aiState.breathingOffset;
193
     desiredPaddleY = Math.max(80, Math.min(height - 80, desiredPaddleY));
225
     desiredPaddleY = Math.max(80, Math.min(height - 80, desiredPaddleY));
194
     
226
     
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
+    
195
     let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
233
     let anchorOffsetNeeded = calculateAnchorOffset(desiredPaddleY, paddlePos, anchorPos);
196
     let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
234
     let targetAnchorY = desiredPaddleY + anchorOffsetNeeded;
197
     
235
     
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
+    
199
     
239
     
200
     // AI Bop decision logic
240
     // AI Bop decision logic
201
     if (ballApproaching && !aiState.consideringBop && !bopState.right.active) {
241
     if (ballApproaching && !aiState.consideringBop && !bopState.right.active) {
@@ -461,44 +501,63 @@ function executeAIMovement(aiSettings, rightSupport) {
461
     let currentY = rightSupport.position.y;
501
     let currentY = rightSupport.position.y;
462
     let deltaY = aiState.targetY - currentY;
502
     let deltaY = aiState.targetY - currentY;
463
     
503
     
464
-    if (Math.abs(deltaY) > 1) {
504
+    // Lower threshold for more constant movement
465
-        let baseSpeed = 0.12 * aiSettings.speed;
505
+    if (Math.abs(deltaY) > 0.5) { // Reduced from 1
506
+        let baseSpeed = 0.15 * aiSettings.speed; // Increased from 0.12
466
         
507
         
467
         // Apply swing power during swing phase
508
         // Apply swing power during swing phase
468
         if (aiState.mode === 'SWINGING') {
509
         if (aiState.mode === 'SWINGING') {
469
-            baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.3);
510
+            baseSpeed *= aiState.swingPower * (1 + aiState.aggressionLevel * 0.5); // Increased multiplier
470
             
511
             
471
             // Add momentum from windup if available
512
             // Add momentum from windup if available
472
             if (aiState.targetVelocity > 0) {
513
             if (aiState.targetVelocity > 0) {
473
-                baseSpeed *= (1 + aiState.targetVelocity * 0.1);
514
+                baseSpeed *= (1 + aiState.targetVelocity * 0.15); // Increased from 0.1
474
-                aiState.targetVelocity *= 0.9; // Decay momentum
515
+                aiState.targetVelocity *= 0.85; // Slower decay
475
             }
516
             }
476
         } else if (aiState.mode === 'WINDING_UP') {
517
         } else if (aiState.mode === 'WINDING_UP') {
477
             // Enhanced windup speed based on phase and settings
518
             // Enhanced windup speed based on phase and settings
478
             let windupSpeedMultiplier = aiSettings.windupSpeed / AI_WINDUP_SPEED;
519
             let windupSpeedMultiplier = aiSettings.windupSpeed / AI_WINDUP_SPEED;
479
-            baseSpeed *= (1.5 + windupSpeedMultiplier);
520
+            baseSpeed *= (2.0 + windupSpeedMultiplier); // Increased from 1.5
480
             
521
             
481
             // Add extra speed at peak velocity points
522
             // Add extra speed at peak velocity points
482
             if (aiState.peakReached) {
523
             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;
484
             }
532
             }
485
         }
533
         }
486
         
534
         
487
-        // Apply aggression multiplier
535
+        // Apply aggression multiplier with higher impact
488
-        baseSpeed *= (1 + aiState.aggressionLevel * 0.3);
536
+        baseSpeed *= (1 + aiState.aggressionLevel * 0.5); // Increased from 0.3
489
         
537
         
490
         let movement = deltaY * baseSpeed;
538
         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));
492
         
543
         
493
         // Import moveSupportEnhanced from game-systems
544
         // Import moveSupportEnhanced from game-systems
494
         const moveSupportEnhanced = window.moveSupportEnhanced;
545
         const moveSupportEnhanced = window.moveSupportEnhanced;
495
         moveSupportEnhanced(rightSupport, movement, window.height);
546
         moveSupportEnhanced(rightSupport, movement, window.height);
496
         
547
         
497
         // Update input buffer for visual effects
548
         // Update input buffer for visual effects
498
-        window.inputBuffer.right = movement / (SUPPORT_SPEED * 1.1);
549
+        window.inputBuffer.right = movement / maxSpeed;
550
+    } else {
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;
499
         } else {
557
         } else {
500
             // Gradually reduce input buffer when AI is not moving
558
             // Gradually reduce input buffer when AI is not moving
501
-        window.inputBuffer.right *= 0.95;
559
+            window.inputBuffer.right *= 0.9; // Slower decay for more visible movement
560
+        }
502
     }
561
     }
503
 }
562
 }
504
 
563
 
js/game-systems.jsmodified
@@ -7,7 +7,7 @@ const CANVAS_WIDTH = 800;
7
 const CANVAS_HEIGHT = 400;
7
 const CANVAS_HEIGHT = 400;
8
 
8
 
9
 // Game constants
9
 // Game constants
10
-const BALL_SPEED    = 6;
10
+const BALL_SPEED    = 5;
11
 const BALL_RADIUS   = 12;
11
 const BALL_RADIUS   = 12;
12
 const PADDLE_WIDTH  = 20;
12
 const PADDLE_WIDTH  = 20;
13
 const PADDLE_HEIGHT = 80;
13
 const PADDLE_HEIGHT = 80;
@@ -41,12 +41,12 @@ const IMPACT_PARTICLES = 8;
41
 const SPRING_PARTICLE_RATE  = 0.3;
41
 const SPRING_PARTICLE_RATE  = 0.3;
42
 
42
 
43
 // Bop system constants
43
 // Bop system constants
44
-const BOP_FORCE             = 1.0;  // self explanatory.      BOP   it.
44
+const BOP_FORCE             = 0.5;  // self explanatory.      BOP   it.
45
-const BOP_RANGE             = 500;  // also self explanatory. TWIST it.
45
+const BOP_RANGE             = 40;   // also self explanatory. TWIST it.
46
-const BOP_DURATION          = 300;  // traversal duration.    SHAKE it.
46
+const BOP_DURATION          = 900;  // traversal duration.    SHAKE it.
47
 const BOP_COOLDOWN          = 500;  // also also self expl.   PULL  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
48
 const ANCHOR_RECOIL         = 40;   // How far the anchor moves backward during bop
49
-const BOP_VELOCITY_BOOST    = 12;   // Initial velocity boost for paddle
49
+const BOP_VELOCITY_BOOST    = 6;    // Initial velocity boost for paddle
50
 
50
 
51
 // ============= BOP SYSTEM =============
51
 // ============= BOP SYSTEM =============
52
 let bopState = {
52
 let bopState = {
@@ -95,7 +95,6 @@ function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, l
95
 
95
 
96
 function activateBop(side, currentTime, paddle, support, engine, particles) {
96
 function activateBop(side, currentTime, paddle, support, engine, particles) {
97
     const Body = Matter.Body;
97
     const Body = Matter.Body;
98
-    const Engine = Matter.Engine;
99
     
98
     
100
     bopState[side].active = true;
99
     bopState[side].active = true;
101
     bopState[side].startTime = currentTime;
100
     bopState[side].startTime = currentTime;
@@ -111,8 +110,8 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
111
         dx /= magnitude;
110
         dx /= magnitude;
112
         dy /= magnitude;
111
         dy /= magnitude;
113
         
112
         
114
-        // Calculate anchor recoil distance
113
+        // Calculate anchor recoil distance (reduced for gentler motion)
115
-        let anchorRecoilDistance = ANCHOR_RECOIL * 0.4;
114
+        let anchorRecoilDistance = ANCHOR_RECOIL * 0.3; // Reduced from 0.4
116
         
115
         
117
         // Move the support BACKWARD (recoil effect)
116
         // Move the support BACKWARD (recoil effect)
118
         let newSupportX = support.position.x - dx * anchorRecoilDistance;
117
         let newSupportX = support.position.x - dx * anchorRecoilDistance;
@@ -126,17 +125,17 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
126
             y: support.position.y + dy * anchorRecoilDistance 
125
             y: support.position.y + dy * anchorRecoilDistance 
127
         };
126
         };
128
         
127
         
129
-        // Set paddle velocity directly for immediate forward thrust
128
+        // Set paddle velocity directly for immediate forward thrust (gentler)
130
-        let forwardSpeed = (BOP_RANGE / SPRING_LENGTH) * BOP_VELOCITY_BOOST;
129
+        let forwardSpeed = BOP_VELOCITY_BOOST * 0.8; // Reduced intensity
131
         Body.setVelocity(paddle, {
130
         Body.setVelocity(paddle, {
132
             x: paddle.velocity.x + dx * forwardSpeed,
131
             x: paddle.velocity.x + dx * forwardSpeed,
133
             y: paddle.velocity.y + dy * forwardSpeed
132
             y: paddle.velocity.y + dy * forwardSpeed
134
         });
133
         });
135
         
134
         
136
-        // Apply a strong forward force for continued acceleration
135
+        // Apply a forward force for continued acceleration (gentler)
137
         Body.applyForce(paddle, paddle.position, {
136
         Body.applyForce(paddle, paddle.position, {
138
-            x: dx * bopState[side].power * BOP_RANGE * 0.1,
137
+            x: dx * bopState[side].power * BOP_RANGE * 0.05, // Reduced from 0.1
139
-            y: dy * bopState[side].power * BOP_RANGE * 0.1
138
+            y: dy * bopState[side].power * BOP_RANGE * 0.05
140
         });
139
         });
141
         
140
         
142
         // Create particle burst for visual feedback
141
         // Create particle burst for visual feedback
@@ -155,9 +154,6 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
155
                 type: 'impact'
154
                 type: 'impact'
156
             });
155
             });
157
         }
156
         }
158
-        
159
-        // Force collision detection update
160
-        Engine.update(engine, 0);
161
     }
157
     }
162
     
158
     
163
     console.log(side + " player BOP!");
159
     console.log(side + " player BOP!");
@@ -265,6 +261,9 @@ function createSpringPaddleSystem(side, width, height) {
265
         frictionAir: side === 'left' ? 0.005 : 0.008,
261
         frictionAir: side === 'left' ? 0.005 : 0.008,
266
         isSensor: false,
262
         isSensor: false,
267
         slop: 0.01,
263
         slop: 0.01,
264
+        // Add rotational physics
265
+        inertia: PADDLE_MASS * 200,  // Lower inertia = more rotation
266
+        frictionAngular: 0.02,       // Slight angular damping
268
         render: {
267
         render: {
269
             fillStyle: side === 'left' ? '#00ff88' : '#ff6464'
268
             fillStyle: side === 'left' ? '#00ff88' : '#ff6464'
270
         }
269
         }
@@ -282,7 +281,10 @@ function createSpringPaddleSystem(side, width, height) {
282
         bodyB: paddle,
281
         bodyB: paddle,
283
         length: SPRING_LENGTH,
282
         length: SPRING_LENGTH,
284
         stiffness: SPRING_STIFFNESS,
283
         stiffness: SPRING_STIFFNESS,
285
-        damping: SPRING_DAMPING
284
+        damping: SPRING_DAMPING,
285
+        // Add angular stiffness to create torque from movement
286
+        angularStiffness: 0.01,  // Allows paddle to rotate based on spring tension
287
+        render: { visible: false }
286
     });
288
     });
287
     
289
     
288
     return { support, paddle, spring };
290
     return { support, paddle, spring };
@@ -346,10 +348,17 @@ function resetBall(ball, world, width, height) {
346
     return ball;
348
     return ball;
347
 }
349
 }
348
 
350
 
351
+// ============= COLLISION =============
349
 // ============= COLLISION =============
352
 // ============= COLLISION =============
350
 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
353
 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
351
     const Body = Matter.Body;
354
     const Body = Matter.Body;
352
     
355
     
356
+    // Track collision state to prevent double-triggering
357
+    let collisionState = {
358
+        left: { inCollision: false, lastCollisionTime: 0 },
359
+        right: { inCollision: false, lastCollisionTime: 0 }
360
+    };
361
+    
353
     Matter.Events.on(engine, 'collisionStart', function(event) {
362
     Matter.Events.on(engine, 'collisionStart', function(event) {
354
         let pairs = event.pairs;
363
         let pairs = event.pairs;
355
         
364
         
@@ -361,37 +370,135 @@ function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles
361
                 
370
                 
362
                 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
371
                 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
363
                 let isLeftPaddle = paddle === leftPaddle;
372
                 let isLeftPaddle = paddle === leftPaddle;
373
+                let side = isLeftPaddle ? 'left' : 'right';
364
                 
374
                 
375
+                // Prevent multiple collisions in quick succession
376
+                let currentTime = Date.now();
377
+                if (currentTime - collisionState[side].lastCollisionTime < 100) {
378
+                    continue; // Skip if we just had a collision
379
+                }
380
+                
381
+                collisionState[side].lastCollisionTime = currentTime;
382
+                
383
+                // Apply bop boost if paddle is currently bopping
365
                 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
384
                 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
385
+                    // Get actual collision point from Matter.js
386
+                    let collision = pair.collision;
387
+                    let contactPoint = collision.supports[0];
388
+                    
389
+                    // Check if this is a valid collision
390
+                    let isValidCollision = false;
391
+                    
392
+                    if (contactPoint) {
393
+                        isValidCollision = true;
394
+                    } else {
395
+                        // Fallback distance check
396
+                        let dx = ball.position.x - paddle.position.x;
397
+                        let dy = ball.position.y - paddle.position.y;
398
+                        let distance = Math.sqrt(dx * dx + dy * dy);
399
+                        let collisionThreshold = BALL_RADIUS + Math.max(PADDLE_WIDTH, PADDLE_HEIGHT)/2 + 10;
400
+                        isValidCollision = distance < collisionThreshold;
401
+                    }
402
+                    
403
+                    if (isValidCollision) {
404
+                        // During bop, ALWAYS apply boost
366
                         let ballVel = ball.velocity;
405
                         let ballVel = ball.velocity;
367
                         let paddleVel = paddle.velocity;
406
                         let paddleVel = paddle.velocity;
368
                         
407
                         
369
-                    let boostX = paddleVel.x * 0.5;
408
+                        // Calculate the normal collision response first
370
-                    let boostY = paddleVel.y * 0.5;
409
+                        let normal = collision.normal;
410
+                        let relativeVelocity = {
411
+                            x: ballVel.x - paddleVel.x,
412
+                            y: ballVel.y - paddleVel.y
413
+                        };
371
                         
414
                         
415
+                        // Calculate the velocity along the collision normal
416
+                        let velocityAlongNormal = relativeVelocity.x * normal.x + relativeVelocity.y * normal.y;
417
+                        
418
+                        // Only apply boost if ball is approaching paddle
419
+                        if (velocityAlongNormal < 0) {
420
+                            // Base reflection velocity (enhanced during bop)
421
+                            let restitution = 1.5; // Higher restitution during bop
422
+                            let impulse = 2 * velocityAlongNormal * restitution;
423
+                            
424
+                            // Apply the impulse
425
+                            let newVelX = ballVel.x - impulse * normal.x;
426
+                            let newVelY = ballVel.y - impulse * normal.y;
427
+                            
428
+                            // Add paddle velocity influence (more during bop)
429
+                            let paddleInfluence = 0.6; // Higher influence during bop
430
+                            newVelX += paddleVel.x * paddleInfluence;
431
+                            newVelY += paddleVel.y * paddleInfluence;
432
+                            
433
+                            // Add bop boost in the direction of the normal
434
+                            let bopBoostMagnitude = 3; // Extra boost during bop
435
+                            newVelX += normal.x * bopBoostMagnitude;
436
+                            newVelY += normal.y * bopBoostMagnitude;
437
+                            
438
+                            // Ensure minimum speed after bop
439
+                            let newSpeed = Math.sqrt(newVelX * newVelX + newVelY * newVelY);
440
+                            let minSpeed = BALL_SPEED * 1.3; // At least 30% faster than normal
441
+                            
442
+                            if (newSpeed < minSpeed) {
443
+                                let scale = minSpeed / newSpeed;
444
+                                newVelX *= scale;
445
+                                newVelY *= scale;
446
+                            }
447
+                            
448
+                            // Apply the new velocity
372
                             Body.setVelocity(ball, {
449
                             Body.setVelocity(ball, {
373
-                        x: ballVel.x * 1.3 + boostX,
450
+                                x: newVelX,
374
-                        y: ballVel.y * 1.3 + boostY
451
+                                y: newVelY
452
+                            });
453
+                            
454
+                            // Move ball slightly away from paddle to prevent sticking
455
+                            let separation = 2; // pixels
456
+                            Body.setPosition(ball, {
457
+                                x: ball.position.x + normal.x * separation,
458
+                                y: ball.position.y + normal.y * separation
375
                             });
459
                             });
376
                             
460
                             
377
                             // Create impact particles
461
                             // Create impact particles
378
-                    for (let j = 0; j < IMPACT_PARTICLES; j++) {
462
+                            for (let j = 0; j < IMPACT_PARTICLES * 2; j++) { // More particles for bop
379
-                        let angle = Math.random() * Math.PI * 2;
463
+                                let angle = Math.atan2(normal.y, normal.x) + (Math.random() - 0.5) * Math.PI;
380
-                        let speed = Math.random() * 6 + 2;
464
+                                let speed = Math.random() * 8 + 4;
381
                                 
465
                                 
382
                                 particles.push({
466
                                 particles.push({
383
-                            x: ball.position.x,
467
+                                    x: contactPoint ? contactPoint.x : ball.position.x,
384
-                            y: ball.position.y,
468
+                                    y: contactPoint ? contactPoint.y : ball.position.y,
385
-                            vx: Math.cos(angle) * speed - ballVel.x * 0.2,
469
+                                    vx: Math.cos(angle) * speed,
386
-                            vy: Math.sin(angle) * speed - ballVel.y * 0.2,
470
+                                    vy: Math.sin(angle) * speed,
387
-                            size: Math.random() * 4 + 2,
471
+                                    size: Math.random() * 5 + 3,
388
                                     life: PARTICLE_LIFE,
472
                                     life: PARTICLE_LIFE,
389
                                     maxLife: PARTICLE_LIFE,
473
                                     maxLife: PARTICLE_LIFE,
390
-                            color: { r: 255, g: Math.random() * 155 + 100, b: Math.random() * 50 + 100 },
474
+                                    color: { r: 255, g: Math.random() * 155 + 100, b: 50 },
391
                                     type: 'impact'
475
                                     type: 'impact'
392
                                 });
476
                                 });
393
                             }
477
                             }
478
+                            
479
+                            console.log(`BOP HIT! Clean bounce. New speed: ${newSpeed.toFixed(1)}, Side: ${side}`);
480
+                        }
481
+                    }
482
+                }
483
+            }
394
         }
484
         }
485
+    });
486
+    
487
+    // Reset collision state on collision end
488
+    Matter.Events.on(engine, 'collisionEnd', function(event) {
489
+        let pairs = event.pairs;
490
+        
491
+        for (let i = 0; i < pairs.length; i++) {
492
+            let pair = pairs[i];
493
+            
494
+            if ((pair.bodyA === ball && (pair.bodyB === leftPaddle || pair.bodyB === rightPaddle)) ||
495
+                (pair.bodyB === ball && (pair.bodyA === leftPaddle || pair.bodyA === rightPaddle))) {
496
+                
497
+                let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
498
+                let isLeftPaddle = paddle === leftPaddle;
499
+                let side = isLeftPaddle ? 'left' : 'right';
500
+                
501
+                collisionState[side].inCollision = false;
395
             }
502
             }
396
         }
503
         }
397
     });
504
     });
js/sprong.jsmodified
@@ -104,7 +104,6 @@ function draw() {
104
         // Enhanced collision detection during bops
104
         // Enhanced collision detection during bops
105
         if (bopState.left.active || bopState.right.active) {
105
         if (bopState.left.active || bopState.right.active) {
106
             Engine.update(engine, 8);
106
             Engine.update(engine, 8);
107
-            Engine.update(engine, 8);
108
         }
107
         }
109
         
108
         
110
         // Draw everything
109
         // Draw everything