zeroed-some/sprong / dffea2f

Browse files

refactor

Authored by espadonne
SHA
dffea2f93d130edd24fc471f2c93df4bcbee2909
Parents
77dfb21
Tree
792acb5

6 changed files

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