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();
});