Gregniuki commited on
Commit
0d3cc49
·
verified ·
1 Parent(s): adb0008

Update script.js

Browse files
Files changed (1) hide show
  1. script.js +405 -162
script.js CHANGED
@@ -1,252 +1,495 @@
 
1
  const gameArea = document.getElementById('gameArea');
2
  const paddleLeft = document.getElementById('paddleLeft');
3
  const paddleRight = document.getElementById('paddleRight');
4
  const ball = document.getElementById('ball');
 
5
  const playerScoreDisplay = document.getElementById('playerScore');
6
  const botScoreDisplay = document.getElementById('botScore');
 
 
7
 
8
  // --- Constants ---
9
- const paddleHeight = 80;
10
- const paddleWidth = 10;
11
- const ballSize = 15;
12
- const maxAngle = 45; // Max bounce angle in degrees
13
- const initialBallSpeedX = 4; // Starting horizontal speed
14
- const speedIncreaseFactor = 1.1; // Speed multiplier on hit
15
- const maxBallSpeed = 15; // Maximum overall ball speed
16
- const botPaddleSpeed = 4; // Speed of the bot paddle
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  // --- Game State Variables ---
19
- let paddleLeftY = window.innerHeight / 2 - paddleHeight / 2;
20
- let paddleRightY = window.innerHeight / 2 - paddleHeight / 2;
21
- let ballX = window.innerWidth / 2 - ballSize / 2;
22
- let ballY = window.innerHeight / 2 - ballSize / 2;
23
- let ballSpeedX = initialBallSpeedX; // Start with initial speed
 
 
24
  let ballSpeedY = 0;
 
25
  let playerScore = 0;
26
  let botScore = 0;
27
- let isGamePaused = true; // Start paused
 
 
 
 
 
 
28
 
29
- // --- Event Listeners ---
30
- gameArea.addEventListener('touchmove', (e) => {
31
- e.preventDefault();
32
- if (!e.touches || e.touches.length === 0) return; // Safety check
 
 
 
 
 
 
 
 
 
 
33
 
34
- let touchY = e.touches[0].clientY;
35
- let touchX = e.touches[0].clientX;
36
 
37
- // Only control left paddle if touch is on the left half
38
- if (touchX < window.innerWidth / 2) {
39
- paddleLeftY = touchY - paddleHeight / 2;
40
- // Clamp paddle position within screen bounds
41
- paddleLeftY = Math.max(0, Math.min(paddleLeftY, window.innerHeight - paddleHeight));
42
- paddleLeft.style.top = paddleLeftY + 'px';
 
 
 
 
 
43
 
44
- // Start the game on first touch if paused
45
  if (isGamePaused) {
46
- // Determine initial direction based on which side was touched (less relevant here, but good practice)
47
- // ballSpeedX = initialBallSpeedX; // Or keep the direction from resetBall
48
  isGamePaused = false;
 
 
49
  }
50
  }
51
- }, { passive: false }); // Use passive: false if preventDefault is needed
52
-
53
- // Add a click/tap listener to start the game as well
54
- gameArea.addEventListener('click', () => {
55
- if (isGamePaused) {
56
- isGamePaused = false;
57
- // Optional: decide initial direction if needed
58
- // if (ballX < window.innerWidth / 2) ballSpeedX = initialBallSpeedX;
59
- // else ballSpeedX = -initialBallSpeedX;
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
 
 
 
 
63
 
64
  // --- Collision Handling ---
65
- function handlePaddleCollision(paddleY, paddleX, isLeftPaddle) {
66
- const ballRadius = ballSize / 2;
67
  const ballCenterX = ballX + ballRadius;
68
  const ballCenterY = ballY + ballRadius;
69
 
70
  const paddleTop = paddleY;
71
  const paddleBottom = paddleY + paddleHeight;
72
  const paddleLeftEdge = paddleX;
73
- const paddleRightEdge = paddleX + paddleWidth;
74
-
75
- // Simple AABB collision check first (optimization)
76
- if (ballCenterX + ballRadius < paddleLeftEdge ||
77
- ballCenterX - ballRadius > paddleRightEdge ||
78
- ballCenterY + ballRadius < paddleTop ||
79
- ballCenterY - ballRadius > paddleBottom) {
80
- return; // No collision based on bounding boxes
81
- }
82
 
83
- // More precise check: Find closest point on paddle to ball center
84
  let closestX = Math.max(paddleLeftEdge, Math.min(ballCenterX, paddleRightEdge));
85
  let closestY = Math.max(paddleTop, Math.min(ballCenterY, paddleBottom));
86
 
87
- // Calculate distance between ball center and closest point
88
  const dx = ballCenterX - closestX;
89
  const dy = ballCenterY - closestY;
90
  const distanceSquared = (dx * dx) + (dy * dy);
91
 
92
- // Check if collision occurred (distance < radius)
93
- if (distanceSquared < (ballRadius * ballRadius)) {
94
-
95
- // --- Collision Response ---
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- // 1. Calculate where the ball hit the paddle vertically (normalized)
98
- // hitPosition: -1 (top edge) to +1 (bottom edge)
99
  const paddleCenterY = paddleTop + paddleHeight / 2;
100
  let hitPosition = (ballCenterY - paddleCenterY) / (paddleHeight / 2);
101
- // Clamp hitPosition to avoid extreme angles if hit exactly on corner
102
- hitPosition = Math.max(-1, Math.min(1, hitPosition));
103
 
104
- // 2. Calculate the bounce angle in radians
105
- const bounceAngle = hitPosition * maxAngle * (Math.PI / 180);
 
 
106
 
107
- // 3. Calculate the current speed
108
  const currentSpeed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
 
 
 
 
 
 
 
 
109
 
110
- // 4. Calculate the new speed (increase and cap)
111
- let newSpeed = currentSpeed * speedIncreaseFactor;
112
- newSpeed = Math.min(newSpeed, maxBallSpeed); // Apply max speed limit
113
 
114
- // 5. Calculate new X and Y speeds based on angle and new total speed
115
- // Determine the outward direction for X based on which paddle was hit
116
  const directionX = isLeftPaddle ? 1 : -1;
117
  ballSpeedX = directionX * newSpeed * Math.cos(bounceAngle);
118
- ballSpeedY = newSpeed * Math.sin(bounceAngle);
 
 
 
 
119
 
120
- // 6. Reposition ball slightly outside the paddle to prevent sticking (optional but recommended)
121
- // Move ball along the collision normal (simplified as horizontal push)
122
  const overlap = ballRadius - Math.sqrt(distanceSquared);
123
- if (isLeftPaddle && dx < 0) { // Ball was moving right, hit left paddle
124
- ballX += overlap; // Push right
125
- } else if (!isLeftPaddle && dx > 0) { // Ball was moving left, hit right paddle
126
- ballX -= overlap; // Push left
127
- }
128
- // A more accurate push would be along the dx, dy vector, but horizontal is often sufficient
129
-
130
- // Ensure ball is definitely outside after adjustment
131
- if (isLeftPaddle && ballX + ballSize < paddleRightEdge) {
132
- ballX = paddleRightEdge - ballSize / 2; // Adjust precisely if needed
133
- } else if (!isLeftPaddle && ballX > paddleLeftEdge) {
134
- ballX = paddleLeftEdge - ballSize / 2; // Adjust precisely if needed
135
- }
136
  }
 
137
  }
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  // --- Game Update Loop ---
141
  function update() {
142
  if (!isGamePaused) {
143
- // Store previous position for collision checks if needed (not strictly needed with current collision)
144
- // let previousBallX = ballX;
145
- // let previousBallY = ballY;
146
-
147
- // Move Ball
148
  ballX += ballSpeedX;
149
- ballY += ballSpeedY;
 
 
150
 
151
- // Ball collision with top and bottom walls
152
  if (ballY <= 0) {
153
- ballY = 0; // Prevent sticking
154
- ballSpeedY = Math.abs(ballSpeedY); // Ensure it bounces down
155
- } else if (ballY >= window.innerHeight - ballSize) {
156
- ballY = window.innerHeight - ballSize; // Prevent sticking
157
- ballSpeedY = -Math.abs(ballSpeedY); // Ensure it bounces up
 
 
 
 
 
 
158
  }
159
 
160
- // Paddle Collisions
161
- // Check collision with left paddle
162
- if (ballSpeedX < 0) { // Only check left paddle if ball is moving left
163
- handlePaddleCollision(paddleLeftY, 0, true); // Left paddle is at x=0
164
- }
165
- // Check collision with right paddle
166
- else if (ballSpeedX > 0) { // Only check right paddle if ball is moving right
167
- handlePaddleCollision(paddleRightY, window.innerWidth - paddleWidth, false); // Right paddle position
168
  }
169
 
 
 
 
170
 
171
- // Ball out of bounds (Score)
172
- if (ballX + ballSize <= 0) { // Ball went past left edge
173
  botScore++;
174
  botScoreDisplay.textContent = botScore;
175
- resetBall('right'); // Bot serves next
176
- } else if (ballX >= window.innerWidth) { // Ball went past right edge
 
177
  playerScore++;
178
  playerScoreDisplay.textContent = playerScore;
179
- resetBall('left'); // Player serves next
 
180
  }
181
 
182
- // --- Bot AI Movement ---
183
- // Move bot paddle only if the ball is moving towards it
184
- if (ballSpeedX > 0) {
185
- const botPaddleCenter = paddleRightY + paddleHeight / 2;
186
- const ballCenter = ballY + ballSize / 2;
187
- const targetY = ballCenter - paddleHeight / 2; // Target top position for bot paddle
188
-
189
- // Move towards the ball's Y position, but not faster than botPaddleSpeed
190
- if (paddleRightY < targetY) {
191
- paddleRightY += Math.min(botPaddleSpeed, targetY - paddleRightY);
192
- } else if (paddleRightY > targetY) {
193
- paddleRightY -= Math.min(botPaddleSpeed, paddleRightY - targetY);
194
- }
195
- // Clamp bot paddle position
196
- paddleRightY = Math.max(0, Math.min(paddleRightY, window.innerHeight - paddleHeight));
197
- paddleRight.style.top = paddleRightY + 'px';
198
- }
199
- }
200
 
201
- // Update visual positions regardless of pause state (allows resetBall positioning)
202
- ball.style.left = ballX + 'px';
203
- ball.style.top = ballY + 'px';
204
- // Player paddle updated in event listener
205
- // Bot paddle updated in AI logic or here if needed outside AI block
206
 
 
 
207
 
208
- // Request next frame
 
 
209
  requestAnimationFrame(update);
210
  }
211
 
212
  // --- Reset Ball Function ---
213
  function resetBall(scoringSide) {
214
- isGamePaused = true; // Pause until next touch/click
215
-
216
- // Center the ball vertically
217
- ballY = window.innerHeight / 2 - ballSize / 2;
218
-
219
- // Position ball near the paddle that will serve
220
- if (scoringSide === 'left') { // Player scored, player serves (ball starts near left paddle)
221
- ballX = paddleWidth + 20; // Start just right of left paddle
222
- ballSpeedX = initialBallSpeedX; // Move right
223
- } else { // Bot scored, bot serves (ball starts near right paddle)
224
- ballX = window.innerWidth - paddleWidth - ballSize - 20; // Start just left of right paddle
225
- ballSpeedX = -initialBallSpeedX; // Move left
226
  }
 
 
 
 
227
 
228
- // Reset vertical speed
 
229
  ballSpeedY = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- // Optional: Briefly show ball position before game restarts on touch/click
232
  ball.style.left = ballX + 'px';
233
  ball.style.top = ballY + 'px';
 
234
 
235
- // Display message (optional)
236
- // gameMessage.textContent = "Tap to Serve";
237
- // gameMessage.style.display = 'block';
238
  }
239
 
240
- // --- Initial Setup ---
241
- // Set initial paddle positions visually
242
- paddleLeft.style.top = paddleLeftY + 'px';
243
- paddleRight.style.top = paddleRightY + 'px';
244
- // Set initial ball position visually
245
- ball.style.left = ballX + 'px';
246
- ball.style.top = ballY + 'px';
247
- // Set initial scores visually
248
- playerScoreDisplay.textContent = playerScore;
249
- botScoreDisplay.textContent = botScore;
250
-
251
- // Start the game loop
252
- requestAnimationFrame(update);
 
1
+ // --- Get Elements ---
2
  const gameArea = document.getElementById('gameArea');
3
  const paddleLeft = document.getElementById('paddleLeft');
4
  const paddleRight = document.getElementById('paddleRight');
5
  const ball = document.getElementById('ball');
6
+ const powerUpElement = document.getElementById('powerUp');
7
  const playerScoreDisplay = document.getElementById('playerScore');
8
  const botScoreDisplay = document.getElementById('botScore');
9
+ const chargeIndicator = document.getElementById('chargeIndicator');
10
+ const chargeIndicatorFill = document.getElementById('chargeIndicatorFill');
11
 
12
  // --- Constants ---
13
+ const BASE_PADDLE_HEIGHT = 80;
14
+ const PADDLE_WIDTH = 10;
15
+ const BALL_SIZE = 15;
16
+ const MAX_ANGLE_DEG = 60; // Increased max angle for more variation
17
+ const MAX_ANGLE_RAD = MAX_ANGLE_DEG * (Math.PI / 180);
18
+ const INITIAL_BALL_SPEED = 4;
19
+ const SPEED_INCREASE_FACTOR = 1.08; // Slightly lower increase
20
+ const MAX_BALL_SPEED = 18;
21
+ const BOT_PADDLE_SPEED = 5; // Base speed
22
+ const BOT_PREDICTION_FACTOR = 0.85; // 0 = No prediction, 1 = Perfect prediction (adjust for difficulty)
23
+ const SPIN_FACTOR = 0.08; // How much paddle speed translates to spin
24
+ const MAX_SPIN = 1.5; // Max spin value
25
+ const SPIN_DRAG = 0.995; // Spin decreases slightly over time
26
+ const MAGNUS_EFFECT_STRENGTH = 0.003; // How much spin affects trajectory
27
+ const CHARGE_RATE = 150; // Lower value = faster charge (ms per 100% charge)
28
+ const MAX_CHARGE_BOOST_FACTOR = 2.0; // Max speed multiplier for fully charged shot
29
+ const POWERUP_SPAWN_CHANCE = 0.002; // Chance per frame to spawn
30
+ const POWERUP_DURATION = 7000; // ms
31
+ const PADDLE_SIZE_INCREASE = 40; // Pixels to add to height
32
 
33
  // --- Game State Variables ---
34
+ let paddleLeftY = window.innerHeight / 2 - BASE_PADDLE_HEIGHT / 2;
35
+ let paddleRightY = window.innerHeight / 2 - BASE_PADDLE_HEIGHT / 2;
36
+ let paddleLeftHeight = BASE_PADDLE_HEIGHT;
37
+ let paddleRightHeight = BASE_PADDLE_HEIGHT;
38
+ let ballX = window.innerWidth / 2 - BALL_SIZE / 2;
39
+ let ballY = window.innerHeight / 2 - BALL_SIZE / 2;
40
+ let ballSpeedX = INITIAL_BALL_SPEED;
41
  let ballSpeedY = 0;
42
+ let ballSpin = 0; // Positive = Clockwise (Topspin from left player), Negative = Counter-Clockwise (Backspin from left player)
43
  let playerScore = 0;
44
  let botScore = 0;
45
+ let isGamePaused = true;
46
+ let lastPlayerPaddleY = paddleLeftY; // For calculating spin
47
+ let isCharging = false;
48
+ let chargeStartTime = 0;
49
+ let currentChargeBoost = 1.0; // Speed multiplier from charge
50
+ let activePowerUp = null; // { type: 'PaddleSizeUp', side: 'left'/'right'/'both', timeoutId: null }
51
+ let powerUpData = { x: 0, y: 0, active: false };
52
 
53
+ // --- Initial Setup ---
54
+ function initializeGame() {
55
+ paddleLeft.style.height = paddleLeftHeight + 'px';
56
+ paddleRight.style.height = paddleRightHeight + 'px';
57
+ paddleLeft.style.top = paddleLeftY + 'px';
58
+ paddleRight.style.top = paddleRightY + 'px';
59
+ ball.style.left = ballX + 'px';
60
+ ball.style.top = ballY + 'px';
61
+ playerScoreDisplay.textContent = playerScore;
62
+ botScoreDisplay.textContent = botScore;
63
+ // Place paddles correctly at start/resize
64
+ paddleRight.style.right = '5px'; // Ensure right paddle is positioned correctly
65
+ paddleLeft.style.left = '5px';
66
+ }
67
 
 
 
68
 
69
+ // --- Input Handling (Touch & Mouse for Player) ---
70
+ let interactionStartY = null; // Track touch start position
71
+
72
+ function handleInteractionStart(clientY, clientX) {
73
+ if (clientX < window.innerWidth / 2) { // Only charge if interacting on the left side
74
+ isCharging = true;
75
+ chargeStartTime = performance.now();
76
+ chargeIndicator.style.display = 'block';
77
+ updateChargeIndicator(); // Update visual immediately
78
+
79
+ interactionStartY = clientY; // Store initial Y for potential movement during charge
80
 
 
81
  if (isGamePaused) {
 
 
82
  isGamePaused = false;
83
+ // Determine starting direction if needed (optional)
84
+ // ballSpeedX = Math.abs(ballSpeedX) || INITIAL_BALL_SPEED;
85
  }
86
  }
87
+ }
88
+
89
+ function handleInteractionMove(clientY, clientX) {
90
+ if (clientX < window.innerWidth / 2) { // Only control left paddle
91
+ // Move paddle based on current position, not start position
92
+ paddleLeftY = clientY - paddleLeftHeight / 2;
93
+ paddleLeftY = Math.max(0, Math.min(paddleLeftY, window.innerHeight - paddleLeftHeight));
94
+ paddleLeft.style.top = paddleLeftY + 'px';
95
+ }
96
+ }
97
+
98
+ function handleInteractionEnd() {
99
+ if (isCharging) {
100
+ isCharging = false;
101
+ const chargeDuration = performance.now() - chargeStartTime;
102
+ const chargeLevel = Math.min(chargeDuration / CHARGE_RATE, 1.0); // Cap at 100%
103
+ currentChargeBoost = 1.0 + chargeLevel * (MAX_CHARGE_BOOST_FACTOR - 1.0);
104
+ // console.log("Charge Released! Boost:", currentChargeBoost.toFixed(2)); // Debug
105
+ chargeIndicator.style.display = 'none';
106
+ chargeIndicatorFill.style.width = '0%';
107
+ // Note: Boost is applied in handlePaddleCollision if released just before hit
108
  }
109
+ interactionStartY = null;
110
+ }
111
+
112
+ gameArea.addEventListener('touchstart', (e) => {
113
+ e.preventDefault();
114
+ const touch = e.touches[0];
115
+ handleInteractionStart(touch.clientY, touch.clientX);
116
+ }, { passive: false });
117
+
118
+ gameArea.addEventListener('touchmove', (e) => {
119
+ e.preventDefault();
120
+ const touch = e.touches[0];
121
+ handleInteractionMove(touch.clientY, touch.clientX);
122
+ }, { passive: false });
123
+
124
+ gameArea.addEventListener('touchend', (e) => {
125
+ handleInteractionEnd();
126
  });
127
+ gameArea.addEventListener('touchcancel', (e) => {
128
+ handleInteractionEnd(); // Treat cancel like end
129
+ });
130
+
131
+ // Mouse fallback for Desktop
132
+ gameArea.addEventListener('mousedown', (e) => handleInteractionStart(e.clientY, e.clientX));
133
+ gameArea.addEventListener('mousemove', (e) => {
134
+ // Only move paddle if mouse button is potentially down OR touch isn't active
135
+ // A better approach might track mouseDown state explicitly
136
+ if (interactionStartY !== null || e.buttons === 1) { // Move if charging or mouse down
137
+ handleInteractionMove(e.clientY, e.clientX);
138
+ }
139
+ });
140
+ gameArea.addEventListener('mouseup', (e) => handleInteractionEnd());
141
+ gameArea.addEventListener('mouseleave', (e) => handleInteractionEnd()); // Stop charging if mouse leaves
142
+
143
+
144
+ function updateChargeIndicator() {
145
+ if (isCharging) {
146
+ const chargeDuration = performance.now() - chargeStartTime;
147
+ const chargeLevel = Math.min(chargeDuration / CHARGE_RATE, 1.0);
148
+ chargeIndicatorFill.style.width = `${chargeLevel * 100}%`;
149
+ requestAnimationFrame(updateChargeIndicator); // Keep updating while charging
150
+ }
151
+ }
152
+
153
+ // --- Power-Up Logic ---
154
+ function spawnPowerUp() {
155
+ if (!powerUpData.active && Math.random() < POWERUP_SPAWN_CHANCE) {
156
+ // console.log("Spawning PowerUp!"); // Debug
157
+ powerUpData.x = Math.random() * (window.innerWidth * 0.6) + window.innerWidth * 0.2; // Spawn near middle horizontally
158
+ powerUpData.y = Math.random() * (window.innerHeight * 0.8) + window.innerHeight * 0.1; // Avoid edges vertically
159
+ powerUpData.active = true;
160
+ powerUpElement.style.left = powerUpData.x + 'px';
161
+ powerUpElement.style.top = powerUpData.y + 'px';
162
+ powerUpElement.style.display = 'block';
163
+ // playSound('powerUpSpawn');
164
+ }
165
+ }
166
+
167
+ function checkPowerUpCollision() {
168
+ if (!powerUpData.active) return;
169
+
170
+ const ballRect = ball.getBoundingClientRect();
171
+ const powerUpRect = powerUpElement.getBoundingClientRect();
172
+
173
+ if (ballRect.left < powerUpRect.right &&
174
+ ballRect.right > powerUpRect.left &&
175
+ ballRect.top < powerUpRect.bottom &&
176
+ ballRect.bottom > powerUpRect.top)
177
+ {
178
+ // console.log("PowerUp Hit!"); // Debug
179
+ powerUpData.active = false;
180
+ powerUpElement.style.display = 'none';
181
+ activatePowerUp('PaddleSizeUp'); // Hardcoded type for now
182
+ // playSound('powerUpCollect');
183
+ }
184
+ }
185
+
186
+ function activatePowerUp(type) {
187
+ clearTimeout(activePowerUp?.timeoutId); // Clear existing timer if any
188
+
189
+ const side = (ballSpeedX > 0) ? 'left' : 'right'; // Give power-up to the player who last hit
190
+ // console.log(`Activating ${type} for ${side}`); // Debug
191
+
192
+ if (type === 'PaddleSizeUp') {
193
+ if (side === 'left' || side === 'both') {
194
+ paddleLeftHeight = BASE_PADDLE_HEIGHT + PADDLE_SIZE_INCREASE;
195
+ paddleLeft.style.height = paddleLeftHeight + 'px';
196
+ }
197
+ if (side === 'right' || side === 'both') {
198
+ paddleRightHeight = BASE_PADDLE_HEIGHT + PADDLE_SIZE_INCREASE;
199
+ paddleRight.style.height = paddleRightHeight + 'px';
200
+ }
201
+ // Set timer to deactivate
202
+ const timeoutId = setTimeout(() => deactivatePowerUp(type, side), POWERUP_DURATION);
203
+ activePowerUp = { type, side, timeoutId };
204
+ }
205
+ // Add other power-up types here (MultiBall, SpeedChange, etc.)
206
+ }
207
+
208
+ function deactivatePowerUp(type, side) {
209
+ // console.log(`Deactivating ${type} for ${side}`); // Debug
210
+ if (type === 'PaddleSizeUp') {
211
+ if (side === 'left' || side === 'both') {
212
+ paddleLeftHeight = BASE_PADDLE_HEIGHT;
213
+ paddleLeft.style.height = paddleLeftHeight + 'px';
214
+ }
215
+ if (side === 'right' || side === 'both') {
216
+ paddleRightHeight = BASE_PADDLE_HEIGHT;
217
+ paddleRight.style.height = paddleRightHeight + 'px';
218
+ }
219
+ }
220
+ // Add deactivation for other types
221
 
222
+ if (activePowerUp?.timeoutId) { // Clean up state
223
+ activePowerUp = null;
224
+ }
225
+ }
226
 
227
  // --- Collision Handling ---
228
+ function handlePaddleCollision(paddleY, paddleHeight, paddleX, isLeftPaddle) {
229
+ const ballRadius = BALL_SIZE / 2;
230
  const ballCenterX = ballX + ballRadius;
231
  const ballCenterY = ballY + ballRadius;
232
 
233
  const paddleTop = paddleY;
234
  const paddleBottom = paddleY + paddleHeight;
235
  const paddleLeftEdge = paddleX;
236
+ const paddleRightEdge = paddleX + PADDLE_WIDTH;
 
 
 
 
 
 
 
 
237
 
238
+ // Find closest point on paddle to ball center
239
  let closestX = Math.max(paddleLeftEdge, Math.min(ballCenterX, paddleRightEdge));
240
  let closestY = Math.max(paddleTop, Math.min(ballCenterY, paddleBottom));
241
 
 
242
  const dx = ballCenterX - closestX;
243
  const dy = ballCenterY - closestY;
244
  const distanceSquared = (dx * dx) + (dy * dy);
245
 
246
+ if (distanceSquared < (ballRadius * ballRadius)) { // Collision detected
247
+ // playSound('paddleHit');
248
+ showImpactEffect(isLeftPaddle ? paddleLeft : paddleRight);
249
+
250
+ // --- Calculate Spin based on paddle movement ---
251
+ let paddleVelocityY = 0;
252
+ if (isLeftPaddle) {
253
+ paddleVelocityY = paddleLeftY - lastPlayerPaddleY; // How much paddle moved since last frame
254
+ } else {
255
+ // Basic AI doesn't have velocity tracking here, could add if needed
256
+ // For now, AI imparts less spin or random spin
257
+ paddleVelocityY = (Math.random() - 0.5) * 2; // Small random spin
258
+ }
259
+ // Add spin based on paddle speed, clamp it
260
+ const addedSpin = -paddleVelocityY * SPIN_FACTOR; // Negative because Y increases downwards
261
+ ballSpin += addedSpin;
262
+ ballSpin = Math.max(-MAX_SPIN, Math.min(MAX_SPIN, ballSpin));
263
 
264
+ // --- Calculate Bounce Angle ---
 
265
  const paddleCenterY = paddleTop + paddleHeight / 2;
266
  let hitPosition = (ballCenterY - paddleCenterY) / (paddleHeight / 2);
267
+ hitPosition = Math.max(-1, Math.min(1, hitPosition)); // Clamp -1 to 1
 
268
 
269
+ // Angle based on hit position AND incoming spin
270
+ let bounceAngle = hitPosition * MAX_ANGLE_RAD;
271
+ // Spin affects bounce angle slightly (e.g., topspin makes it bounce lower)
272
+ bounceAngle -= ballSpin * 0.1; // Adjust multiplier as needed
273
 
274
+ // --- Calculate Speed ---
275
  const currentSpeed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
276
+ let chargeBoostToApply = 1.0;
277
+ if (isLeftPaddle && currentChargeBoost > 1.0) {
278
+ chargeBoostToApply = currentChargeBoost;
279
+ currentChargeBoost = 1.0; // Reset boost after applying
280
+ // console.log("Applying Charge Boost!", chargeBoostToApply.toFixed(2)); // Debug
281
+ // playSound('chargeHit');
282
+ showImpactEffect(ball, true); // Extra effect for charged hit
283
+ }
284
 
285
+ let newSpeed = currentSpeed * SPEED_INCREASE_FACTOR * chargeBoostToApply;
286
+ newSpeed = Math.min(newSpeed, MAX_BALL_SPEED);
 
287
 
288
+ // --- Calculate New Velocities ---
 
289
  const directionX = isLeftPaddle ? 1 : -1;
290
  ballSpeedX = directionX * newSpeed * Math.cos(bounceAngle);
291
+ // Ball speed Y influenced by bounce angle and slightly by existing spin reversal
292
+ ballSpeedY = newSpeed * Math.sin(bounceAngle) - ballSpin * 0.5; // Spin influences vertical speed
293
+
294
+ // Reverse spin slightly on paddle impact (simulating friction)
295
+ ballSpin *= -0.6; // Dampen and reverse spin slightly
296
 
297
+ // --- Reposition Ball ---
298
+ // Simple horizontal push out based on direction
299
  const overlap = ballRadius - Math.sqrt(distanceSquared);
300
+ ballX += directionX * (overlap + 1); // Push out slightly more
301
+
302
+ // Ensure it's fully out (optional safety)
303
+ if (isLeftPaddle && ballX < paddleRightEdge) ballX = paddleRightEdge;
304
+ else if (!isLeftPaddle && ballX + BALL_SIZE > paddleLeftEdge) ballX = paddleLeftEdge - BALL_SIZE;
305
+
306
+ return true; // Indicate collision happened
 
 
 
 
 
 
307
  }
308
+ return false; // No collision
309
  }
310
 
311
 
312
+ // --- Visual Effects ---
313
+ function showImpactEffect(element, strong = false) {
314
+ element.classList.add('impact');
315
+ if (strong) {
316
+ element.style.boxShadow = '0 0 25px 10px red'; // Stronger effect
317
+ }
318
+ setTimeout(() => {
319
+ element.classList.remove('impact');
320
+ element.style.boxShadow = ''; // Reset specific strong effect
321
+ }, 100); // Duration of the flash
322
+ }
323
+
324
+ function updateBallTrail() {
325
+ const speed = Math.sqrt(ballSpeedX * ballSpeedX + ballSpeedY * ballSpeedY);
326
+ if (speed > INITIAL_BALL_SPEED * 1.5) { // Only show trail above certain speed
327
+ ball.classList.add('moving');
328
+ // Optional: Adjust trail length/opacity based on speed here if desired
329
+ } else {
330
+ ball.classList.remove('moving');
331
+ }
332
+ // Reset the animation trick for CSS trail
333
+ void ball.offsetWidth; // Trigger reflow to restart CSS transition
334
+ }
335
+
336
+
337
+ // --- AI Logic ---
338
+ function updateBotAI() {
339
+ // Predictive AI: Estimate where the ball will cross the bot's paddle line
340
+ let predictedY = ballY;
341
+ if (ballSpeedX > 0) { // Only predict if ball is moving towards bot
342
+ const timeToReachPaddle = (window.innerWidth - PADDLE_WIDTH - ballX) / ballSpeedX;
343
+
344
+ // Simple prediction (doesn't account for wall bounces during flight yet)
345
+ predictedY = ballY + ballSpeedY * timeToReachPaddle;
346
+
347
+ // Add prediction based on spin (Magnus effect over time)
348
+ // Simplified: Average spin effect over the predicted time
349
+ predictedY += ballSpin * MAGNUS_EFFECT_STRENGTH * timeToReachPaddle * timeToReachPaddle * ballSpeedX / 2; // Rough estimate
350
+
351
+ // Add inaccuracy based on prediction factor
352
+ const targetError = (paddleRightHeight / 2) * (1 - BOT_PREDICTION_FACTOR) * (Math.random() - 0.5);
353
+ predictedY += targetError;
354
+
355
+ // Clamp prediction to bounds (prevent predicting outside court)
356
+ predictedY = Math.max(BALL_SIZE / 2, Math.min(predictedY, window.innerHeight - BALL_SIZE / 2));
357
+ } else {
358
+ // If ball moving away, slowly center the paddle
359
+ predictedY = window.innerHeight / 2;
360
+ }
361
+
362
+
363
+ // Move Paddle towards predicted Y
364
+ const botPaddleCenterTarget = predictedY - paddleRightHeight / 2; // Target top position for paddle
365
+ const currentBotCenter = paddleRightY + paddleRightHeight / 2;
366
+
367
+ if (paddleRightY + paddleRightHeight / 2 < predictedY - 5) { // Move down
368
+ paddleRightY += BOT_PADDLE_SPEED;
369
+ } else if (paddleRightY + paddleRightHeight / 2 > predictedY + 5) { // Move up
370
+ paddleRightY -= BOT_PADDLE_SPEED;
371
+ }
372
+
373
+ // Clamp bot paddle position
374
+ paddleRightY = Math.max(0, Math.min(paddleRightY, window.innerHeight - paddleRightHeight));
375
+ paddleRight.style.top = paddleRightY + 'px';
376
+ }
377
+
378
  // --- Game Update Loop ---
379
  function update() {
380
  if (!isGamePaused) {
381
+ // --- Ball Movement ---
 
 
 
 
382
  ballX += ballSpeedX;
383
+ // Apply Magnus effect (curve due to spin)
384
+ ballY += ballSpeedY + (ballSpin * MAGNUS_EFFECT_STRENGTH * Math.abs(ballSpeedX)); // Effect stronger at higher horizontal speed
385
+ ballSpin *= SPIN_DRAG; // Spin decays slowly
386
 
387
+ // --- Wall Collisions ---
388
  if (ballY <= 0) {
389
+ ballY = 0;
390
+ ballSpeedY *= -1;
391
+ ballSpin *= 0.8; // Spin dampens on wall hit
392
+ // playSound('wallHit');
393
+ showImpactEffect(ball);
394
+ } else if (ballY >= window.innerHeight - BALL_SIZE) {
395
+ ballY = window.innerHeight - BALL_SIZE;
396
+ ballSpeedY *= -1;
397
+ ballSpin *= 0.8;
398
+ // playSound('wallHit');
399
+ showImpactEffect(ball);
400
  }
401
 
402
+ // --- Paddle Collisions ---
403
+ let collisionOccurred = false;
404
+ if (ballSpeedX < 0) { // Moving left
405
+ collisionOccurred = handlePaddleCollision(paddleLeftY, paddleLeftHeight, 5, true); // Left paddle at x=5
406
+ } else { // Moving right
407
+ collisionOccurred = handlePaddleCollision(paddleRightY, paddleRightHeight, window.innerWidth - PADDLE_WIDTH - 5, false); // Right paddle position
 
 
408
  }
409
 
410
+ // --- Power-Up Logic ---
411
+ spawnPowerUp();
412
+ checkPowerUpCollision();
413
 
414
+ // --- Scoring ---
415
+ if (ballX + BALL_SIZE <= 0) {
416
  botScore++;
417
  botScoreDisplay.textContent = botScore;
418
+ resetBall('right'); // Bot serves
419
+ // playSound('score');
420
+ } else if (ballX >= window.innerWidth) {
421
  playerScore++;
422
  playerScoreDisplay.textContent = playerScore;
423
+ resetBall('left'); // Player serves
424
+ // playSound('score');
425
  }
426
 
427
+ // --- AI Update ---
428
+ updateBotAI();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
 
430
+ // --- Update Visuals ---
431
+ ball.style.left = ballX + 'px';
432
+ ball.style.top = ballY + 'px';
433
+ updateBallTrail();
 
434
 
435
+ // Track player paddle's last position AFTER potential collision handling
436
+ lastPlayerPaddleY = paddleLeftY;
437
 
438
+ } // End if(!isGamePaused)
439
+
440
+ // Loop
441
  requestAnimationFrame(update);
442
  }
443
 
444
  // --- Reset Ball Function ---
445
  function resetBall(scoringSide) {
446
+ isGamePaused = true; // Pause until next interaction
447
+
448
+ // Deactivate any active powerups immediately on score
449
+ if (activePowerUp) {
450
+ clearTimeout(activePowerUp.timeoutId);
451
+ deactivatePowerUp(activePowerUp.type, activePowerUp.side); // Visually reset paddles
 
 
 
 
 
 
452
  }
453
+ if (powerUpData.active) {
454
+ powerUpData.active = false;
455
+ powerUpElement.style.display = 'none';
456
+ }
457
 
458
+
459
+ ballY = window.innerHeight / 2 - BALL_SIZE / 2;
460
  ballSpeedY = 0;
461
+ ballSpin = 0;
462
+ currentChargeBoost = 1.0; // Reset charge boost
463
+ isCharging = false; // Ensure charging stops
464
+ chargeIndicator.style.display = 'none';
465
+ chargeIndicatorFill.style.width = '0%';
466
+
467
+
468
+ if (scoringSide === 'left') { // Player scored, player serves
469
+ ballX = PADDLE_WIDTH + 30;
470
+ ballSpeedX = INITIAL_BALL_SPEED;
471
+ } else { // Bot scored, bot serves
472
+ ballX = window.innerWidth - PADDLE_WIDTH - BALL_SIZE - 30;
473
+ ballSpeedX = -INITIAL_BALL_SPEED;
474
+ }
475
 
476
+ // Position ball visually for restart
477
  ball.style.left = ballX + 'px';
478
  ball.style.top = ballY + 'px';
479
+ ball.classList.remove('moving'); // Stop trail
480
 
481
+ // Could add a "Tap to serve" message here
 
 
482
  }
483
 
484
+ // --- Utility for Sound (Placeholder) ---
485
+ // function playSound(soundName) {
486
+ // // console.log("Playing sound:", soundName); // Placeholder
487
+ // // In a real implementation:
488
+ // // const audio = new Audio(`sounds/${soundName}.wav`);
489
+ // // audio.play();
490
+ // }
491
+
492
+ // --- Start Game ---
493
+ initializeGame();
494
+ requestAnimationFrame(update); // Start the loop
495
+