Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dirt Rally Arcade</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: #111;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
#game-container {
position: relative;
width: 800px;
height: 600px;
box-shadow: 0 0 20px rgba(255, 120, 0, 0.4);
border: 3px solid #ff6600;
}
canvas {
display: block;
width: 100%;
height: 100%;
background: #000;
}
#ui-overlay {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
display: flex;
justify-content: space-between;
pointer-events: none;
}
.hud-box {
background: rgba(0, 0, 0, 0.7);
border-left: 4px solid #ff6600;
padding: 10px 18px;
font-size: 20px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
}
.hud-box span {
color: #ff6600;
}
#controls-hint {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.6);
padding: 6px 15px;
border-radius: 20px;
font-size: 13px;
color: #aaa;
pointer-events: none;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="canvas" width="800" height="600"></canvas>
<div id="ui-overlay">
<div class="hud-box">SPEED: <span id="speed-val">0</span> KM/H</div>
<div class="hud-box">SCORE: <span id="score-val">0</span></div>
</div>
<div id="controls-hint">Controls: [UP / W] Accelerate | [DOWN / S] Brake | [LEFT/RIGHT / A/D] Steer | [SPACE] Handbrake Drift</div>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const speedEl = document.getElementById('speed-val');
const scoreEl = document.getElementById('score-val');
// Game Config & Constants
const CANVAS_WIDTH = 800;
const CANVAS_HEIGHT = 600;
const ROAD_WIDTH = 2000;
const SEGMENT_LENGTH = 200;
const CAM_DEPTH = 0.8;
const DRAW_DISTANCE = 300;
// Inputs
const keys = {
Up: false,
Down: false,
Left: false,
Right: false,
Space: false
};
// Player Physics State
let playerX = 0; // Normalized position (-1 left, 1 right relative to road center)
let playerZ = 0; // Distance along track
let speed = 0; // Current speed
const maxSpeed = 12000; // Max units/sec
const accel = 4800;
const braking = 8000;
const decel = 3000;
const offroadDecel = 9000;
const offroadMaxSpeed = 4000;
let driftFactor = 0;
let score = 0;
// Track Generation
let segments = [];
const TRACK_LENGTH = 1200;
function buildTrack() {
segments = [];
for (let i = 0; i < TRACK_LENGTH; i++) {
let curve = 0;
// Add curves at specific segments to simulate rally stages
if (i > 100 && i < 300) curve = 2.5;
if (i > 400 && i < 600) curve = -3.5;
if (i > 700 && i < 900) curve = 4;
if (i > 1000 && i < 1150) curve = -2;
segments.push({
index: i,
p1: { world: { z: i * SEGMENT_LENGTH }, camera: {}, screen: {} },
p2: { world: { z: (i + 1) * SEGMENT_LENGTH }, camera: {}, screen: {} },
curve: curve,
color: (Math.floor(i / 3) % 2) ?
{ grass: '#4e3b2b', road: '#6e5a44', lane: '#8a735a', border: '#ff6600' } :
{ grass: '#3a2b1f', road: '#5c4a37', lane: '#6e5a44', border: '#ffffff' }
});
}
}
// Projection Geometry
function project(p, cameraX, cameraY, cameraZ) {
p.camera.x = p.world.x - cameraX;
p.camera.y = p.world.y - cameraY;
p.camera.z = p.world.z - cameraZ;
let scale = CAM_DEPTH / (p.camera.z / 1000);
p.screen.scale = scale;
p.screen.x = Math.round((CANVAS_WIDTH / 2) + (scale * p.camera.x * CANVAS_WIDTH / 2));
p.screen.y = Math.round((CANVAS_HEIGHT / 2) - (scale * p.camera.y * CANVAS_HEIGHT / 2));
p.screen.w = Math.round(scale * ROAD_WIDTH * CANVAS_WIDTH / 2);
}
// Input Handlers
window.addEventListener('keydown', (e) => {
if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.Up = true;
if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.Down = true;
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.Left = true;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.Right = true;
if (e.code === 'Space') keys.Space = true;
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.Up = false;
if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.Down = false;
if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.Left = false;
if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.Right = false;
if (e.code === 'Space') keys.Space = false;
});
// Segment Finder Helper
function findSegment(z) {
return segments[Math.floor(z / SEGMENT_LENGTH) % segments.length];
}
// Render Trapezoid Road Segment
function renderSegment(ctx, width, lanes, x1, y1, w1, x2, y2, w2, color) {
// Grass background
ctx.fillStyle = color.grass;
ctx.fillRect(0, y2, width, y1 - y2);
// Road Base
ctx.fillStyle = color.road;
ctx.beginPath();
ctx.moveTo(x1 - w1, y1);
ctx.lineTo(x2 - w2, y2);
ctx.lineTo(x2 + w2, y2);
ctx.lineTo(x1 + w1, y1);
ctx.closePath();
ctx.fill();
// Red/White Dirt Borders
let bw1 = w1 * 0.1;
let bw2 = w2 * 0.1;
ctx.fillStyle = color.border;
ctx.beginPath();
ctx.moveTo(x1 - w1 - bw1, y1); ctx.lineTo(x2 - w2 - bw2, y2);
ctx.lineTo(x2 - w2, y2); ctx.lineTo(x1 - w1, y1);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(x1 + w1, y1); ctx.lineTo(x2 + w2, y2);
ctx.lineTo(x2 + w2 + bw2, y2); ctx.lineTo(x1 + w1 + bw1, y1);
ctx.closePath(); ctx.fill();
}
// Render Player Car Sprite
function renderPlayer(ctx, speedRatio, turnSign, isDrifting) {
let cx = CANVAS_WIDTH / 2;
let cy = CANVAS_HEIGHT - 70;
ctx.save();
ctx.translate(cx, cy);
// Dynamic tilt angle
let angle = turnSign * 0.08;
if (isDrifting) angle = turnSign * 0.22;
ctx.rotate(angle);
// Car Body Base
ctx.fillStyle = '#cc2200'; // Rally Red Body
ctx.fillRect(-40, -25, 80, 40);
// Roof / Windshield
ctx.fillStyle = '#111';
ctx.fillRect(-30, -20, 60, 20);
// Headlights / Taillights
ctx.fillStyle = '#ff2200'; // Red Taillights
ctx.fillRect(-38, 10, 15, 6);
ctx.fillRect(23, 10, 15, 6);
// Rear Bumper Details
ctx.fillStyle = '#333';
ctx.fillRect(-40, 12, 80, 5);
// Tires
ctx.fillStyle = '#000';
ctx.fillRect(-45, -5, 10, 20); // Rear Left
ctx.fillRect(35, -5, 10, 20); // Rear Right
// Dust particles when drifting/turning
if (speedRatio > 0.2 && (isDrifting || Math.abs(turnSign) > 0.5)) {
ctx.fillStyle = 'rgba(160, 130, 90, 0.6)';
for (let i = 0; i < 6; i++) {
let px = (Math.random() - 0.5) * 60 + (turnSign * -30);
let py = 15 + Math.random() * 20;
let pSize = Math.random() * 10 + 4;
ctx.beginPath();
ctx.arc(px, py, pSize, 0, Math.PI * 2);
ctx.fill();
}
}
ctx.restore();
}
// Game Loop Processing
let lastTime = performance.now();
function update(dt) {
let currentSegment = findSegment(playerZ);
let speedRatio = speed / maxSpeed;
// Steering & Drifting
let steerPower = 1.4;
let isDrifting = keys.Space && speed > 2000;
if (isDrifting) {
steerPower = 2.8; // Enhanced response during drift
driftFactor = Math.min(driftFactor + dt * 2, 1);
if (speed > 1000) score += Math.floor(dt * 500);
} else {
driftFactor = Math.max(driftFactor - dt * 2, 0);
}
let turnSign = 0;
if (keys.Left) {
playerX -= steerPower * speedRatio * dt;
turnSign = -1;
}
if (keys.Right) {
playerX += steerPower * speedRatio * dt;
turnSign = 1;
}
// Apply curve centrifugal force
playerX -= (currentSegment.curve * speedRatio * speedRatio * 0.8 * dt);
// Acceleration / Braking
if (keys.Up) {
speed += accel * dt;
} else if (keys.Down) {
speed -= braking * dt;
} else {
speed -= decel * dt;
}
// Off-road Penalty
if ((playerX < -1.1 || playerX > 1.1)) {
if (speed > offroadMaxSpeed) {
speed -= offroadDecel * dt;
}
}
// Clamp speed and bounds
speed = Math.max(0, Math.min(speed, maxSpeed));
playerX = Math.max(-2.5, Math.min(2.5, playerX));
// Move Player Forward
playerZ += speed * dt;
while (playerZ >= TRACK_LENGTH * SEGMENT_LENGTH) {
playerZ -= TRACK_LENGTH * SEGMENT_LENGTH;
}
// Accumulate Distance Score
if (speed > 0) {
score += Math.floor((speed * dt) / 100);
}
// UI Updates
speedEl.innerText = Math.floor((speed / maxSpeed) * 210); // Display simulated km/h
scoreEl.innerText = score;
}
function render() {
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Render Sky / Background Horizon
let skyGradient = ctx.createLinearGradient(0, 0, 0, CANVAS_HEIGHT / 2);
skyGradient.addColorStop(0, '#1a0d00');
skyGradient.addColorStop(1, '#663300');
ctx.fillStyle = skyGradient;
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT / 2);
// Draw Distance Segments
let baseSegment = findSegment(playerZ);
let cameraX = playerX * ROAD_WIDTH;
let cameraY = 1000; // Camera height above ground
let cameraZ = playerZ;
let dx = 0;
let x = 0;
let maxy = CANVAS_HEIGHT;
for (let n = 0; n < DRAW_DISTANCE; n++) {
let segment = segments[(baseSegment.index + n) % segments.length];
// Loop track logic for projection calculations
let loopOffset = (segment.index < baseSegment.index) ? TRACK_LENGTH * SEGMENT_LENGTH : 0;
segment.p1.world.x = -x;
segment.p1.world.y = 0;
segment.p1.world.z = (segment.index * SEGMENT_LENGTH) + loopOffset;
segment.p2.world.x = -x - dx;
segment.p2.world.y = 0;
segment.p2.world.z = ((segment.index + 1) * SEGMENT_LENGTH) + loopOffset;
dx += segment.curve;
x += dx;
project(segment.p1, cameraX, cameraY, cameraZ);
project(segment.p2, cameraX, cameraY, cameraZ);
// Cull segments behind camera or occluded
if (segment.p1.camera.z <= CAM_DEPTH || segment.p2.screen.y >= maxy) {
continue;
}
renderSegment(
ctx,
CANVAS_WIDTH,
3,
segment.p1.screen.x,
segment.p1.screen.y,
segment.p1.screen.w,
segment.p2.screen.x,
segment.p2.screen.y,
segment.p2.screen.w,
segment.color
);
maxy = segment.p1.screen.y;
}
// Draw Player
let turnSign = keys.Left ? -1 : (keys.Right ? 1 : 0);
renderPlayer(ctx, speed / maxSpeed, turnSign, keys.Space);
}
function gameLoop(now) {
let dt = Math.min(0.1, (now - lastTime) / 1000);
lastTime = now;
update(dt);
render();
requestAnimationFrame(gameLoop);
}
// Initialization
buildTrack();
requestAnimationFrame(gameLoop);
</script>
</body>
</html>1
1
12KB
12KB
96.0ms
208.0ms
96.0ms