|
35 | 35 | // Game manager - handles different game types |
36 | 36 | let currentGame = null; |
37 | 37 | let gameUpdateInterval = null; |
| 38 | + let gameAnimationFrame = null; |
38 | 39 |
|
39 | 40 | function setStatus(msg) { |
40 | 41 | const status = document.getElementById('status'); |
|
88 | 89 |
|
89 | 90 | this.opponentPositions = [0, 0, 0]; |
90 | 91 | this.opponentSpeeds = config.racing?.opponentSpeeds || [0.3, 0.4, 0.5]; |
| 92 | + // Convert speeds from pixels per frame (at 60fps) to pixels per second |
| 93 | + // Original speeds: 0.3, 0.4, 0.5 px/frame at 60fps = 18, 24, 30 px/s |
| 94 | + this.opponentSpeedsPxPerSec = this.opponentSpeeds.map(speed => speed * 60); |
91 | 95 | // Current speeds with randomness (initialized to base speeds) |
92 | | - this.currentOpponentSpeeds = [...this.opponentSpeeds]; |
93 | | - this.speedUpdateTimer = 0; // Timer for speed updates |
| 96 | + this.currentOpponentSpeeds = [...this.opponentSpeedsPxPerSec]; |
| 97 | + this.speedUpdateTimer = 0; // Timer for speed updates (in milliseconds) |
94 | 98 | this.speedUpdateInterval = 1500 + Math.random() * 1000; // Update speed every 1.5-2.5 seconds |
| 99 | + this.lastFrameTime = null; // For delta time calculation |
95 | 100 | this.trackWidth = 0; |
96 | 101 | this.finishLineTextPosition = 0; // Position in text coordinates |
97 | 102 | this.isFinished = false; |
|
126 | 131 | this.isFinished = false; |
127 | 132 | this.finishLineTextPosition = 0; |
128 | 133 | this.playerWon = null; // Reset win/loss status |
129 | | - // Reset speeds to base speeds |
130 | | - this.currentOpponentSpeeds = [...this.opponentSpeeds]; |
| 134 | + // Reset speeds to base speeds (in px/s) |
| 135 | + this.currentOpponentSpeeds = [...this.opponentSpeedsPxPerSec]; |
131 | 136 | this.speedUpdateTimer = 0; |
132 | 137 | this.speedUpdateInterval = 1500 + Math.random() * 1000; // Reset update interval |
| 138 | + this.lastFrameTime = null; // Reset frame time |
133 | 139 |
|
134 | 140 | if (this.playerCar) { |
135 | 141 | this.playerCar.style.left = '20px'; |
|
151 | 157 | updateOpponentSpeeds() { |
152 | 158 | // Update speeds with small random variations |
153 | 159 | // Variations are ±20% of base speed to keep it realistic |
154 | | - this.opponentSpeeds.forEach((baseSpeed, index) => { |
| 160 | + this.opponentSpeedsPxPerSec.forEach((baseSpeed, index) => { |
155 | 161 | const variation = 0.2; // ±20% variation |
156 | 162 | const randomFactor = 1 + (Math.random() * 2 - 1) * variation; // Random between 0.8 and 1.2 |
157 | 163 | this.currentOpponentSpeeds[index] = baseSpeed * randomFactor; |
|
239 | 245 | // Finish line is positioned with right: 0 in CSS, so it automatically aligns with track edge |
240 | 246 | } |
241 | 247 |
|
242 | | - updateOpponents() { |
| 248 | + updateOpponents(currentTime) { |
243 | 249 | if (this.isFinished || !startTime) return; |
244 | 250 |
|
| 251 | + // Calculate delta time (time since last frame) in seconds |
| 252 | + let deltaTime = 0; |
| 253 | + if (this.lastFrameTime !== null) { |
| 254 | + deltaTime = (currentTime - this.lastFrameTime) / 1000; // Convert to seconds |
| 255 | + // Clamp delta time to prevent large jumps (e.g., when tab regains focus) |
| 256 | + deltaTime = Math.min(deltaTime, 0.1); // Max 100ms delta (10fps minimum) |
| 257 | + } |
| 258 | + this.lastFrameTime = currentTime; |
| 259 | + |
| 260 | + // Skip update if this is the first frame (no delta time yet) |
| 261 | + if (deltaTime === 0) return; |
| 262 | + |
245 | 263 | // Update finish line position first (in case text changed) |
246 | 264 | this.updateFinishLinePosition(); |
247 | 265 |
|
248 | 266 | // Update speeds periodically with randomness |
249 | | - this.speedUpdateTimer += 16; // ~60fps, so 16ms per frame |
| 267 | + this.speedUpdateTimer += deltaTime * 1000; // Convert to milliseconds |
250 | 268 | if (this.speedUpdateTimer >= this.speedUpdateInterval) { |
251 | 269 | this.updateOpponentSpeeds(); |
252 | 270 | } |
|
259 | 277 | this.opponentCars.forEach((car, index) => { |
260 | 278 | if (!car) return; |
261 | 279 |
|
262 | | - // Use current speed (with randomness) instead of base speed |
263 | | - const speed = this.currentOpponentSpeeds[index] || this.opponentSpeeds[index] || 0.3; |
264 | | - this.opponentPositions[index] += speed; |
| 280 | + // Use current speed (with randomness) in pixels per second |
| 281 | + // Multiply by deltaTime to get frame-rate independent movement |
| 282 | + const speedPxPerSec = this.currentOpponentSpeeds[index] || this.opponentSpeedsPxPerSec[index] || 18; |
| 283 | + const movementThisFrame = speedPxPerSec * deltaTime; // pixels this frame |
| 284 | + this.opponentPositions[index] += movementThisFrame; |
265 | 285 |
|
266 | 286 | // Calculate car position |
267 | 287 | // Car's left edge is at: 20px (start) + opponentPositions[index] |
|
356 | 376 | clearInterval(gameUpdateInterval); |
357 | 377 | gameUpdateInterval = null; |
358 | 378 | } |
| 379 | + if (gameAnimationFrame !== null) { |
| 380 | + cancelAnimationFrame(gameAnimationFrame); |
| 381 | + gameAnimationFrame = null; |
| 382 | + } |
359 | 383 | } |
360 | 384 |
|
361 | 385 | // Initialize based on game type |
|
369 | 393 |
|
370 | 394 | currentGame.initialize(); |
371 | 395 |
|
372 | | - // Start game update loop for racing |
| 396 | + // Start game update loop for racing using requestAnimationFrame |
373 | 397 | if (gameType === 'racing' && currentGame instanceof RacingGame) { |
374 | | - gameUpdateInterval = setInterval(() => { |
| 398 | + function animate(currentTime) { |
375 | 399 | if (currentGame && currentGame.updateOpponents) { |
376 | | - currentGame.updateOpponents(); |
| 400 | + currentGame.updateOpponents(currentTime); |
377 | 401 | } |
378 | | - }, 16); // ~60fps |
| 402 | + // Continue animation loop |
| 403 | + gameAnimationFrame = requestAnimationFrame(animate); |
| 404 | + } |
| 405 | + // Start the animation loop |
| 406 | + gameAnimationFrame = requestAnimationFrame(animate); |
379 | 407 | } |
380 | 408 |
|
381 | 409 | // Re-render text if it's already loaded |
|
0 commit comments