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
@@ -1,15 +1,21 @@
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 = {
@@ -21,12 +27,13 @@ const AI_SETTINGS = {
2127
         aggression: 0.2,
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: {
3239
         reactionTime: 250,
@@ -36,27 +43,29 @@ const AI_SETTINGS = {
3643
         aggression: 0.5,
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: {
4755
         reactionTime: 150,
4856
         accuracy: 0.95,
49
-        speed: 1.2,
50
-        prediction: 0.8,
51
-        aggression: 0.8,
57
+        speed: 1.3,  // Increased from 1.2
58
+        prediction: 0.85, // Increased from 0.8
59
+        aggression: 0.9,  // Increased from 0.8
5260
         oscillation: 1.0,
5361
         bopChance: 0.85,
54
-        // New windup parameters
5562
         windupSpeed: 0.2,
56
-        windupRadius: 50,
63
+        windupRadius: 60,  // Increased from 50
5764
         comboBopChance: 0.5,
5865
         circularMotion: 0.8,
59
-        phaseSpeed: 0.1
66
+        phaseSpeed: 0.12,  // Increased from 0.1
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;
@@ -41,12 +41,12 @@ const IMPACT_PARTICLES = 8;
4141
 const SPRING_PARTICLE_RATE  = 0.3;
4242
 
4343
 // Bop system constants
44
-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.
44
+const BOP_FORCE             = 0.5;  // self explanatory.      BOP   it.
45
+const BOP_RANGE             = 40;   // also self explanatory. TWIST it.
46
+const BOP_DURATION          = 900;  // traversal duration.    SHAKE it.
4747
 const BOP_COOLDOWN          = 500;  // also also self expl.   PULL  it.
4848
 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
5050
 
5151
 // ============= BOP SYSTEM =============
5252
 let bopState = {
@@ -95,7 +95,6 @@ function handleBopInput(keys, aiEnabled, currentTime, leftPaddle, rightPaddle, l
9595
 
9696
 function activateBop(side, currentTime, paddle, support, engine, particles) {
9797
     const Body = Matter.Body;
98
-    const Engine = Matter.Engine;
9998
     
10099
     bopState[side].active = true;
101100
     bopState[side].startTime = currentTime;
@@ -111,8 +110,8 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
111110
         dx /= magnitude;
112111
         dy /= magnitude;
113112
         
114
-        // Calculate anchor recoil distance
115
-        let anchorRecoilDistance = ANCHOR_RECOIL * 0.4;
113
+        // Calculate anchor recoil distance (reduced for gentler motion)
114
+        let anchorRecoilDistance = ANCHOR_RECOIL * 0.3; // Reduced from 0.4
116115
         
117116
         // Move the support BACKWARD (recoil effect)
118117
         let newSupportX = support.position.x - dx * anchorRecoilDistance;
@@ -126,17 +125,17 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
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
+        // Set paddle velocity directly for immediate forward thrust (gentler)
129
+        let forwardSpeed = BOP_VELOCITY_BOOST * 0.8; // Reduced intensity
131130
         Body.setVelocity(paddle, {
132131
             x: paddle.velocity.x + dx * forwardSpeed,
133132
             y: paddle.velocity.y + dy * forwardSpeed
134133
         });
135134
         
136
-        // Apply a strong forward force for continued acceleration
135
+        // Apply a forward force for continued acceleration (gentler)
137136
         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
137
+            x: dx * bopState[side].power * BOP_RANGE * 0.05, // Reduced from 0.1
138
+            y: dy * bopState[side].power * BOP_RANGE * 0.05
140139
         });
141140
         
142141
         // Create particle burst for visual feedback
@@ -155,9 +154,6 @@ function activateBop(side, currentTime, paddle, support, engine, particles) {
155154
                 type: 'impact'
156155
             });
157156
         }
158
-        
159
-        // Force collision detection update
160
-        Engine.update(engine, 0);
161157
     }
162158
     
163159
     console.log(side + " player BOP!");
@@ -265,6 +261,9 @@ function createSpringPaddleSystem(side, width, height) {
265261
         frictionAir: side === 'left' ? 0.005 : 0.008,
266262
         isSensor: false,
267263
         slop: 0.01,
264
+        // Add rotational physics
265
+        inertia: PADDLE_MASS * 200,  // Lower inertia = more rotation
266
+        frictionAngular: 0.02,       // Slight angular damping
268267
         render: {
269268
             fillStyle: side === 'left' ? '#00ff88' : '#ff6464'
270269
         }
@@ -282,7 +281,10 @@ function createSpringPaddleSystem(side, width, height) {
282281
         bodyB: paddle,
283282
         length: SPRING_LENGTH,
284283
         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 }
286288
     });
287289
     
288290
     return { support, paddle, spring };
@@ -346,10 +348,17 @@ function resetBall(ball, world, width, height) {
346348
     return ball;
347349
 }
348350
 
351
+// ============= COLLISION =============
349352
 // ============= COLLISION =============
350353
 function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles) {
351354
     const Body = Matter.Body;
352355
     
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
+    
353362
     Matter.Events.on(engine, 'collisionStart', function(event) {
354363
         let pairs = event.pairs;
355364
         
@@ -361,38 +370,136 @@ function setupCollisionHandlers(engine, ball, leftPaddle, rightPaddle, particles
361370
                 
362371
                 let paddle = pair.bodyA === ball ? pair.bodyB : pair.bodyA;
363372
                 let isLeftPaddle = paddle === leftPaddle;
373
+                let side = isLeftPaddle ? 'left' : 'right';
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
+                }
364380
                 
381
+                collisionState[side].lastCollisionTime = currentTime;
382
+                
383
+                // Apply bop boost if paddle is currently bopping
365384
                 if ((isLeftPaddle && bopState.left.active) || (!isLeftPaddle && bopState.right.active)) {
366
-                    let ballVel = ball.velocity;
367
-                    let paddleVel = paddle.velocity;
385
+                    // Get actual collision point from Matter.js
386
+                    let collision = pair.collision;
387
+                    let contactPoint = collision.supports[0];
368388
                     
369
-                    let boostX = paddleVel.x * 0.5;
370
-                    let boostY = paddleVel.y * 0.5;
389
+                    // Check if this is a valid collision
390
+                    let isValidCollision = false;
371391
                     
372
-                    Body.setVelocity(ball, {
373
-                        x: ballVel.x * 1.3 + boostX,
374
-                        y: ballVel.y * 1.3 + boostY
375
-                    });
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
+                    }
376402
                     
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;
403
+                    if (isValidCollision) {
404
+                        // During bop, ALWAYS apply boost
405
+                        let ballVel = ball.velocity;
406
+                        let paddleVel = paddle.velocity;
407
+                        
408
+                        // Calculate the normal collision response first
409
+                        let normal = collision.normal;
410
+                        let relativeVelocity = {
411
+                            x: ballVel.x - paddleVel.x,
412
+                            y: ballVel.y - paddleVel.y
413
+                        };
381414
                         
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
-                        });
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
449
+                            Body.setVelocity(ball, {
450
+                                x: newVelX,
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
459
+                            });
460
+                            
461
+                            // Create impact particles
462
+                            for (let j = 0; j < IMPACT_PARTICLES * 2; j++) { // More particles for bop
463
+                                let angle = Math.atan2(normal.y, normal.x) + (Math.random() - 0.5) * Math.PI;
464
+                                let speed = Math.random() * 8 + 4;
465
+                                
466
+                                particles.push({
467
+                                    x: contactPoint ? contactPoint.x : ball.position.x,
468
+                                    y: contactPoint ? contactPoint.y : ball.position.y,
469
+                                    vx: Math.cos(angle) * speed,
470
+                                    vy: Math.sin(angle) * speed,
471
+                                    size: Math.random() * 5 + 3,
472
+                                    life: PARTICLE_LIFE,
473
+                                    maxLife: PARTICLE_LIFE,
474
+                                    color: { r: 255, g: Math.random() * 155 + 100, b: 50 },
475
+                                    type: 'impact'
476
+                                });
477
+                            }
478
+                            
479
+                            console.log(`BOP HIT! Clean bounce. New speed: ${newSpeed.toFixed(1)}, Side: ${side}`);
480
+                        }
393481
                     }
394482
                 }
395483
             }
396484
         }
397485
     });
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;
502
+            }
503
+        }
504
+    });
398505
 }
js/sprong.jsmodified
@@ -104,7 +104,6 @@ function draw() {
104104
         // Enhanced collision detection during bops
105105
         if (bopState.left.active || bopState.right.active) {
106106
             Engine.update(engine, 8);
107
-            Engine.update(engine, 8);
108107
         }
109108
         
110109
         // Draw everything