Laps: 0/3 | Time: 0.0s | Speed: 0 km/h
// Game Settings canvas.width = 800; canvas.height = 600; const FRICTION = 0.98; const DRIFT = 0.96; const ACCEL = 0.22; const TURN_SPEED = 0.07; const MAX_SPEED = 8; const OFF_ROAD_FRICTION = 0.8; let gameRunning = false; let startTime = 0; let currentLaps = 0; const keys = {}; window.addEventListener('keydown', e => keys[e.code] = true); window.addEventListener('keyup', e => keys[e.code] = false); class Car { constructor(x, y, color, isAI = false) { this.x = x; this.y = y; this.angle = 0; this.speed = 0; this.color = color; this.width = 20; this.height = 40; this.isAI = isAI; this.checkpoints = [ {x: 150, y: 300, r: 60}, // Start/Finish {x: 400, y: 100, r: 60}, // Top {x: 700, y: 300, r: 60}, // Right {x: 400, y: 500, r: 60}, // Bottom {x: 100, y: 450, r: 60} // Left Turn ]; this.currentCheckpoint = 0; } update() { if (this.isAI) { this.updateAI(); } else { if (keys['ArrowUp'] || keys['KeyW']) this.speed += ACCEL; else if (keys['ArrowDown'] || keys['KeyS']) this.speed -= ACCEL; else this.speed *= FRICTION; if (Math.abs(this.speed) > 0.5) { const direction = this.speed > 0 ? 1 : -1; const turnFactor = Math.min(Math.abs(this.speed), MAX_SPEED) / MAX_SPEED; if (keys['ArrowLeft'] || keys['KeyA']) this.angle -= TURN_SPEED * turnFactor * direction; if (keys['ArrowRight'] || keys['KeyD']) this.angle += TURN_SPEED * turnFactor * direction; } this.speed *= FRICTION; if (this.speed > MAX_SPEED) this.speed = MAX_SPEED; if (this.speed < -MAX_SPEED / 2) this.speed = -MAX_SPEED / 2; this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; // Off-road detection: if car is far from the center of the track // Simplified: if it's near the edges of the play area if (this.x < 80 || this.x > 720 || this.y < 80 || this.y > 520) { this.speed *= OFF_ROAD_FRICTION; } } if (!this.isAI) { const cp = this.checkpoints[this.currentCheckpoint]; const dist = Math.hypot(this.x - cp.x, this.y - cp.y); if (dist < cp.r) { this.currentCheckpoint = (this.currentCheckpoint + 1) % this.checkpoints.length; if (this.currentCheckpoint === 0) { currentLaps++; lapEl.innerText = currentLaps; if (currentLaps >= 3) endGame(); } } } } updateAI() { const target = this.checkpoints[this.currentCheckpoint]; const dx = target.x - this.x; const dy = target.y - this.y; const angleToTarget = Math.atan2(dy, dx); let diff = angleToTarget - this.angle; while (diff < -Math.PI) diff += Math.PI * 2; while (diff > Math.PI) diff -= Math.PI * 2; this.angle += diff * 0.04; this.speed += ACCEL * 0.8; if (this.speed > MAX_SPEED) this.speed = MAX_SPEED; this.speed *= 0.98; this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; const dist = Math.hypot(this.x - target.x, this.y - target.y); if (dist < target.r) { this.currentCheckpoint = (this.currentCheckpoint + 1) % this.checkpoints.length; } } draw() { ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle); // Car Body ctx.fillStyle = this.color; ctx.fillRect(-this.height/2, -this.width/2, this.height, this.width); // Windshield ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fillRect(5, -this.width/2 + 2, 12, this.width - 4); // Tires ctx.fillStyle = 'black'; ctx.fillRect(-15, -this.width/2 - 2, 10, 4); ctx.fillRect(-15, this.width/2 - 2, 10, 4); ctx.fillRect(10, -this.width/2 - 2, 10, 4); ctx.fillRect(10, this.width/2 - 2, 10, 4); ctx.restore(); } } const player = new Car(150, 300, '#ff3300'); const enemies = [ new Car(150, 320, '#00ff00', true), new Car(150, 280, '#0000ff', true) ]; const boostPads = [ {x: 400, y: 100, r: 30}, {x: 700, y: 300, r: 30}, {x: 400, y: 500, r: 30} ]; function drawTrack() { // Grass (Background) ctx.fillStyle = '#2d5a27'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Road ctx.strokeStyle = '#444'; ctx.lineWidth = 110; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(150, 300); ctx.lineTo(400, 100); ctx.lineTo(700, 300); ctx.lineTo(400, 500); ctx.lineTo(100, 450); ctx.closePath(); ctx.stroke(); // Road Surface details ctx.strokeStyle = '#555'; ctx.lineWidth = 105; ctx.stroke(); // Center Line ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.setLineDash([20, 20]); ctx.beginPath(); ctx.moveTo(150, 300); ctx.lineTo(400, 100); ctx.lineTo(700, 300); ctx.lineTo(400, 500); ctx.lineTo(100, 450); ctx.closePath(); ctx.stroke(); ctx.setLineDash([]); // Boost Pads boostPads.forEach(pad => { ctx.beginPath(); ctx.arc(pad.x, pad.y, pad.r, 0, Math.PI * 2); ctx.fillStyle = 'rgba(255, 255, 0, 0.6)'; ctx.fill(); ctx.strokeStyle = 'yellow'; ctx.lineWidth = 3; ctx.stroke(); }); // Checkpoints Visual player.checkpoints.forEach((cp, i) => { ctx.beginPath(); ctx.arc(cp.x, cp.y, cp.r, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(255,255,255,0.1)'; ctx.stroke(); ctx.fillStyle = `rgba(255, 255, 0, ${0.1 + (i/5)})`; ctx.fill(); }); } function endGame() { gameRunning = false; uiOverlay.style.display = 'flex'; uiOverlay.querySelector('h1').innerText = 'Race Finished!'; uiOverlay.querySelector('p').innerText = `Laps Completed: ${currentLaps}`; startBtn.innerText = 'RESTART'; } function gameLoop() { if (!gameRunning) return; ctx.clearRect(0, 0, canvas.width, canvas.height); drawTrack(); // Boost logic boostPads.forEach(pad => { if (Math.hypot(player.x - pad.x, player.y - pad.y) < pad.r) { player.speed += 2; ctx.beginPath(); ctx.arc(pad.x, pad.y, pad.r + 10, 0, Math.PI * 2); ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.stroke(); } }); player.update(); player.draw(); enemies.forEach(enemy => { enemy.update(); enemy.draw(); }); // HUD updates timerEl.innerText = (performance.now() - startTime) / 1000; speedEl.innerText = Math.floor(Math.abs(player.speed) * 20); requestAnimationFrame(gameLoop); } startBtn.addEventListener('click', () => { currentLaps = 0; lapEl.innerText = '0'; player.x = 150; player.y = 300; player.speed = 0; player.angle = 0; player.currentCheckpoint = 0; enemies.forEach((e, i) => { e.x = 150; e.y = 300 + (i * 40); e.speed = 0; e.currentCheckpoint = 0; }); startTime = performance.now(); gameRunning = true; uiOverlay.style.display = 'none'; gameLoop(); });