File size: 20,000 Bytes
0d3cc49
a3b97ad
 
 
 
0d3cc49
a4b6ccc
 
0d3cc49
 
a3b97ad
adb0008
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb0008
 
0d3cc49
 
 
 
 
 
 
adb0008
0d3cc49
adb0008
 
0d3cc49
 
 
 
 
 
 
5994a14
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
adb0008
5994a14
0d3cc49
 
 
 
 
 
 
 
 
 
 
a4b6ccc
 
 
0d3cc49
 
a4b6ccc
5994a14
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb0008
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5994a14
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3b97ad
0d3cc49
 
 
 
adb0008
 
0d3cc49
 
adb0008
 
 
484355f
 
adb0008
0d3cc49
484355f
0d3cc49
adb0008
 
 
 
 
 
 
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb0008
0d3cc49
adb0008
 
0d3cc49
adb0008
0d3cc49
 
 
 
adb0008
0d3cc49
adb0008
0d3cc49
 
 
 
 
 
 
 
adb0008
0d3cc49
 
adb0008
0d3cc49
adb0008
 
0d3cc49
 
 
 
 
adb0008
0d3cc49
 
adb0008
0d3cc49
 
 
 
 
 
 
25cc5de
0d3cc49
8d12a80
 
adb0008
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adb0008
a3b97ad
a4b6ccc
0d3cc49
a4b6ccc
0d3cc49
 
 
a4b6ccc
0d3cc49
adb0008
0d3cc49
 
 
 
 
 
 
 
 
 
 
a4b6ccc
a3b97ad
0d3cc49
 
 
 
 
 
adb0008
a3b97ad
0d3cc49
 
 
ae20685
0d3cc49
 
a4b6ccc
 
0d3cc49
 
 
a4b6ccc
 
0d3cc49
 
a4b6ccc
a3b97ad
0d3cc49
 
fc7647d
0d3cc49
 
 
 
adb0008
0d3cc49
 
a3b97ad
0d3cc49
 
 
a3b97ad
 
 
adb0008
 
0d3cc49
 
 
 
 
 
a4b6ccc
0d3cc49
 
 
 
a4b6ccc
0d3cc49
 
ae20685
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
 
 
adb0008
0d3cc49
adb0008
 
0d3cc49
adb0008
0d3cc49
a3b97ad
 
0d3cc49
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
// --- Get Elements ---
const gameArea = document.getElementById('gameArea');
const paddleLeft = document.getElementById('paddleLeft');
const paddleRight = document.getElementById('paddleRight');
const ball = document.getElementById('ball');
const powerUpElement = document.getElementById('powerUp');
const playerScoreDisplay = document.getElementById('playerScore');
const botScoreDisplay = document.getElementById('botScore');
const chargeIndicator = document.getElementById('chargeIndicator');
const chargeIndicatorFill = document.getElementById('chargeIndicatorFill');

// --- Constants ---
const BASE_PADDLE_HEIGHT = 80;
const PADDLE_WIDTH = 10;
const BALL_SIZE = 15;
const MAX_ANGLE_DEG = 60; // Increased max angle for more variation
const MAX_ANGLE_RAD = MAX_ANGLE_DEG * (Math.PI / 180);
const INITIAL_BALL_SPEED = 4;
const SPEED_INCREASE_FACTOR = 1.08; // Slightly lower increase
const MAX_BALL_SPEED = 18;
const BOT_PADDLE_SPEED = 5; // Base speed
const BOT_PREDICTION_FACTOR = 0.85; // 0 = No prediction, 1 = Perfect prediction (adjust for difficulty)
const SPIN_FACTOR = 0.08; // How much paddle speed translates to spin
const MAX_SPIN = 1.5;     // Max spin value
const SPIN_DRAG = 0.995;  // Spin decreases slightly over time
const MAGNUS_EFFECT_STRENGTH = 0.003; // How much spin affects trajectory
const CHARGE_RATE = 150; // Lower value = faster charge (ms per 100% charge)
const MAX_CHARGE_BOOST_FACTOR = 2.0; // Max speed multiplier for fully charged shot
const POWERUP_SPAWN_CHANCE = 0.002; // Chance per frame to spawn
const POWERUP_DURATION = 7000; // ms
const PADDLE_SIZE_INCREASE = 40; // Pixels to add to height

// --- Game State Variables ---
let paddleLeftY = window.innerHeight / 2 - BASE_PADDLE_HEIGHT / 2;
let paddleRightY = window.innerHeight / 2 - BASE_PADDLE_HEIGHT / 2;
let paddleLeftHeight = BASE_PADDLE_HEIGHT;
let paddleRightHeight = BASE_PADDLE_HEIGHT;
let ballX = window.innerWidth / 2 - BALL_SIZE / 2;
let ballY = window.innerHeight / 2 - BALL_SIZE / 2;
let ballSpeedX = INITIAL_BALL_SPEED;
let ballSpeedY = 0;
let ballSpin = 0; // Positive = Clockwise (Topspin from left player), Negative = Counter-Clockwise (Backspin from left player)
let playerScore = 0;
let botScore = 0;
let isGamePaused = true;
let lastPlayerPaddleY = paddleLeftY; // For calculating spin
let isCharging = false;
let chargeStartTime = 0;
let currentChargeBoost = 1.0; // Speed multiplier from charge
let activePowerUp = null; // { type: 'PaddleSizeUp', side: 'left'/'right'/'both', timeoutId: null }
let powerUpData = { x: 0, y: 0, active: false };

// --- Initial Setup ---
function initializeGame() {
    paddleLeft.style.height = paddleLeftHeight + 'px';
    paddleRight.style.height = paddleRightHeight + 'px';
    paddleLeft.style.top = paddleLeftY + 'px';
    paddleRight.style.top = paddleRightY + 'px';
    ball.style.left = ballX + 'px';
    ball.style.top = ballY + 'px';
    playerScoreDisplay.textContent = playerScore;
    botScoreDisplay.textContent = botScore;
    // Place paddles correctly at start/resize
    paddleRight.style.right = '5px'; // Ensure right paddle is positioned correctly
    paddleLeft.style.left = '5px';
}


// --- Input Handling (Touch & Mouse for Player) ---
let interactionStartY = null; // Track touch start position

function handleInteractionStart(clientY, clientX) {
    if (clientX < window.innerWidth / 2) { // Only charge if interacting on the left side
        isCharging = true;
        chargeStartTime = performance.now();
        chargeIndicator.style.display = 'block';
        updateChargeIndicator(); // Update visual immediately

        interactionStartY = clientY; // Store initial Y for potential movement during charge

        if (isGamePaused) {
            isGamePaused = false;
            // Determine starting direction if needed (optional)
            // ballSpeedX = Math.abs(ballSpeedX) || INITIAL_BALL_SPEED;
        }
    }
}

function handleInteractionMove(clientY, clientX) {
    if (clientX < window.innerWidth / 2) { // Only control left paddle
        // Move paddle based on current position, not start position
        paddleLeftY = clientY - paddleLeftHeight / 2;
        paddleLeftY = Math.max(0, Math.min(paddleLeftY, window.innerHeight - paddleLeftHeight));
        paddleLeft.style.top = paddleLeftY + 'px';
    }
}

function handleInteractionEnd() {
    if (isCharging) {
        isCharging = false;
        const chargeDuration = performance.now() - chargeStartTime;
        const chargeLevel = Math.min(chargeDuration / CHARGE_RATE, 1.0); // Cap at 100%
        currentChargeBoost = 1.0 + chargeLevel * (MAX_CHARGE_BOOST_FACTOR - 1.0);
        // console.log("Charge Released! Boost:", currentChargeBoost.toFixed(2)); // Debug
        chargeIndicator.style.display = 'none';
        chargeIndicatorFill.style.width = '0%';
        // Note: Boost is applied in handlePaddleCollision if released just before hit
    }
    interactionStartY = null;
}

gameArea.addEventListener('touchstart', (e) => {
    e.preventDefault();
    const touch = e.touches[0];
    handleInteractionStart(touch.clientY, touch.clientX);
}, { passive: false });

gameArea.addEventListener('touchmove', (e) => {
    e.preventDefault();
    const touch = e.touches[0];
    handleInteractionMove(touch.clientY, touch.clientX);
}, { passive: false });

gameArea.addEventListener('touchend', (e) => {
    handleInteractionEnd();
});
gameArea.addEventListener('touchcancel', (e) => {
    handleInteractionEnd(); // Treat cancel like end
});

// Mouse fallback for Desktop
gameArea.addEventListener('mousedown', (e) => handleInteractionStart(e.clientY, e.clientX));
gameArea.addEventListener('mousemove', (e) => {
     // Only move paddle if mouse button is potentially down OR touch isn't active
     // A better approach might track mouseDown state explicitly
     if (interactionStartY !== null || e.buttons === 1) { // Move if charging or mouse down
        handleInteractionMove(e.clientY, e.clientX);
     }
});
gameArea.addEventListener('mouseup', (e) => handleInteractionEnd());
gameArea.addEventListener('mouseleave', (e) => handleInteractionEnd()); // Stop charging if mouse leaves


function updateChargeIndicator() {
     if (isCharging) {
         const chargeDuration = performance.now() - chargeStartTime;
         const chargeLevel = Math.min(chargeDuration / CHARGE_RATE, 1.0);
         chargeIndicatorFill.style.width = `${chargeLevel * 100}%`;
         requestAnimationFrame(updateChargeIndicator); // Keep updating while charging
     }
}

// --- Power-Up Logic ---
function spawnPowerUp() {
    if (!powerUpData.active && Math.random() < POWERUP_SPAWN_CHANCE) {
        // console.log("Spawning PowerUp!"); // Debug
        powerUpData.x = Math.random() * (window.innerWidth * 0.6) + window.innerWidth * 0.2; // Spawn near middle horizontally
        powerUpData.y = Math.random() * (window.innerHeight * 0.8) + window.innerHeight * 0.1; // Avoid edges vertically
        powerUpData.active = true;
        powerUpElement.style.left = powerUpData.x + 'px';
        powerUpElement.style.top = powerUpData.y + 'px';
        powerUpElement.style.display = 'block';
        // playSound('powerUpSpawn');
    }
}

function checkPowerUpCollision() {
    if (!powerUpData.active) return;

    const ballRect = ball.getBoundingClientRect();
    const powerUpRect = powerUpElement.getBoundingClientRect();

    if (ballRect.left < powerUpRect.right &&
        ballRect.right > powerUpRect.left &&
        ballRect.top < powerUpRect.bottom &&
        ballRect.bottom > powerUpRect.top)
    {
        // console.log("PowerUp Hit!"); // Debug
        powerUpData.active = false;
        powerUpElement.style.display = 'none';
        activatePowerUp('PaddleSizeUp'); // Hardcoded type for now
        // playSound('powerUpCollect');
    }
}

function activatePowerUp(type) {
    clearTimeout(activePowerUp?.timeoutId); // Clear existing timer if any

    const side = (ballSpeedX > 0) ? 'left' : 'right'; // Give power-up to the player who last hit
    // console.log(`Activating ${type} for ${side}`); // Debug

    if (type === 'PaddleSizeUp') {
        if (side === 'left' || side === 'both') {
            paddleLeftHeight = BASE_PADDLE_HEIGHT + PADDLE_SIZE_INCREASE;
            paddleLeft.style.height = paddleLeftHeight + 'px';
        }
        if (side === 'right' || side === 'both') {
            paddleRightHeight = BASE_PADDLE_HEIGHT + PADDLE_SIZE_INCREASE;
            paddleRight.style.height = paddleRightHeight + 'px';
        }
        // Set timer to deactivate
        const timeoutId = setTimeout(() => deactivatePowerUp(type, side), POWERUP_DURATION);
        activePowerUp = { type, side, timeoutId };
    }
    // Add other power-up types here (MultiBall, SpeedChange, etc.)
}

function deactivatePowerUp(type, side) {
    // console.log(`Deactivating ${type} for ${side}`); // Debug
    if (type === 'PaddleSizeUp') {
        if (side === 'left' || side === 'both') {
            paddleLeftHeight = BASE_PADDLE_HEIGHT;
            paddleLeft.style.height = paddleLeftHeight + 'px';
        }
        if (side === 'right' || side === 'both') {
            paddleRightHeight = BASE_PADDLE_HEIGHT;
            paddleRight.style.height = paddleRightHeight + 'px';
        }
    }
    // Add deactivation for other types

    if (activePowerUp?.timeoutId) { // Clean up state
       activePowerUp = null;
    }
}

// --- Collision Handling ---
function handlePaddleCollision(paddleY, paddleHeight, paddleX, isLeftPaddle) {
    const ballRadius = BALL_SIZE / 2;
    const ballCenterX = ballX + ballRadius;
    const ballCenterY = ballY + ballRadius;

    const paddleTop = paddleY;
    const paddleBottom = paddleY + paddleHeight;
    const paddleLeftEdge = paddleX;
    const paddleRightEdge = paddleX + PADDLE_WIDTH;

    // Find closest point on paddle to ball center
    let closestX = Math.max(paddleLeftEdge, Math.min(ballCenterX, paddleRightEdge));
    let closestY = Math.max(paddleTop, Math.min(ballCenterY, paddleBottom));

    const dx = ballCenterX - closestX;
    const dy = ballCenterY - closestY;
    const distanceSquared = (dx * dx) + (dy * dy);

    if (distanceSquared < (ballRadius * ballRadius)) { // Collision detected
        // playSound('paddleHit');
        showImpactEffect(isLeftPaddle ? paddleLeft : paddleRight);

        // --- Calculate Spin based on paddle movement ---
        let paddleVelocityY = 0;
        if (isLeftPaddle) {
            paddleVelocityY = paddleLeftY - lastPlayerPaddleY; // How much paddle moved since last frame
        } else {
            // Basic AI doesn't have velocity tracking here, could add if needed
            // For now, AI imparts less spin or random spin
            paddleVelocityY = (Math.random() - 0.5) * 2; // Small random spin
        }
        // Add spin based on paddle speed, clamp it
        const addedSpin = -paddleVelocityY * SPIN_FACTOR; // Negative because Y increases downwards
        ballSpin += addedSpin;
        ballSpin = Math.max(-MAX_SPIN, Math.min(MAX_SPIN, ballSpin));

        // --- Calculate Bounce Angle ---
        const paddleCenterY = paddleTop + paddleHeight / 2;
        let hitPosition = (ballCenterY - paddleCenterY) / (paddleHeight / 2);
        hitPosition = Math.max(-1, Math.min(1, hitPosition)); // Clamp -1 to 1

        // Angle based on hit position AND incoming spin
        let bounceAngle = hitPosition * MAX_ANGLE_RAD;
        // Spin affects bounce angle slightly (e.g., topspin makes it bounce lower)
        bounceAngle -= ballSpin * 0.1; // Adjust multiplier as needed

        // --- Calculate Speed ---
        const currentSpeed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
        let chargeBoostToApply = 1.0;
        if (isLeftPaddle && currentChargeBoost > 1.0) {
             chargeBoostToApply = currentChargeBoost;
             currentChargeBoost = 1.0; // Reset boost after applying
             // console.log("Applying Charge Boost!", chargeBoostToApply.toFixed(2)); // Debug
             // playSound('chargeHit');
             showImpactEffect(ball, true); // Extra effect for charged hit
        }

        let newSpeed = currentSpeed * SPEED_INCREASE_FACTOR * chargeBoostToApply;
        newSpeed = Math.min(newSpeed, MAX_BALL_SPEED);

        // --- Calculate New Velocities ---
        const directionX = isLeftPaddle ? 1 : -1;
        ballSpeedX = directionX * newSpeed * Math.cos(bounceAngle);
        // Ball speed Y influenced by bounce angle and slightly by existing spin reversal
        ballSpeedY = newSpeed * Math.sin(bounceAngle) - ballSpin * 0.5; // Spin influences vertical speed

        // Reverse spin slightly on paddle impact (simulating friction)
        ballSpin *= -0.6; // Dampen and reverse spin slightly

        // --- Reposition Ball ---
        // Simple horizontal push out based on direction
        const overlap = ballRadius - Math.sqrt(distanceSquared);
        ballX += directionX * (overlap + 1); // Push out slightly more

        // Ensure it's fully out (optional safety)
         if (isLeftPaddle && ballX < paddleRightEdge) ballX = paddleRightEdge;
         else if (!isLeftPaddle && ballX + BALL_SIZE > paddleLeftEdge) ballX = paddleLeftEdge - BALL_SIZE;

        return true; // Indicate collision happened
    }
    return false; // No collision
}


// --- Visual Effects ---
function showImpactEffect(element, strong = false) {
    element.classList.add('impact');
    if (strong) {
        element.style.boxShadow = '0 0 25px 10px red'; // Stronger effect
    }
    setTimeout(() => {
        element.classList.remove('impact');
        element.style.boxShadow = ''; // Reset specific strong effect
    }, 100); // Duration of the flash
}

function updateBallTrail() {
    const speed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
    if (speed > INITIAL_BALL_SPEED * 1.5) { // Only show trail above certain speed
        ball.classList.add('moving');
         // Optional: Adjust trail length/opacity based on speed here if desired
    } else {
        ball.classList.remove('moving');
    }
    // Reset the animation trick for CSS trail
    void ball.offsetWidth; // Trigger reflow to restart CSS transition
}


// --- AI Logic ---
function updateBotAI() {
    // Predictive AI: Estimate where the ball will cross the bot's paddle line
    let predictedY = ballY;
    if (ballSpeedX > 0) { // Only predict if ball is moving towards bot
        const timeToReachPaddle = (window.innerWidth - PADDLE_WIDTH - ballX) / ballSpeedX;

        // Simple prediction (doesn't account for wall bounces during flight yet)
        predictedY = ballY + ballSpeedY * timeToReachPaddle;

        // Add prediction based on spin (Magnus effect over time)
        // Simplified: Average spin effect over the predicted time
        predictedY += ballSpin * MAGNUS_EFFECT_STRENGTH * timeToReachPaddle * timeToReachPaddle * ballSpeedX / 2; // Rough estimate

        // Add inaccuracy based on prediction factor
        const targetError = (paddleRightHeight / 2) * (1 - BOT_PREDICTION_FACTOR) * (Math.random() - 0.5);
        predictedY += targetError;

        // Clamp prediction to bounds (prevent predicting outside court)
         predictedY = Math.max(BALL_SIZE / 2, Math.min(predictedY, window.innerHeight - BALL_SIZE / 2));
    } else {
         // If ball moving away, slowly center the paddle
         predictedY = window.innerHeight / 2;
    }


    // Move Paddle towards predicted Y
    const botPaddleCenterTarget = predictedY - paddleRightHeight / 2; // Target top position for paddle
    const currentBotCenter = paddleRightY + paddleRightHeight / 2;

    if (paddleRightY + paddleRightHeight / 2 < predictedY - 5) { // Move down
        paddleRightY += BOT_PADDLE_SPEED;
    } else if (paddleRightY + paddleRightHeight / 2 > predictedY + 5) { // Move up
        paddleRightY -= BOT_PADDLE_SPEED;
    }

    // Clamp bot paddle position
    paddleRightY = Math.max(0, Math.min(paddleRightY, window.innerHeight - paddleRightHeight));
    paddleRight.style.top = paddleRightY + 'px';
}

// --- Game Update Loop ---
function update() {
    if (!isGamePaused) {
        // --- Ball Movement ---
        ballX += ballSpeedX;
        // Apply Magnus effect (curve due to spin)
        ballY += ballSpeedY + (ballSpin * MAGNUS_EFFECT_STRENGTH * Math.abs(ballSpeedX)); // Effect stronger at higher horizontal speed
        ballSpin *= SPIN_DRAG; // Spin decays slowly

        // --- Wall Collisions ---
        if (ballY <= 0) {
            ballY = 0;
            ballSpeedY *= -1;
            ballSpin *= 0.8; // Spin dampens on wall hit
            // playSound('wallHit');
            showImpactEffect(ball);
        } else if (ballY >= window.innerHeight - BALL_SIZE) {
            ballY = window.innerHeight - BALL_SIZE;
            ballSpeedY *= -1;
            ballSpin *= 0.8;
            // playSound('wallHit');
             showImpactEffect(ball);
        }

        // --- Paddle Collisions ---
        let collisionOccurred = false;
        if (ballSpeedX < 0) { // Moving left
            collisionOccurred = handlePaddleCollision(paddleLeftY, paddleLeftHeight, 5, true); // Left paddle at x=5
        } else { // Moving right
            collisionOccurred = handlePaddleCollision(paddleRightY, paddleRightHeight, window.innerWidth - PADDLE_WIDTH - 5, false); // Right paddle position
        }

        // --- Power-Up Logic ---
        spawnPowerUp();
        checkPowerUpCollision();

        // --- Scoring ---
        if (ballX + BALL_SIZE <= 0) {
            botScore++;
            botScoreDisplay.textContent = botScore;
            resetBall('right'); // Bot serves
            // playSound('score');
        } else if (ballX >= window.innerWidth) {
            playerScore++;
            playerScoreDisplay.textContent = playerScore;
            resetBall('left'); // Player serves
             // playSound('score');
        }

        // --- AI Update ---
        updateBotAI();

        // --- Update Visuals ---
        ball.style.left = ballX + 'px';
        ball.style.top = ballY + 'px';
        updateBallTrail();

        // Track player paddle's last position AFTER potential collision handling
        lastPlayerPaddleY = paddleLeftY;

    } // End if(!isGamePaused)

    // Loop
    requestAnimationFrame(update);
}

// --- Reset Ball Function ---
function resetBall(scoringSide) {
    isGamePaused = true; // Pause until next interaction

    // Deactivate any active powerups immediately on score
    if (activePowerUp) {
        clearTimeout(activePowerUp.timeoutId);
        deactivatePowerUp(activePowerUp.type, activePowerUp.side); // Visually reset paddles
    }
     if (powerUpData.active) {
         powerUpData.active = false;
         powerUpElement.style.display = 'none';
     }


    ballY = window.innerHeight / 2 - BALL_SIZE / 2;
    ballSpeedY = 0;
    ballSpin = 0;
    currentChargeBoost = 1.0; // Reset charge boost
    isCharging = false; // Ensure charging stops
    chargeIndicator.style.display = 'none';
    chargeIndicatorFill.style.width = '0%';


    if (scoringSide === 'left') { // Player scored, player serves
        ballX = PADDLE_WIDTH + 30;
        ballSpeedX = INITIAL_BALL_SPEED;
    } else { // Bot scored, bot serves
        ballX = window.innerWidth - PADDLE_WIDTH - BALL_SIZE - 30;
        ballSpeedX = -INITIAL_BALL_SPEED;
    }

    // Position ball visually for restart
    ball.style.left = ballX + 'px';
    ball.style.top = ballY + 'px';
    ball.classList.remove('moving'); // Stop trail

    // Could add a "Tap to serve" message here
}

// --- Utility for Sound (Placeholder) ---
// function playSound(soundName) {
//     // console.log("Playing sound:", soundName); // Placeholder
//     // In a real implementation:
//     // const audio = new Audio(`sounds/${soundName}.wav`);
//     // audio.play();
// }

// --- Start Game ---
initializeGame();
requestAnimationFrame(update); // Start the loop