Rally
Qwen 3.6 27b MTP · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 95,992 bytes · SHA-256 d02fe9073d9d
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rally Racing 3D</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', Arial, sans-serif; user-select: none; -webkit-user-select: none; }
#gameContainer { position: relative; width: 100vw; height: 100vh; }
#gameCanvas { display: block; width: 100%; height: 100%; }
/* HUD */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#speedometer { position: absolute; bottom: 25px; right: 25px; text-align: center; color: #fff; background: rgba(0,0,0,0.65); border-radius: 14px; padding: 10px 20px; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.1); }
#speedValue { font-size: 52px; font-weight: bold; line-height: 1; text-shadow: 0 0 15px rgba(255,200,0,0.6); letter-spacing: -2px; }
#speedUnit { font-size: 13px; color: #999; margin-top: 2px; }
#gearDisplay { position: absolute; bottom: 85px; right: 40px; font-size: 36px; font-weight: bold; color: #ffcc00; background: rgba(0,0,0,0.6); border-radius: 10px; padding: 6px 14px; text-shadow: 0 0 8px rgba(255,204,0,0.4); }
#rpmBar { position: absolute; bottom: 79px; right: 95px; width: 130px; height: 14px; background: rgba(0,0,0,0.6); border-radius: 7px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); }
#rpmFill { height: 100%; width: 0%; background: linear-gradient(to right, #0f0, #ff0, #f00); transition: width 0.05s; border-radius: 7px; }
#lapInfo { position: absolute; top: 20px; left: 20px; color: #fff; background: rgba(0,0,0,0.65); border-radius: 14px; padding: 14px 20px; line-height: 1.7; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.1); }
#currentLap { font-size: 22px; font-weight: bold; color: #ffcc00; text-shadow: 0 0 8px rgba(255,204,0,0.3); }
#currentTime { font-size: 18px; font-variant-numeric: tabular-nums; }
#bestLap { font-size: 14px; color: #aaa; margin-top: 2px; }
#minimap { position: absolute; top: 20px; right: 20px; border-radius: 14px; background: rgba(0,0,0,0.5); border: 2px solid rgba(255,255,255,0.2); }
/* Surface indicator */
#surfaceIndicator { position: absolute; bottom: 85px; left: 20px; color: #aaa; font-size: 14px; background: rgba(0,0,0,0.6); border-radius: 8px; padding: 6px 12px; backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); border: 1px solid rgba(255,255,255,0.1); }
/* Countdown */
#countdown { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 20; pointer-events: none; background: rgba(0,0,0,0.3); }
#countdownText { font-size: 150px; font-weight: bold; color: #fff; text-shadow: 0 0 40px rgba(255,200,0,0.8), 0 0 80px rgba(255,100,0,0.4); animation: pulse 0.5s ease-in-out infinite alternate; }
@keyframes pulse { from { transform: scale(1); opacity: 1; } to { transform: scale(1.1); opacity: 0.9; } }
/* Pause Menu */
#pauseMenu { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.75); z-index: 30; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
#pauseMenu h2 { color: #fff; font-size: 52px; margin-bottom: 30px; text-shadow: 0 0 20px rgba(255,200,0,0.5); letter-spacing: 4px; }
#pauseMenu button { pointer-events: auto; padding: 14px 45px; font-size: 20px; margin: 8px; border: none; border-radius: 10px; cursor: pointer; background: #ffcc00; color: #333; font-weight: bold; transition: all 0.2s; }
#pauseMenu button:hover { background: #ffe066; transform: scale(1.05); }
/* Finish Screen */
#finishScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.85); z-index: 30; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
#finishScreen h1 { color: #ffcc00; font-size: 56px; margin-bottom: 25px; text-shadow: 0 0 30px rgba(255,200,0,0.6); letter-spacing: 2px; }
#finalTime, #finalBestLap { color: #fff; font-size: 30px; margin: 10px; text-shadow: 0 0 10px rgba(255,255,255,0.3); }
#finishScreen button { pointer-events: auto; padding: 16px 55px; font-size: 24px; margin-top: 35px; border: none; border-radius: 10px; cursor: pointer; background: #ffcc00; color: #333; font-weight: bold; transition: all 0.2s; }
#finishScreen button:hover { background: #ffe066; transform: scale(1.05); }
/* Touch Controls */
#touchControls { position: absolute; bottom: 0; left: 0; width: 100%; height: 200px; z-index: 15; pointer-events: none; display: flex; justify-content: space-between; padding: 10px 20px; }
#dpad { position: relative; width: 140px; height: 140px; pointer-events: auto; }
#dpad button { position: absolute; width: 45px; height: 45px; background: rgba(255,255,255,0.25); border: 2px solid rgba(255,255,255,0.4); border-radius: 10px; color: #fff; font-size: 18px; pointer-events: auto; -webkit-tap-highlight-color: transparent; }
#dpad button:active { background: rgba(255,255,255,0.5); }
#btnUp { top: 0; left: 47px; }
#btnDown { bottom: 0; left: 47px; }
#btnLeft { top: 47px; left: 0; }
#btnRight { top: 47px; right: 0; }
#actionButtons { display: flex; gap: 12px; align-items: flex-end; pointer-events: auto; }
#actionButtons button { width: 65px; height: 65px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.4); font-size: 13px; font-weight: bold; pointer-events: auto; -webkit-tap-highlight-color: transparent; }
#actionButtons button:active { transform: scale(0.9); }
#btnGas { background: rgba(0,200,0,0.4); color: #fff; }
#btnBrake { background: rgba(200,0,0,0.4); color: #fff; }
#btnHandbrake { background: rgba(255,165,0,0.4); color: #fff; }
/* Controls hint */
#controlsHint { position: absolute; bottom: 85px; right: 20px; color: rgba(255,255,255,0.4); font-size: 11px; text-align: right; line-height: 1.6; }
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas"></canvas>
<!-- HUD Overlay -->
<div id="hud" style="display:none;">
<div id="speedometer">
<div id="speedValue">0</div>
<div id="speedUnit">km/h</div>
</div>
<div id="gearDisplay">N</div>
<div id="rpmBar"><div id="rpmFill"></div></div>
<div id="lapInfo">
<div id="currentLap">Lap 1/3</div>
<div id="currentTime">00:00.000</div>
<div id="bestLap">Best: --:--.---</div>
</div>
<canvas id="minimap" width="180" height="180"></canvas>
<div id="surfaceIndicator">ASPHALT</div><div id="controlsHint">WASD/Arrows: Drive<br>Space: Handbrake | C: Camera<br>P: Pause | R: Restart</div></div>
<!-- Countdown Overlay -->
<div id="countdown" style="display:none;">
<div id="countdownText">3</div>
</div>
<!-- Pause Menu -->
<div id="pauseMenu" style="display:none;">
<h2>PAUSED</h2>
<button id="resumeBtn">Resume</button>
<button id="restartPauseBtn">Restart</button>
</div>
<!-- Finish Screen -->
<div id="finishScreen" style="display:none;">
<h1>RACE COMPLETE!</h1>
<div id="finalTime"></div>
<div id="finalBestLap"></div>
<button id="restartFinishBtn">Race Again</button>
</div>
<!-- Touch Controls -->
<div id="touchControls" style="display:none;">
<div id="dpad">
<button id="btnUp">▲</button>
<button id="btnLeft">◄</button>
<button id="btnRight">►</button>
<button id="btnDown">▼</button>
</div>
<div id="actionButtons">
<button id="btnGas">GAS</button>
<button id="btnBrake">BRAKE</button>
<button id="btnHandbrake">HB</button>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./input":"src/input.ts","./audio":"src/audio.ts","./track":"src/track.ts","./car":"src/car.ts","./camera":"src/camera.ts","./scenery":"src/scenery.ts","./particles":"src/particles.ts","./hud":"src/hud.ts","./gameState":"src/gameState.ts","./types":"src/types.ts"},"src/input.ts":{"./types":"src/types.ts"},"src/track.ts":{"./types":"src/types.ts"},"src/car.ts":{"./types":"src/types.ts"},"src/scenery.ts":{"./types":"src/types.ts"},"src/hud.ts":{"./types":"src/types.ts"},"src/gameState.ts":{"./types":"src/types.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// ============================================================
// Main entry point - Rally Racing 3D
// Initializes Three.js scene, game systems, and runs the game loop
// ============================================================
const input_1 = require("./input");
const audio_1 = require("./audio");
const track_1 = require("./track");
const car_1 = require("./car");
const camera_1 = require("./camera");
const scenery_1 = require("./scenery");
const particles_1 = require("./particles");
const hud_1 = require("./hud");
const gameState_1 = require("./gameState");
const types_1 = require("./types");
// ============================================================
// Global game object (avoids namespace pollution)
// ============================================================
const RallyGame = {
// Three.js core
scene: null,
renderer: null,
clock: new THREE.Clock(),
// Systems
input: new input_1.InputManager(),
audio: new audio_1.AudioManager(),
trackGen: new track_1.TrackGenerator(),
car: new car_1.CarSystem(),
camera: new camera_1.CameraSystem(),
sceneryGen: new scenery_1.SceneryGenerator(),
particles: new particles_1.ParticleSystem(),
hud: new hud_1.HUDManager(),
gameMgr: new gameState_1.GameManager(),
// Scene objects
groundMesh: null,
trackMeshes: [],
skidMarkMeshes: [],
// Track data for minimap
trackPositions: [],
_lastCountdownVal: '',
_skidMarkTimer: 0,
init() {
// Initialize Three.js scene
RallyGame.scene = new THREE.Scene();
// Sky color and fog
const skyColor = 0x87CEEB;
RallyGame.scene.background = new THREE.Color(skyColor);
RallyGame.scene.fog = new THREE.Fog(0xc4d8e2, 60, 350);
// Renderer
const canvas = document.getElementById('gameCanvas');
RallyGame.renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
RallyGame.renderer.setSize(window.innerWidth, window.innerHeight);
RallyGame.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Lighting
const ambientLight = new THREE.AmbientLight(0x6688aa, 0.5);
RallyGame.scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x445522, 0.4);
RallyGame.scene.add(hemiLight);
const sunLight = new THREE.DirectionalLight(0xfff5e6, 1.0);
sunLight.position.set(50, 80, 30);
sunLight.castShadow = true;
RallyGame.scene.add(sunLight);
// Setup shadow map
const shadowCam = sunLight.shadow.camera;
shadowCam.left = -60;
shadowCam.right = 60;
shadowCam.top = 60;
shadowCam.bottom = -60;
shadowCam.near = 1;
shadowCam.far = 200;
sunLight.shadow.mapSize.x = 1024;
sunLight.shadow.mapSize.y = 1024;
// Generate track
RallyGame.trackGen.generate();
const trackPoints = RallyGame.trackGen.getPoints();
const surfaces = RallyGame.trackGen.getSurfaces();
// Build ground plane (large terrain)
RallyGame.buildGround(trackPoints);
// Build track surface
RallyGame.buildTrackSurface(trackPoints, surfaces);
// Store track positions for minimap
RallyGame.trackPositions = trackPoints.map(p => new THREE.Vector3(p.x, p.y, p.z));
// Place car at start position
const startPoint = trackPoints[0];
RallyGame.car.reset(new THREE.Vector3(startPoint.x, startPoint.y + 1.5, startPoint.z), startPoint.heading);
// Build and add car mesh to scene
const carMesh = RallyGame.car.buildMesh();
RallyGame.scene.add(carMesh);
// Generate scenery (trees, rocks, signs)
RallyGame.sceneryGen.generate(trackPoints, RallyGame.scene);
// Initialize particle system
RallyGame.particles.init(RallyGame.scene);
// Add camera to scene
RallyGame.scene.add(RallyGame.camera.getCamera());
// Setup HUD
RallyGame.hud.init();
RallyGame.hud.setState(types_1.GameState.COUNTDOWN);
// Wire up input callbacks
RallyGame.input.setOnRestart(() => RallyGame.restartRace());
RallyGame.input.setOnPause(() => {
RallyGame.gameMgr.togglePause();
RallyGame.hud.setState(RallyGame.gameMgr.state);
});
RallyGame.input.setOnToggleCamera(() => RallyGame.camera.toggleMode());
// Wire up HUD callbacks
RallyGame.hud.setOnResume(() => {
RallyGame.gameMgr.togglePause();
RallyGame.hud.setState(RallyGame.gameMgr.state);
});
RallyGame.hud.setOnRestart(() => RallyGame.restartRace());
// Handle window resize
window.addEventListener('resize', () => {
if (RallyGame.renderer) {
RallyGame.renderer.setSize(window.innerWidth, window.innerHeight);
}
RallyGame.camera.resize(window.innerWidth, window.innerHeight);
});
// Resume audio on first user interaction
const resumeAudio = () => {
RallyGame.audio.init();
RallyGame.audio.resume();
window.removeEventListener('click', resumeAudio);
window.removeEventListener('keydown', resumeAudio);
window.removeEventListener('touchstart', resumeAudio);
};
window.addEventListener('click', resumeAudio);
window.addEventListener('keydown', resumeAudio);
window.addEventListener('touchstart', resumeAudio);
// Start game loop
RallyGame.gameLoop();
},
buildGround(trackPoints) {
const size = 600;
const segments = 40;
const geo = new THREE.PlaneGeometry(size, size, segments, segments);
// Add slight height variation to vertices for terrain feel
const positions = geo.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const z = positions[i + 1];
positions[i + 2] = Math.sin(x * 0.02) * Math.cos(z * 0.02) * 3;
}
const mat = new THREE.MeshStandardMaterial({
color: 0x4a7c3f, roughness: 0.95, flatShading: true
});
const ground = new THREE.Mesh(geo, mat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.1;
ground.receiveShadow = true;
RallyGame.scene.add(ground);
RallyGame.groundMesh = ground;
},
buildTrackSurface(trackPoints, surfaces) {
const trackWidth = 12;
const halfW = trackWidth / 2;
for (let i = 0; i < trackPoints.length - 1; i++) {
const p1 = trackPoints[i];
const p2 = trackPoints[i + 1];
// Calculate perpendicular direction for track width
const dx = p2.x - p1.x;
const dz = p2.z - p1.z;
const len = Math.sqrt(dx * dx + dz * dz) || 0.001;
const nx = -dz / len;
const nz = dx / len;
// Surface color based on type
let surfaceColor;
switch (surfaces[i]) {
case types_1.SurfaceType.ASPHALT:
surfaceColor = 0x444444;
break;
case types_1.SurfaceType.DIRT:
surfaceColor = 0x8B7355;
break;
case types_1.SurfaceType.GRASS:
surfaceColor = 0x5a8c3f;
break;
case types_1.SurfaceType.MUD:
surfaceColor = 0x5c4033;
break;
default: surfaceColor = 0x444444;
}
// Create track segment as a quad
const segLen = Math.sqrt((p2.x - p1.x) ** 2 + (p2.z - p1.z) ** 2) + 0.5;
const segGeo = new THREE.PlaneGeometry(segLen, trackWidth);
const segMat = new THREE.MeshStandardMaterial({
color: surfaceColor, roughness: surfaces[i] === types_1.SurfaceType.ASPHALT ? 0.7 : 0.95, flatShading: true
});
const segment = new THREE.Mesh(segGeo, segMat);
const midX = (p1.x + p2.x) / 2;
const midY = (p1.y + p2.y) / 2 + 0.05;
const midZ = (p1.z + p2.z) / 2;
segment.position.set(midX, midY, midZ);
segment.rotation.x = -Math.PI / 2;
segment.rotation.z = -p1.heading;
segment.receiveShadow = true;
RallyGame.scene.add(segment);
RallyGame.trackMeshes.push(segment);
// Track edge lines for asphalt
if (surfaces[i] === types_1.SurfaceType.ASPHALT && i % 3 === 0) {
for (const side of [-1, 1]) {
const edgeGeo = new THREE.PlaneGeometry(segLen + 0.5, 0.3);
const edgeMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
const edge = new THREE.Mesh(edgeGeo, edgeMat);
edge.position.set(midX + nx * halfW * side, midY + 0.06, midZ + nz * halfW * side);
edge.rotation.x = -Math.PI / 2;
edge.rotation.z = -p1.heading;
RallyGame.scene.add(edge);
}
}
// Start/finish line (checkerboard)
if (i === 0) {
const checkCanvas = document.createElement('canvas');
checkCanvas.width = 64;
checkCanvas.height = 64;
const ctx = checkCanvas.getContext('2d');
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
ctx.fillStyle = (r + c) % 2 === 0 ? '#ffffff' : '#111111';
ctx.fillRect(c * 8, r * 8, 8, 8);
}
}
const checkTex = new THREE.CanvasTexture(checkCanvas);
const startGeo = new THREE.PlaneGeometry(3, trackWidth);
const startMat = new THREE.MeshStandardMaterial({ map: checkTex });
const startLine = new THREE.Mesh(startGeo, startMat);
startLine.position.set(p1.x, p1.y + 0.07, p1.z);
startLine.rotation.x = -Math.PI / 2;
startLine.rotation.z = -p1.heading;
RallyGame.scene.add(startLine);
}
}
// Track barrier posts along edges
for (let i = 0; i < trackPoints.length; i += 5) {
const p = trackPoints[i];
const nextP = trackPoints[(i + 1) % trackPoints.length];
const bdx = nextP.x - p.x;
const bdz = nextP.z - p.z;
const blen = Math.sqrt(bdx * bdx + bdz * bdz) || 0.001;
const bnx = -bdz / blen;
const bnz = bdx / blen;
for (const side of [-1, 1]) {
const postGeo = new THREE.CylinderGeometry(0.15, 0.15, 1.2, 6);
const isRed = i % 10 === 0;
const postMat = new THREE.MeshStandardMaterial({
color: isRed ? 0xff3333 : 0xffffff, flatShading: true
});
const post = new THREE.Mesh(postGeo, postMat);
post.position.set(p.x + bnx * halfW * side * 1.2, p.y + 0.6, p.z + bnz * halfW * side * 1.2);
RallyGame.scene.add(post);
}
}
},
restartRace() {
const trackPoints = RallyGame.trackGen.getPoints();
const startPoint = trackPoints[0];
RallyGame.gameMgr.reset();
RallyGame.car.reset(new THREE.Vector3(startPoint.x, startPoint.y + 1.5, startPoint.z), startPoint.heading);
// Clear skid marks
for (const mesh of RallyGame.skidMarkMeshes) {
RallyGame.scene.remove(mesh);
}
RallyGame.skidMarkMeshes = [];
RallyGame.hud.setState(types_1.GameState.COUNTDOWN);
},
gameLoop() {
requestAnimationFrame(RallyGame.gameLoop);
const dt = Math.min(RallyGame.clock.getDelta(), 0.05);
const state = RallyGame.gameMgr.state;
// Update countdown
if (state === types_1.GameState.COUNTDOWN) {
const justStarted = RallyGame.gameMgr.updateCountdown(dt);
RallyGame.hud.showCountdown(RallyGame.gameMgr.getCountdownValue());
if (justStarted) {
RallyGame.audio.playGo();
RallyGame.hud.setState(types_1.GameState.RACING);
}
else {
const val = RallyGame.gameMgr.getCountdownValue();
if (val !== RallyGame._lastCountdownVal) {
if (val === '3' || val === '2' || val === '1') {
RallyGame.audio.playBeep(440 + parseInt(val) * 100, 0.15);
}
RallyGame._lastCountdownVal = val;
}
}
RallyGame.render();
return;
}
if (state === types_1.GameState.PAUSED || state === types_1.GameState.FINISHED) {
RallyGame.render();
return;
}
// ---- RACING STATE ----
const input = RallyGame.input.state;
const carState = RallyGame.car.state;
// Determine surface and track status
const onTrack = RallyGame.trackGen.isOnTrack(carState.position.x, carState.position.z);
const surface = RallyGame.trackGen.getSurfaceAt(carState.position.x, carState.position.z);
// Update car physics
RallyGame.car.update(dt, input, surface, onTrack);
// Check scenery collisions
const collision = RallyGame.sceneryGen.checkCollision(carState.position);
if (collision) {
RallyGame.car.applyBounce(collision.normal, 0.4);
RallyGame.audio.playCrash();
RallyGame.particles.emitSparks(carState.position.x, carState.position.y, carState.position.z);
}
// Clamp car to reasonable bounds
const trackElevation = RallyGame.trackGen.getElevationAt(carState.position.x, carState.position.z);
if (carState.position.y < trackElevation + 0.5) {
carState.position.y = trackElevation + 0.5;
}
// Checkpoint validation
const checkpoints = RallyGame.trackGen.getCheckpoints();
for (let i = 0; i < checkpoints.length; i++) {
const cpIdx = checkpoints[i];
const cpPoint = RallyGame.trackGen.getPosition(cpIdx);
const cdx = carState.position.x - cpPoint.x;
const cdz = carState.position.z - cpPoint.z;
if (Math.sqrt(cdx * cdx + cdz * cdz) < 15) {
RallyGame.gameMgr.checkCheckpoint(i, checkpoints.length);
}
}
// Particle effects for drifting/off-road
if (carState.isDrifting || !onTrack) {
const intensity = carState.isDrifting ? 2 : 0.8;
RallyGame.particles.emitDust(carState.position.x, carState.position.y - 0.3, carState.position.z, intensity * Math.abs(carState.speed) / 15);
// Add skid marks when drifting
if (carState.isDrifting && RallyGame._skidMarkTimer <= 0) {
const heading = carState.heading;
for (const side of [-0.8, 0.8]) {
const sx = carState.position.x + Math.cos(heading) * side;
const sz = carState.position.z - Math.sin(heading) * side;
RallyGame.particles.addSkidMark(sx, trackElevation, sz);
const skidGeo = new THREE.PlaneGeometry(0.3, 1.5);
const skidMat = new THREE.MeshStandardMaterial({
color: 0x222222, transparent: true, opacity: 0.4
});
const skidMesh = new THREE.Mesh(skidGeo, skidMat);
skidMesh.position.set(sx, trackElevation + 0.03, sz);
skidMesh.rotation.x = -Math.PI / 2;
skidMesh.rotation.z = heading;
RallyGame.scene.add(skidMesh);
RallyGame.skidMarkMeshes.push(skidMesh);
if (RallyGame.skidMarkMeshes.length > 300) {
const old = RallyGame.skidMarkMeshes.shift();
RallyGame.scene.remove(old);
}
}
}
}
RallyGame._skidMarkTimer -= dt;
// Update audio
RallyGame.audio.updateEngine(carState.speed, carState.gear);
RallyGame.audio.updateSkid(carState.isDrifting, !onTrack);
// Update particles
RallyGame.particles.update(dt);
// Update camera
RallyGame.camera.update(carState.position, carState.heading, dt);
// Update HUD
RallyGame.hud.updateSpeed(carState.speed);
RallyGame.hud.updateGear(carState.gear);
RallyGame.hud.updateRPM(carState.rpm);
RallyGame.hud.updateLapInfo(RallyGame.gameMgr.getCurrentLap(), RallyGame.gameMgr.getCurrentTimeMs(), RallyGame.gameMgr.getBestLapMs());
RallyGame.hud.updateSurface(carState.currentSurface, carState.onTrack);
// Draw minimap
RallyGame.hud.drawMinimap(RallyGame.trackPositions, carState.position, RallyGame.gameMgr.getLastCheckpointIndex(), carState.heading);
// Check for race finish
if (RallyGame.gameMgr.state === types_1.GameState.FINISHED) {
RallyGame.audio.playFinish();
RallyGame.hud.showFinishScreen(RallyGame.gameMgr.getFinalTimeMs(), RallyGame.gameMgr.getBestLapMs());
RallyGame.hud.setState(types_1.GameState.FINISHED);
}
// Render the scene
RallyGame.render();
},
render() {
if (!RallyGame.renderer || !RallyGame.scene)
return;
const camera = RallyGame.camera.getCamera();
RallyGame.renderer.render(RallyGame.scene, camera);
}
};
// Start the game when DOM is ready
RallyGame.init();
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// Type definitions for the rally racing game
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.TOTAL_LAPS = exports.GEAR_RANGES = exports.CAR_PHYSICS = exports.GameState = exports.SurfaceType = void 0;
var SurfaceType;
(function (SurfaceType) {
SurfaceType["ASPHALT"] = "asphalt";
SurfaceType["DIRT"] = "dirt";
SurfaceType["GRASS"] = "grass";
SurfaceType["MUD"] = "mud"; // Very slow, very low grip
})(SurfaceType || (exports.SurfaceType = SurfaceType = {}));
var GameState;
(function (GameState) {
GameState["COUNTDOWN"] = "countdown";
GameState["RACING"] = "racing";
GameState["PAUSED"] = "paused";
GameState["FINISHED"] = "finished";
})(GameState || (exports.GameState = GameState = {}));
// Car physics constants
exports.CAR_PHYSICS = {
maxSpeed: 45, // m/s (~162 km/h) on asphalt
acceleration: 18, // m/s²
brakingForce: 25, // m/s²
reverseMaxSpeed: -10, // m/s
steeringSensitivity: 2.5,
driftThreshold: 0.6, // Speed ratio at which drifting begins
gripAsphalt: 0.92,
gripDirt: 0.7,
gripGrass: 0.45,
gripMud: 0.3,
speedMultiplierAsphalt: 1.0,
speedMultiplierDirt: 0.8,
speedMultiplierGrass: 0.6,
speedMultiplierMud: 0.4,
handbrakeGripLoss: 0.3,
driftAngleDecay: 2.5,
};
exports.GEAR_RANGES = [
{ min: 0, max: 12, ratio: 3.5 }, // Gear 1
{ min: 8, max: 24, ratio: 2.8 }, // Gear 2
{ min: 18, max: 36, ratio: 2.2 }, // Gear 3
{ min: 28, max: 50, ratio: 1.7 }, // Gear 4
{ min: 40, max: 999, ratio: 1.3 }, // Gear 5
];
exports.TOTAL_LAPS = 3;
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputManager = void 0;
class InputManager {
get state() { return this._state; }
constructor() {
this._state = {
throttle: false, brake: false, reverse: false,
left: false, right: false, handbrake: false
};
// Callbacks set by main game loop
this._onRestart = () => { };
this._onPause = () => { };
this._onToggleCamera = () => { };
this.setupKeyboard();
this.setupTouch();
}
setupKeyboard() {
const keyMap = {
'ArrowUp': 'throttle', 'KeyW': 'throttle',
'ArrowDown': 'reverse', 'KeyS': 'reverse',
'ArrowLeft': 'left', 'KeyA': 'left',
'ArrowRight': 'right', 'KeyD': 'right',
'Space': 'handbrake', 'KeyB': 'handbrake'
};
window.addEventListener('keydown', (e) => {
const key = e.code;
if (key === 'KeyR') {
this._onRestart();
return;
}
if (key === 'KeyP') {
this._onPause();
return;
}
if (key === 'KeyC') {
this._onToggleCamera();
return;
}
const action = keyMap[key];
if (action) {
e.preventDefault();
this._state[action] = true;
}
});
window.addEventListener('keyup', (e) => {
const key = e.code;
const action = keyMap[key];
if (action) {
e.preventDefault();
this._state[action] = false;
}
});
}
setupTouch() {
// Only show touch controls on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (!isTouchDevice)
return;
const touchEl = document.getElementById('touchControls');
if (touchEl)
touchEl.style.display = 'flex';
const bindBtn = (id, action) => {
const btn = document.getElementById(id);
if (!btn)
return;
btn.addEventListener('touchstart', (e) => { e.preventDefault(); this._state[action] = true; });
btn.addEventListener('touchend', (e) => { e.preventDefault(); this._state[action] = false; });
btn.addEventListener('mousedown', () => { this._state[action] = true; });
btn.addEventListener('mouseup', () => { this._state[action] = false; });
};
bindBtn('btnUp', 'throttle');
bindBtn('btnDown', 'reverse');
bindBtn('btnLeft', 'left');
bindBtn('btnRight', 'right');
bindBtn('btnGas', 'throttle');
bindBtn('btnBrake', 'brake');
bindBtn('btnHandbrake', 'handbrake');
}
setOnRestart(fn) { this._onRestart = fn; }
setOnPause(fn) { this._onPause = fn; }
setOnToggleCamera(fn) { this._onToggleCamera = fn; }
// Reset all inputs
reset() {
Object.keys(this._state).forEach(k => {
this._state[k] = false;
});
}
}
exports.InputManager = InputManager;
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// Audio synthesis using Web Audio API
// Engine sounds, countdown beeps, crash sounds, skid sounds
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioManager = void 0;
class AudioManager {
constructor() {
this.ctx = null;
this.engineOsc = null;
this.engineGain = null;
this.noiseSource = null;
this.noiseGain = null;
this.masterGain = null;
this.initialized = false;
}
init() {
if (this.initialized)
return;
try {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.5;
this.masterGain.connect(this.ctx.destination);
// Engine oscillator - low rumble
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 60;
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0;
this.engineOsc.connect(this.engineGain);
this.engineGain.connect(this.masterGain);
this.engineOsc.start();
// Noise for skid/gravel sounds
const bufferSize = this.ctx.sampleRate * 2;
const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
// Create a filter for the noise to make it sound like gravel/skid
const noiseFilter = this.ctx.createBiquadFilter();
noiseFilter.type = 'bandpass';
noiseFilter.frequency.value = 3000;
noiseFilter.Q.value = 0.5;
this.noiseSource = this.ctx.createBufferSource();
this.noiseSource.buffer = noiseBuffer;
this.noiseSource.loop = true;
this.noiseGain = this.ctx.createGain();
this.noiseGain.gain.value = 0;
this.noiseSource.connect(noiseFilter);
noiseFilter.connect(this.noiseGain);
this.noiseGain.connect(this.masterGain);
this.noiseSource.start();
this.initialized = true;
}
catch (e) {
console.warn('Audio init failed:', e);
}
}
// Update engine sound based on speed and gear
updateEngine(speed, gear) {
if (!this.ctx || !this.engineOsc || !this.engineGain)
return;
const rpm = Math.max(60, 80 + Math.abs(speed) * 15);
this.engineOsc.frequency.value = rpm;
const volume = Math.min(0.3, 0.02 + Math.abs(speed) / 200);
this.engineGain.gain.setTargetAtTime(volume, this.ctx.currentTime, 0.05);
}
// Update skid/gravel noise based on drifting and surface
updateSkid(isDrifting, offTrack) {
if (!this.noiseGain)
return;
let vol = 0;
if (isDrifting)
vol = Math.max(vol, 0.15);
if (offTrack)
vol = Math.max(vol, 0.1);
this.noiseGain.gain.setTargetAtTime(vol, this.ctx.currentTime, 0.1);
}
// Play countdown beep
playBeep(freq = 440, duration = 0.2) {
if (!this.ctx)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.3, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + duration);
}
// Play GO sound (higher pitch)
playGo() {
if (!this.ctx)
return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.value = 880;
gain.gain.setValueAtTime(0.4, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.5);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.5);
}
// Play crash sound
playCrash() {
if (!this.ctx)
return;
const bufferSize = this.ctx.sampleRate * 0.3;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.1));
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const gain = this.ctx.createGain();
gain.gain.value = 0.4;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 800;
source.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
source.start();
}
// Play finish fanfare
playFinish() {
if (!this.ctx)
return;
const notes = [523, 659, 784, 1047]; // C E G C
notes.forEach((freq, i) => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.value = freq;
const t = this.ctx.currentTime + i * 0.2;
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(0.3, t + 0.05);
gain.gain.exponentialRampToValueAtTime(0.01, t + 0.4);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start(t);
osc.stop(t + 0.5);
});
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
}
exports.AudioManager = AudioManager;
};
// ── module: src/track.ts ──
__mods["src/track.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TrackGenerator = void 0;
// ============================================================
// Procedural track generation
// Creates a closed-loop rally track with curves, hills, dips
// and varied surface types
// ============================================================
const types_1 = require("./types");
class TrackGenerator {
constructor() {
this.points = [];
this.surfaces = [];
this.checkpoints = []; // indices of checkpoint positions
}
generate() {
this.points = [];
this.surfaces = [];
// Define track as a series of control points forming an interesting rally loop
// Using polar coordinates for smoother curves
const numControls = 16;
const controls = [];
// Create an oval-ish track with interesting features
for (let i = 0; i < numControls; i++) {
const angle = (i / numControls) * Math.PI * 2;
// Varying radius creates curves
let radius = 120 + Math.sin(angle * 3) * 40 + Math.cos(angle * 2) * 30;
controls.push({
x: Math.cos(angle) * radius,
z: -Math.sin(angle) * radius // Negative for proper orientation
});
}
const numSegments = 80; // Points per control segment (total ~1280 points)
for (let i = 0; i < controls.length; i++) {
const curr = controls[i];
const next = controls[(i + 1) % controls.length];
const prev = controls[(i - 1 + controls.length) % controls.length];
// Calculate tangent using Catmull-Rom style interpolation
const dx = next.x - prev.x;
const dz = next.z - prev.z;
for (let j = 0; j < numSegments; j++) {
const t = j / numSegments;
// Smooth easing for natural curves
const eased = t * t * (3 - 2 * t);
// Quadratic bezier-like interpolation for smoothness
const midX = (curr.x + next.x) / 2;
const midZ = (curr.z + next.z) / 2;
const x = (1 - eased) * curr.x + eased * next.x + Math.sin(eased * Math.PI) * (midX - ((1 - eased) * curr.x + eased * next.x)) * 0.3;
const z = (1 - eased) * curr.z + eased * next.z + Math.sin(eased * Math.PI) * (midZ - ((1 - eased) * curr.z + eased * next.z)) * 0.3;
// Elevation: hills and dips based on position along track
let y = 0;
const trackT = (i / controls.length);
// Hill section (around control points 4-6)
if (trackT > 0.25 && trackT < 0.4) {
const hillT = (trackT - 0.25) / 0.15;
y = Math.sin(hillT * Math.PI) * 10;
}
// Valley section (around control points 8-9)
else if (trackT > 0.5 && trackT < 0.6) {
const valleyT = (trackT - 0.5) / 0.1;
y = -Math.sin(valleyT * Math.PI) * 5;
}
// Rolling hills section
else if (trackT > 0.7 && trackT < 0.85) {
const rollT = (trackT - 0.7) / 0.15;
y = Math.sin(rollT * Math.PI * 3) * 3;
}
// Add subtle terrain variation
y += Math.sin(x * 0.03 + z * 0.02) * 1.0;
// Calculate heading (direction along track)
const lookAhead = 5;
const nextIdx = ((i * numSegments + j + lookAhead) % (controls.length * numSegments));
const futureT = Math.min(1, lookAhead / numSegments);
const futureX = curr.x + dx * futureT;
const futureZ = curr.z + dz * futureT;
const heading = Math.atan2(futureX - x, futureZ - z);
this.points.push({ x, y, z, heading });
}
}
// Assign surface types to segments based on track position
for (let i = 0; i < this.points.length; i++) {
const t = i / this.points.length;
if (t < 0.12)
this.surfaces[i] = types_1.SurfaceType.ASPHALT; // Start straight
else if (t < 0.25)
this.surfaces[i] = types_1.SurfaceType.DIRT; // Dirt section with curves
else if (t < 0.38)
this.surfaces[i] = types_1.SurfaceType.ASPHALT; // Fast hill climb
else if (t < 0.48)
this.surfaces[i] = types_1.SurfaceType.GRASS; // Grass field shortcut area
else if (t < 0.58)
this.surfaces[i] = types_1.SurfaceType.MUD; // Mud puddle section
else if (t < 0.72)
this.surfaces[i] = types_1.SurfaceType.DIRT; // Dirt return with rolling hills
else if (t < 0.85)
this.surfaces[i] = types_1.SurfaceType.ASPHALT; // Fast approach
else
this.surfaces[i] = types_1.SurfaceType.GRASS; // Final grass section before finish
}
// Set checkpoints at regular intervals around the track
const numCheckpoints = 8;
for (let i = 0; i < numCheckpoints; i++) {
this.checkpoints.push(Math.floor((i / numCheckpoints) * this.points.length));
}
}
getPoints() { return this.points; }
getSurfaces() { return this.surfaces; }
getCheckpoints() { return this.checkpoints; }
getLength() { return this.points.length; }
// Get track position at a given index (with wrapping)
getPosition(index) {
const i = ((index % this.points.length) + this.points.length) % this.points.length;
return this.points[i];
}
// Find closest track point to a world position (optimized with spatial hashing)
findClosestTrackIndex(x, z) {
let minDist = Infinity;
let closestIdx = 0;
// Check every other point for performance
const step = this.points.length > 500 ? 2 : 1;
for (let i = 0; i < this.points.length; i += step) {
const p = this.points[i];
const dx = x - p.x;
const dz = z - p.z;
const dist = dx * dx + dz * dz;
if (dist < minDist) {
minDist = dist;
closestIdx = i;
}
}
// Refine: check neighbors of closest point
for (let j = -2; j <= 2; j++) {
const idx = ((closestIdx + j % this.points.length) + this.points.length) % this.points.length;
const p = this.points[idx];
const dx = x - p.x;
const dz = z - p.z;
const dist = dx * dx + dz * dz;
if (dist < minDist) {
minDist = dist;
closestIdx = idx;
}
}
return closestIdx;
}
// Get distance from a world position to the track centerline
getDistanceToTrack(x, z) {
const idx = this.findClosestTrackIndex(x, z);
const p = this.points[idx];
return Math.sqrt((x - p.x) ** 2 + (z - p.z) ** 2);
}
// Get surface type at a world position
getSurfaceAt(x, z) {
const idx = this.findClosestTrackIndex(x, z);
return this.surfaces[idx];
}
// Check if position is on the track (within width)
isOnTrack(x, z, trackWidth = 12) {
const dist = this.getDistanceToTrack(x, z);
return dist < trackWidth;
}
// Get interpolated heading at a position along the track
getHeadingAt(index) {
const i = ((index % this.points.length) + this.points.length) % this.points.length;
return this.points[i].heading;
}
// Get track elevation at a world position
getElevationAt(x, z) {
const idx = this.findClosestTrackIndex(x, z);
return this.points[idx].y;
}
}
exports.TrackGenerator = TrackGenerator;
};
// ── module: src/car.ts ──
__mods["src/car.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CarSystem = void 0;
// ============================================================
// Car physics and 3D mesh creation
// Handles acceleration, braking, steering, drifting, collisions
// ============================================================
const types_1 = require("./types");
class CarSystem {
constructor() {
this.wheelMeshes = [];
this.driftAngle = 0; // Visual drift angle for the car body
this.state = {
position: new THREE.Vector3(0, 2, 0),
velocity: new THREE.Vector3(0, 0, 0),
heading: 0,
speed: 0,
angularVelocity: 0,
gear: 1,
rpm: 800,
isDrifting: false,
onTrack: true,
currentSurface: types_1.SurfaceType.ASPHALT
};
this.mesh = new THREE.Group();
}
// Build the low-poly rally car mesh
buildMesh() {
const group = new THREE.Group();
// Main body - lower section (red rally car)
const bodyGeo = new THREE.BoxGeometry(2.0, 0.7, 4.2);
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xe63946, roughness: 0.4, metalness: 0.3 });
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.position.y = 0.5;
body.castShadow = true;
group.add(body);
// Cabin/roof (dark blue)
const cabinGeo = new THREE.BoxGeometry(1.6, 0.6, 2.0);
const cabinMat = new THREE.MeshStandardMaterial({ color: 0x1d3557, roughness: 0.2, metalness: 0.5 });
const cabin = new THREE.Mesh(cabinGeo, cabinMat);
cabin.position.set(0, 1.1, -0.2);
cabin.castShadow = true;
group.add(cabin);
// Windshield (front)
const windshieldGeo = new THREE.BoxGeometry(1.5, 0.55, 0.05);
const glassMat = new THREE.MeshStandardMaterial({ color: 0x87ceeb, roughness: 0.1, metalness: 0.8 });
const windshield = new THREE.Mesh(windshieldGeo, glassMat);
windshield.position.set(0, 1.1, 0.82);
group.add(windshield);
// Rear window
const rearWindow = new THREE.Mesh(new THREE.BoxGeometry(1.5, 0.55, 0.05), glassMat);
rearWindow.position.set(0, 1.1, -1.22);
group.add(rearWindow);
// Spoiler (black)
const spoilerGeo = new THREE.BoxGeometry(1.8, 0.08, 0.4);
const spoilerMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.5 });
const spoiler = new THREE.Mesh(spoilerGeo, spoilerMat);
spoiler.position.set(0, 1.4, -1.8);
group.add(spoiler);
// Spoiler supports
for (const side of [-0.6, 0.6]) {
const support = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.35, 0.08), spoilerMat);
support.position.set(side, 1.22, -1.8);
group.add(support);
}
// Headlights (front) - yellowish with glow
for (const side of [-0.7, 0.7]) {
const hlGeo = new THREE.BoxGeometry(0.35, 0.2, 0.05);
const hlMat = new THREE.MeshStandardMaterial({ color: 0xffffcc, emissive: 0xffffaa, emissiveIntensity: 0.5 });
const hl = new THREE.Mesh(hlGeo, hlMat);
hl.position.set(side, 0.45, 2.12);
group.add(hl);
}
// Taillights (rear) - red with glow
for (const side of [-0.7, 0.7]) {
const tlGeo = new THREE.BoxGeometry(0.35, 0.2, 0.05);
const tlMat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 0.3 });
const tl = new THREE.Mesh(tlGeo, tlMat);
tl.position.set(side, 0.45, -2.12);
group.add(tl);
}
// Bumpers (dark gray)
for (const z of [2.15, -2.15]) {
const bumper = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.3, 0.1), new THREE.MeshStandardMaterial({ color: 0x444444 }));
bumper.position.set(0, 0.35, z);
group.add(bumper);
}
// Wheels (4) - black tires with silver hubs
const wheelGeo = new THREE.CylinderGeometry(0.35, 0.35, 0.25, 8);
const wheelMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.9 });
const hubGeo = new THREE.CylinderGeometry(0.15, 0.15, 0.27, 6);
const hubMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.8 });
const wheelPositions = [
{ x: -1.1, y: 0.35, z: 1.4 }, // Front left
{ x: 1.1, y: 0.35, z: 1.4 }, // Front right
{ x: -1.1, y: 0.35, z: -1.4 }, // Rear left
{ x: 1.1, y: 0.35, z: -1.4 }, // Rear right
];
for (const pos of wheelPositions) {
const tire = new THREE.Mesh(wheelGeo, wheelMat);
tire.rotation.z = Math.PI / 2;
tire.position.set(pos.x, pos.y, pos.z);
tire.castShadow = true;
group.add(tire);
const hub = new THREE.Mesh(hubGeo, hubMat);
hub.rotation.z = Math.PI / 2;
hub.position.set(pos.x, pos.y, pos.z);
group.add(hub);
this.wheelMeshes.push(tire);
}
// Rally number plates on sides (white)
const numGeo = new THREE.BoxGeometry(0.5, 0.4, 0.02);
const numMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
for (const side of [-1.02, 1.02]) {
const numPlate = new THREE.Mesh(numGeo, numMat);
numPlate.position.set(side, 0.65, -0.3);
group.add(numPlate);
}
// Roof light bar (orange)
const lightBar = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.08, 0.15), new THREE.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xffaa00, emissiveIntensity: 0.3 }));
lightBar.position.set(0, 1.42, -0.2);
group.add(lightBar);
// Side skirts (rally car detail)
for (const side of [-1.05, 1.05]) {
const skirt = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.3, 3.5), new THREE.MeshStandardMaterial({ color: 0x222222 }));
skirt.position.set(side, 0.25, 0);
group.add(skirt);
}
this.mesh = group;
return group;
}
getMesh() { return this.mesh; }
// Update car physics based on input and track conditions
update(dt, input, surface, onTrack) {
const s = this.state;
const cp = types_1.CAR_PHYSICS;
// Determine grip and speed multiplier based on surface
let grip = cp.gripAsphalt;
let speedMult = cp.speedMultiplierAsphalt;
switch (surface) {
case types_1.SurfaceType.DIRT:
grip = cp.gripDirt;
speedMult = cp.speedMultiplierDirt;
break;
case types_1.SurfaceType.GRASS:
grip = cp.gripGrass;
speedMult = cp.speedMultiplierGrass;
break;
case types_1.SurfaceType.MUD:
grip = cp.gripMud;
speedMult = cp.speedMultiplierMud;
break;
}
// Handbrake reduces rear grip significantly for drifting
if (input.handbrake) {
grip *= cp.handbrakeGripLoss;
}
const maxSpeed = cp.maxSpeed * speedMult;
// Acceleration / braking
let accelForce = 0;
if (input.throttle) {
accelForce = cp.acceleration * speedMult;
}
else if (input.brake || input.reverse) {
if (s.speed > 0.5) {
accelForce = -cp.brakingForce;
}
else if (input.reverse) {
accelForce = cp.acceleration * 0.4 * speedMult;
}
}
// Apply acceleration to speed with gear-dependent curve
s.speed += accelForce * dt;
// Drag / friction - higher off-track
const dragCoeff = onTrack ? 0.015 : 0.08;
s.speed -= Math.abs(s.speed) * dragCoeff * dt * 10;
// Clamp speed
if (s.speed > maxSpeed)
s.speed = maxSpeed;
if (s.speed < cp.reverseMaxSpeed)
s.speed = cp.reverseMaxSpeed;
if (Math.abs(s.speed) < 0.05 && !input.throttle && !input.reverse)
s.speed = 0;
// Steering - speed dependent: tight at low speed, stable at high speed
let steerAmount = 0;
const absSpeed = Math.abs(s.speed);
if (absSpeed > 0.1) {
// Speed-dependent steering sensitivity
const steerFactor = cp.steeringSensitivity * (1.0 / (1.0 + absSpeed * 0.02));
if (input.left)
steerAmount = steerFactor;
if (input.right)
steerAmount = -steerFactor;
// Drifting detection: at high speed with low grip, car slides more than it turns
const driftRatio = Math.max(0, (absSpeed / maxSpeed - cp.driftThreshold));
s.isDrifting = (driftRatio > 0.1 && !onTrack) || (driftRatio > 0.2 && input.handbrake);
if (s.isDrifting) {
// In a drift, steering has less effect on heading but more slide angle
steerAmount *= grip;
}
}
// Apply rotation based on speed and steering
const turnRate = steerAmount * Math.min(absSpeed / 5, 1.0);
s.heading += turnRate * dt;
// Calculate velocity direction from heading and drift angle
let velAngle = s.heading;
if (s.isDrifting && absSpeed > 3) {
// Build up drift angle during drift
const targetDriftAngle = steerAmount * 0.8 * Math.min(absSpeed / maxSpeed, 1);
this.driftAngle += (targetDriftAngle - this.driftAngle) * dt * cp.driftAngleDecay;
velAngle += this.driftAngle;
}
else {
// Decay drift angle when not drifting
this.driftAngle *= Math.max(0, 1 - dt * 5);
}
// Update velocity vector from heading and speed
s.velocity.x = Math.sin(velAngle) * s.speed;
s.velocity.z = Math.cos(velAngle) * s.speed;
// Apply velocity to position
s.position.x += s.velocity.x * dt;
s.position.z += s.velocity.z * dt;
// Update track state
s.onTrack = onTrack;
s.currentSurface = surface;
// Calculate gear and RPM
this.updateGearAndRPM();
// Sync mesh position and rotation
this.mesh.position.copy(s.position);
// Visual heading includes drift angle for dramatic effect
const visualHeading = s.heading + (s.isDrifting ? this.driftAngle * 0.3 : 0);
this.mesh.rotation.y = visualHeading;
// Animate wheels (spin based on speed)
const wheelSpin = s.speed * dt * 4;
for (const wheel of this.wheelMeshes) {
wheel.rotation.x += wheelSpin;
}
}
updateGearAndRPM() {
const s = this.state;
const absSpeed = Math.abs(s.speed);
if (s.speed < -0.5) {
s.gear = -1; // Reverse
s.rpm = 800 + absSpeed * 40;
}
else if (absSpeed < 0.5) {
s.gear = 0; // Neutral
s.rpm = 800;
}
else {
let gearFound = false;
for (let i = 0; i < types_1.GEAR_RANGES.length; i++) {
const gr = types_1.GEAR_RANGES[i];
if (absSpeed >= gr.min && absSpeed <= gr.max) {
s.gear = i + 1;
// RPM based on position within gear range
const range = gr.max - gr.min;
const pos = (absSpeed - gr.min) / range;
s.rpm = 2000 + pos * 5000;
gearFound = true;
break;
}
}
if (!gearFound) {
s.gear = types_1.GEAR_RANGES.length;
s.rpm = 7000;
}
}
// Clamp RPM display
s.rpm = Math.max(600, Math.min(8000, s.rpm));
}
// Apply collision bounce - reflect velocity off surface normal
applyBounce(normal, force) {
const s = this.state;
const dot = s.velocity.x * normal.x + s.velocity.z * normal.z;
if (dot < 0) {
s.velocity.x -= 2 * dot * normal.x;
s.velocity.z -= 2 * dot * normal.z;
// Reduce speed on bounce
s.velocity.multiplyScalar(1 - force);
s.speed = Math.sqrt(s.velocity.x ** 2 + s.velocity.z ** 2) * (s.speed >= 0 ? 1 : -1);
}
}
reset(position, heading) {
this.state.position.copy(position);
this.state.velocity.set(0, 0, 0);
this.state.heading = heading;
this.state.speed = 0;
this.state.angularVelocity = 0;
this.state.gear = 1;
this.state.rpm = 800;
this.state.isDrifting = false;
this.driftAngle = 0;
}
}
exports.CarSystem = CarSystem;
};
// ── module: src/camera.ts ──
__mods["src/camera.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// Chase camera system with smooth following
// Supports chase cam and cinematic cam modes
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.CameraSystem = void 0;
class CameraSystem {
constructor() {
this.mode = 'chase';
// Chase camera offsets
this.chaseOffset = new THREE.Vector3(0, 4.5, -9);
this.cinematicOffset = new THREE.Vector3(0, 18, -25);
// Smooth interpolation targets
this.currentPos = new THREE.Vector3(0, 5, -10);
this.targetLookAt = new THREE.Vector3();
this.currentLookAt = new THREE.Vector3();
this.camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 800);
this.currentPos.copy(this.chaseOffset);
}
getCamera() { return this.camera; }
toggleMode() {
this.mode = this.mode === 'chase' ? 'cinematic' : 'chase';
}
update(carPosition, carHeading, dt) {
// Calculate desired camera position based on mode and car heading
const offset = this.mode === 'chase' ? this.chaseOffset : this.cinematicOffset;
// Rotate offset by car heading to follow behind the car
const cosH = Math.cos(carHeading);
const sinH = Math.sin(carHeading);
const targetX = carPosition.x + (offset.x * cosH - offset.z * sinH);
const targetZ = carPosition.z + (offset.x * sinH + offset.z * cosH);
const targetY = carPosition.y + offset.y;
// Smooth interpolation (lag for dynamic feel)
const lerpSpeed = this.mode === 'chase' ? 4.0 : 2.5;
const t = Math.min(1, dt * lerpSpeed);
this.currentPos.x += (targetX - this.currentPos.x) * t;
this.currentPos.y += (targetY - this.currentPos.y) * t;
this.currentPos.z += (targetZ - this.currentPos.z) * t;
// Look slightly ahead of the car
const lookAhead = 8;
this.targetLookAt.set(carPosition.x + Math.sin(carHeading) * lookAhead, carPosition.y + 1.5, carPosition.z + Math.cos(carHeading) * lookAhead);
// Smooth look-at interpolation
const lookLerp = Math.min(1, dt * 6);
this.currentLookAt.x += (this.targetLookAt.x - this.currentLookAt.x) * lookLerp;
this.currentLookAt.y += (this.targetLookAt.y - this.currentLookAt.y) * lookLerp;
this.currentLookAt.z += (this.targetLookAt.z - this.currentLookAt.z) * lookLerp;
// Apply to camera
this.camera.position.copy(this.currentPos);
this.camera.lookAt(this.currentLookAt);
}
resize(width, height) {
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
}
}
exports.CameraSystem = CameraSystem;
};
// ── module: src/scenery.ts ──
__mods["src/scenery.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SceneryGenerator = void 0;
class SceneryGenerator {
constructor() {
this.objects = [];
}
generate(trackPoints, scene) {
this.objects = [];
// Generate scenery along the track
const step = Math.max(1, Math.floor(trackPoints.length / 80));
for (let i = 0; i < trackPoints.length; i += step) {
const p = trackPoints[i];
// Place trees on both sides of the track
this.placeTree(p, scene, -1);
if (Math.random() > 0.3)
this.placeTree(p, scene, 1);
// Occasional rocks
if (Math.random() > 0.85) {
const side = Math.random() > 0.5 ? 1 : -1;
this.placeRock(p, scene, side);
}
// Guard rails on curves
if (i % (step * 3) === 0) {
this.placeBarrier(p, scene, -1);
this.placeBarrier(p, scene, 1);
}
// Banners/signs at intervals
if (i % (step * 8) === 0) {
this.placeBanner(p, scene, Math.random() > 0.5 ? 1 : -1);
}
}
// Add some random trees in the background for density
for (let i = 0; i < 60; i++) {
const idx = Math.floor(Math.random() * trackPoints.length);
const p = trackPoints[idx];
const side = Math.random() > 0.5 ? 1 : -1;
const dist = 20 + Math.random() * 30;
this.placeTreeAt(p.x + side * dist, p.z + (Math.random() - 0.5) * 20, p.y, scene);
}
}
placeTree(point, scene, side) {
const offset = 10 + Math.random() * 8;
const x = point.x + Math.cos(point.heading) * offset * side;
const z = point.z - Math.sin(point.heading) * offset * side;
this.placeTreeAt(x, z, point.y, scene);
}
placeTreeAt(x, z, groundY, scene) {
const group = new THREE.Group();
// Trunk
const trunkGeo = new THREE.CylinderGeometry(0.2, 0.35, 2 + Math.random(), 6);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x8B4513, flatShading: true });
const trunk = new THREE.Mesh(trunkGeo, trunkMat);
trunk.position.y = 1;
trunk.castShadow = true;
group.add(trunk);
// Foliage layers (cone shapes for pine trees)
const numLayers = 2 + Math.floor(Math.random() * 2);
for (let i = 0; i < numLayers; i++) {
const radius = 1.5 - i * 0.4;
const height = 1.8 - i * 0.3;
const coneGeo = new THREE.ConeGeometry(radius, height, 6);
// Vary green color slightly
const greenVal = Math.floor(80 + Math.random() * 60);
const leafColor = (0x1f << 16) | (greenVal << 8) | 0x1a;
const leafMat = new THREE.MeshStandardMaterial({
color: leafColor,
flatShading: true
});
const cone = new THREE.Mesh(coneGeo, leafMat);
cone.position.y = 2 + i * 1.2;
cone.castShadow = true;
group.add(cone);
}
group.position.set(x, groundY, z);
scene.add(group);
this.objects.push({
mesh: group, type: 'tree', radius: 0.5,
position: new THREE.Vector3(x, groundY, z)
});
}
placeRock(point, scene, side) {
const offset = 8 + Math.random() * 12;
const x = point.x + Math.cos(point.heading) * offset * side;
const z = point.z - Math.sin(point.heading) * offset * side;
const group = new THREE.Group();
const size = 0.5 + Math.random() * 1.0;
// Main rock body (irregular shape using scaled sphere)
const rockGeo = new THREE.SphereGeometry(size, 6, 5);
const grayVal = Math.floor(80 + Math.random() * 60);
const rockColor = (grayVal << 16) | (grayVal << 8) | Math.max(0, grayVal - 10);
const rockMat = new THREE.MeshStandardMaterial({
color: rockColor,
flatShading: true, roughness: 0.9
});
const rock = new THREE.Mesh(rockGeo, rockMat);
rock.scale.set(1 + Math.random() * 0.5, 0.6 + Math.random() * 0.4, 1 + Math.random() * 0.3);
rock.position.y = size * 0.4;
rock.castShadow = true;
group.add(rock);
group.position.set(x, point.y, z);
scene.add(group);
this.objects.push({
mesh: group, type: 'rock', radius: size,
position: new THREE.Vector3(x, point.y, z)
});
}
placeBarrier(point, scene, side) {
const offset = 7;
const x = point.x + Math.cos(point.heading) * offset * side;
const z = point.z - Math.sin(point.heading) * offset * side;
// Guard rail post
const postGeo = new THREE.CylinderGeometry(0.05, 0.05, 1.2, 4);
const metalMat = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.7 });
const group = new THREE.Group();
// Posts
for (let i = -1; i <= 1; i++) {
const post = new THREE.Mesh(postGeo, metalMat);
post.position.set(i * 2, 0.6, 0);
group.add(post);
}
// Rail bar
const railGeo = new THREE.BoxGeometry(4.2, 0.1, 0.1);
const rail = new THREE.Mesh(railGeo, metalMat);
rail.position.y = 1.0;
group.add(rail);
// Red/white stripes on posts
for (let i = -1; i <= 1; i++) {
const stripeGeo = new THREE.BoxGeometry(0.12, 0.3, 0.12);
const stripeMat = new THREE.MeshStandardMaterial({ color: i % 2 === 0 ? 0xff0000 : 0xffffff });
const stripe = new THREE.Mesh(stripeGeo, stripeMat);
stripe.position.set(i * 2, 0.8, 0);
group.add(stripe);
}
group.position.set(x, point.y, z);
scene.add(group);
this.objects.push({
mesh: group, type: 'barrier', radius: 1.5,
position: new THREE.Vector3(x, point.y, z)
});
}
placeBanner(point, scene, side) {
const offset = 12;
const x = point.x + Math.cos(point.heading) * offset * side;
const z = point.z - Math.sin(point.heading) * offset * side;
const group = new THREE.Group();
// Poles
const poleGeo = new THREE.CylinderGeometry(0.06, 0.06, 4, 5);
const poleMat = new THREE.MeshStandardMaterial({ color: 0x666666 });
for (let i of [-1.5, 1.5]) {
const pole = new THREE.Mesh(poleGeo, poleMat);
pole.position.set(i, 2, 0);
group.add(pole);
}
// Banner cloth
const bannerColors = [0xe63946, 0x457b9d, 0xf1faee, 0x2a9d8f];
const bannerColor = bannerColors[Math.floor(Math.random() * bannerColors.length)];
const bannerGeo = new THREE.PlaneGeometry(3, 1.5);
const bannerMat = new THREE.MeshStandardMaterial({
color: bannerColor, side: 2 // double-sided
});
const banner = new THREE.Mesh(bannerGeo, bannerMat);
banner.position.set(0, 3.2, 0);
group.add(banner);
group.position.set(x, point.y, z);
scene.add(group);
this.objects.push({
mesh: group, type: 'banner', radius: 1,
position: new THREE.Vector3(x, point.y, z)
});
}
// Check collision between car and scenery objects
checkCollision(carPos) {
for (const obj of this.objects) {
const dx = carPos.x - obj.position.x;
const dz = carPos.z - obj.position.z;
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist < obj.radius + 1.0) { // Car radius ~1m
return {
hit: true,
normal: new THREE.Vector3(dx / (dist || 0.001), 0, dz / (dist || 0.001))
};
}
}
return null;
}
getObjects() { return this.objects; }
}
exports.SceneryGenerator = SceneryGenerator;
};
// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
// ============================================================
// Particle effects system
// Dust clouds when off-road/drifting, sparks on collision, skid marks
// ============================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParticleSystem = void 0;
class ParticleSystem {
constructor() {
this.dustParticles = [];
this.sparkParticles = [];
this.skidMarkPositions = [];
this.scene = null;
this.dustPoints = null;
this.sparkPoints = null;
this.maxDust = 200;
this.maxSparks = 100;
}
init(scene) {
this.scene = scene;
// Create dust particle system using Points
const dustGeo = new THREE.BufferGeometry();
const positions = new Float32Array(this.maxDust * 3);
const colors = new Float32Array(this.maxDust * 3);
const sizes = new Float32Array(this.maxDust);
for (let i = 0; i < this.maxDust; i++) {
positions[i * 3] = 0;
positions[i * 3 + 1] = -100; // Hide initially
positions[i * 3 + 2] = 0;
colors[i * 3] = 0.7;
colors[i * 3 + 1] = 0.65;
colors[i * 3 + 2] = 0.5;
sizes[i] = 0;
}
dustGeo.setAttribute('position', { array: positions, itemSize: 3 });
dustGeo.setAttribute('color', { array: colors, itemSize: 3 });
const dustMat = new THREE.PointsMaterial({
color: 0xffffff, size: 1.5, transparent: true, opacity: 0.6,
blending: THREE.NormalBlending, depthWrite: false, vertexColors: true
});
this.dustPoints = new THREE.Points(dustGeo, dustMat);
scene.add(this.dustPoints);
// Create spark particle system
const sparkGeo = new THREE.BufferGeometry();
const sPositions = new Float32Array(this.maxSparks * 3);
for (let i = 0; i < this.maxSparks; i++) {
sPositions[i * 3] = 0;
sPositions[i * 3 + 1] = -100;
sPositions[i * 3 + 2] = 0;
}
sparkGeo.setAttribute('position', { array: sPositions, itemSize: 3 });
const sparkMat = new THREE.PointsMaterial({
color: 0xffaa00, size: 0.5, transparent: true, opacity: 0.9,
blending: THREE.AdditiveBlending, depthWrite: false
});
this.sparkPoints = new THREE.Points(sparkGeo, sparkMat);
scene.add(this.sparkPoints);
}
// Emit dust particles (for drifting or off-road)
emitDust(x, y, z, intensity) {
const count = Math.floor(intensity * 3);
for (let i = 0; i < count && this.dustParticles.length < this.maxDust; i++) {
this.dustParticles.push({
x: x + (Math.random() - 0.5) * 1.5,
y: y + Math.random() * 0.3,
z: z + (Math.random() - 0.5) * 1.5,
vx: (Math.random() - 0.5) * 2,
vy: Math.random() * 2 + 0.5,
vz: (Math.random() - 0.5) * 2,
life: 1.0,
maxLife: 0.8 + Math.random() * 0.6,
size: 0.5 + Math.random() * 1.0,
color: 0x998877
});
}
}
// Emit sparks (for collisions)
emitSparks(x, y, z) {
for (let i = 0; i < 15 && this.sparkParticles.length < this.maxSparks; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 3 + Math.random() * 5;
this.sparkParticles.push({
x, y: y + Math.random(), z,
vx: Math.cos(angle) * speed,
vy: Math.random() * 4 + 1,
vz: Math.sin(angle) * speed,
life: 1.0, maxLife: 0.3 + Math.random() * 0.3,
size: 0.2 + Math.random() * 0.3,
color: 0xffaa00
});
}
}
// Add a skid mark position (rendered as flat planes on the ground)
addSkidMark(x, y, z) {
this.skidMarkPositions.push(new THREE.Vector3(x, y + 0.02, z));
if (this.skidMarkPositions.length > 500) {
this.skidMarkPositions.shift(); // Remove oldest
}
}
update(dt) {
// Update dust particles
for (let i = this.dustParticles.length - 1; i >= 0; i--) {
const p = this.dustParticles[i];
p.life -= dt / p.maxLife;
if (p.life <= 0) {
this.dustParticles.splice(i, 1);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
p.vy -= 2 * dt; // Gravity
if (p.y < 0) {
p.y = 0;
p.vy *= -0.3;
}
}
// Update spark particles
for (let i = this.sparkParticles.length - 1; i >= 0; i--) {
const p = this.sparkParticles[i];
p.life -= dt / p.maxLife;
if (p.life <= 0) {
this.sparkParticles.splice(i, 1);
continue;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
p.vy -= 8 * dt; // Strong gravity for sparks
}
// Update dust Points geometry
if (this.dustPoints) {
const posAttr = this.dustPoints.geometry.getAttribute('position');
const colAttr = this.dustPoints.geometry.getAttribute('color');
if (posAttr && colAttr) {
for (let i = 0; i < this.maxDust; i++) {
if (i < this.dustParticles.length) {
const p = this.dustParticles[i];
posAttr.array[i * 3] = p.x;
posAttr.array[i * 3 + 1] = p.y;
posAttr.array[i * 3 + 2] = p.z;
const alpha = p.life;
colAttr.array[i * 3] = 0.7 * alpha;
colAttr.array[i * 3 + 1] = 0.65 * alpha;
colAttr.array[i * 3 + 2] = 0.5 * alpha;
}
else {
posAttr.array[i * 3 + 1] = -100; // Hide
}
}
posAttr.needsUpdate = true;
colAttr.needsUpdate = true;
}
}
// Update spark Points geometry
if (this.sparkPoints) {
const posAttr = this.sparkPoints.geometry.getAttribute('position');
if (posAttr) {
for (let i = 0; i < this.maxSparks; i++) {
if (i < this.sparkParticles.length) {
const p = this.sparkParticles[i];
posAttr.array[i * 3] = p.x;
posAttr.array[i * 3 + 1] = p.y;
posAttr.array[i * 3 + 2] = p.z;
}
else {
posAttr.array[i * 3 + 1] = -100; // Hide
}
}
posAttr.needsUpdate = true;
}
}
}
getSkidMarkPositions() { return this.skidMarkPositions; }
}
exports.ParticleSystem = ParticleSystem;
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUDManager = void 0;
// ============================================================
// HUD (Heads-Up Display) management
// Speedometer, gear/RPM display, lap info, minimap, overlays
// ============================================================
const types_1 = require("./types");
class HUDManager {
constructor() {
this.hudEl = null;
this.speedValueEl = null;
this.gearDisplayEl = null;
this.rpmFillEl = null;
this.currentLapEl = null;
this.currentTimeEl = null;
this.bestLapEl = null;
this.countdownEl = null;
this.countdownTextEl = null;
this.pauseMenuEl = null;
this.finishScreenEl = null;
this.minimapCanvas = null;
this.minimapCtx = null;
this.surfaceIndicatorEl = null;
this._onResume = () => { };
this._onRestart = () => { };
}
init() {
this.hudEl = document.getElementById('hud');
this.speedValueEl = document.getElementById('speedValue');
this.gearDisplayEl = document.getElementById('gearDisplay');
this.rpmFillEl = document.getElementById('rpmFill');
this.currentLapEl = document.getElementById('currentLap');
this.currentTimeEl = document.getElementById('currentTime');
this.bestLapEl = document.getElementById('bestLap');
this.countdownEl = document.getElementById('countdown');
this.countdownTextEl = document.getElementById('countdownText');
this.pauseMenuEl = document.getElementById('pauseMenu');
this.finishScreenEl = document.getElementById('finishScreen');
this.minimapCanvas = document.getElementById('minimap');
if (this.minimapCanvas) {
this.minimapCtx = this.minimapCanvas.getContext('2d');
}
this.surfaceIndicatorEl = document.getElementById('surfaceIndicator');
// Wire up buttons
const resumeBtn = document.getElementById('resumeBtn');
if (resumeBtn)
resumeBtn.addEventListener('click', () => this._onResume());
const restartPauseBtn = document.getElementById('restartPauseBtn');
if (restartPauseBtn)
restartPauseBtn.addEventListener('click', () => this._onRestart());
const restartFinishBtn = document.getElementById('restartFinishBtn');
if (restartFinishBtn)
restartFinishBtn.addEventListener('click', () => this._onRestart());
}
setOnResume(fn) { this._onResume = fn; }
setOnRestart(fn) { this._onRestart = fn; }
// Show/hide HUD elements based on game state
setState(state) {
if (!this.hudEl)
return;
switch (state) {
case types_1.GameState.COUNTDOWN:
this.hudEl.style.display = 'block';
this.countdownEl.style.display = 'flex';
this.pauseMenuEl.style.display = 'none';
this.finishScreenEl.style.display = 'none';
break;
case types_1.GameState.RACING:
this.hudEl.style.display = 'block';
this.countdownEl.style.display = 'none';
this.pauseMenuEl.style.display = 'none';
this.finishScreenEl.style.display = 'none';
break;
case types_1.GameState.PAUSED:
this.hudEl.style.display = 'block';
this.countdownEl.style.display = 'none';
this.pauseMenuEl.style.display = 'flex';
this.finishScreenEl.style.display = 'none';
break;
case types_1.GameState.FINISHED:
this.hudEl.style.display = 'none';
this.countdownEl.style.display = 'none';
this.pauseMenuEl.style.display = 'none';
this.finishScreenEl.style.display = 'flex';
break;
}
}
// Update speed display (km/h)
updateSpeed(speedMs) {
if (!this.speedValueEl)
return;
const kmh = Math.abs(Math.round(speedMs * 3.6));
this.speedValueEl.textContent = String(kmh);
}
// Update gear display
updateGear(gear) {
if (!this.gearDisplayEl)
return;
if (gear === -1)
this.gearDisplayEl.textContent = 'R';
else if (gear === 0)
this.gearDisplayEl.textContent = 'N';
else
this.gearDisplayEl.textContent = String(gear);
}
// Update RPM bar
updateRPM(rpm) {
if (!this.rpmFillEl)
return;
const pct = Math.min(100, ((rpm - 600) / (8000 - 600)) * 100);
this.rpmFillEl.style.width = `${pct}%`;
// Color change at high RPM
if (pct > 85) {
this.rpmFillEl.style.background = '#ff3333';
}
else if (pct > 65) {
this.rpmFillEl.style.background = 'linear-gradient(to right, #0f0, #ff0, #f80)';
}
else {
this.rpmFillEl.style.background = 'linear-gradient(to right, #0f0, #ff0)';
}
}
// Update lap info
updateLapInfo(currentLap, currentTimeMs, bestLapMs) {
if (this.currentLapEl) {
this.currentLapEl.textContent = `Lap ${currentLap}/${types_1.TOTAL_LAPS}`;
}
if (this.currentTimeEl) {
this.currentTimeEl.textContent = this.formatTime(currentTimeMs);
}
if (this.bestLapEl) {
if (bestLapMs > 0) {
this.bestLapEl.textContent = `Best: ${this.formatTime(bestLapMs)}`;
}
else {
this.bestLapEl.textContent = 'Best: --:--.---';
}
}
}
// Show countdown number
showCountdown(value) {
if (!this.countdownTextEl)
return;
this.countdownTextEl.textContent = value;
// Color changes
if (value === 'GO!') {
this.countdownTextEl.style.color = '#00ff00';
this.countdownTextEl.style.fontSize = '120px';
}
else {
const num = parseInt(value);
if (num <= 1) {
this.countdownTextEl.style.color = '#ff4444';
}
else {
this.countdownTextEl.style.color = '#ffffff';
}
this.countdownTextEl.style.fontSize = '150px';
}
}
// Show finish screen data
showFinishScreen(finalTimeMs, bestLapMs) {
const finalTimeEl = document.getElementById('finalTime');
const finalBestEl = document.getElementById('finalBestLap');
if (finalTimeEl)
finalTimeEl.textContent = `Final Time: ${this.formatTime(finalTimeMs)}`;
if (finalBestEl)
finalBestEl.textContent = `Best Lap: ${bestLapMs > 0 ? this.formatTime(bestLapMs) : '--:--.---'}`;
}
// Draw minimap showing track layout and car position
drawMinimap(trackPoints, carPos, checkpointIndex, carHeading) {
if (!this.minimapCtx || !this.minimapCanvas)
return;
const ctx = this.minimapCtx;
const w = this.minimapCanvas.width;
const h = this.minimapCanvas.height;
// Clear with semi-transparent background
ctx.fillStyle = 'rgba(0, 15, 8, 0.9)';
ctx.fillRect(0, 0, w, h);
if (trackPoints.length < 2)
return;
// Find bounds of track
let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
for (const p of trackPoints) {
if (p.x < minX)
minX = p.x;
if (p.x > maxX)
maxX = p.x;
if (p.z < minZ)
minZ = p.z;
if (p.z > maxZ)
maxZ = p.z;
}
const padding = 15;
const scaleX = (w - padding * 2) / ((maxX - minX) || 1);
const scaleZ = (h - padding * 2) / ((maxZ - minZ) || 1);
const scale = Math.min(scaleX, scaleZ);
const offsetX = (w - (maxX - minX) * scale) / 2;
const offsetZ = (h - (maxZ - minZ) * scale) / 2;
// Draw track path with color coding for surface types
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
let started = false;
for (let i = 0; i < trackPoints.length; i += 2) {
const px = (trackPoints[i].x - minX) * scale + offsetX;
const pz = (trackPoints[i].z - minZ) * scale + offsetZ;
if (!started) {
ctx.moveTo(px, pz);
started = true;
}
else
ctx.lineTo(px, pz);
}
ctx.closePath();
ctx.strokeStyle = '#4a6b4a';
ctx.stroke();
// Draw track center line (thinner)
ctx.lineWidth = 2;
ctx.beginPath();
started = false;
for (let i = 0; i < trackPoints.length; i += 2) {
const px = (trackPoints[i].x - minX) * scale + offsetX;
const pz = (trackPoints[i].z - minZ) * scale + offsetZ;
if (!started) {
ctx.moveTo(px, pz);
started = true;
}
else
ctx.lineTo(px, pz);
}
ctx.closePath();
ctx.strokeStyle = '#6a8b6a';
ctx.stroke();
// Draw checkpoints as small dots
const numCheckpoints = 8;
for (let i = 0; i < numCheckpoints; i++) {
const idx = Math.floor((i / numCheckpoints) * trackPoints.length);
if (idx >= trackPoints.length)
continue;
const px = (trackPoints[idx].x - minX) * scale + offsetX;
const pz = (trackPoints[idx].z - minZ) * scale + offsetZ;
ctx.fillStyle = i <= checkpointIndex ? '#00ff44' : '#555';
ctx.beginPath();
ctx.arc(px, pz, 3, 0, Math.PI * 2);
ctx.fill();
// Checkpoint ring
ctx.strokeStyle = i <= checkpointIndex ? '#00ff44' : '#444';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(px, pz, 5, 0, Math.PI * 2);
ctx.stroke();
}
// Draw car position (bright dot with heading indicator)
const carPx = (carPos.x - minX) * scale + offsetX;
const carPz = (carPos.z - minZ) * scale + offsetZ;
// Car glow
ctx.fillStyle = 'rgba(255, 204, 0, 0.3)';
ctx.beginPath();
ctx.arc(carPx, carPz, 8, 0, Math.PI * 2);
ctx.fill();
// Car body
ctx.fillStyle = '#ffcc00';
ctx.beginPath();
ctx.arc(carPx, carPz, 4, 0, Math.PI * 2);
ctx.fill();
// Car heading indicator (arrow)
const arrowLen = 12;
const ax = carPx + Math.sin(carHeading) * arrowLen;
const az = carPz - Math.cos(carHeading) * arrowLen;
ctx.strokeStyle = '#ffcc00';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(carPx, carPz);
ctx.lineTo(ax, az);
ctx.stroke();
// Arrow tip
const tipAngle = Math.atan2(az - carPz, ax - carPx);
ctx.fillStyle = '#ffcc00';
ctx.beginPath();
ctx.moveTo(ax, az);
ctx.lineTo(ax + Math.cos(tipAngle + 2.5) * 4, az + Math.sin(tipAngle + 2.5) * 4);
ctx.lineTo(ax + Math.cos(tipAngle - 2.5) * 4, az + Math.sin(tipAngle - 2.5) * 4);
ctx.closePath();
ctx.fill();
// Border
ctx.strokeStyle = 'rgba(255,255,255,0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, w, h);
}
// Update surface indicator display
updateSurface(surface, onTrack) {
if (!this.surfaceIndicatorEl)
return;
let text = '';
let color = '#aaa';
switch (surface) {
case 'asphalt':
text = '🏎️ ASPHALT';
color = '#88ccff';
break;
case 'dirt':
text = '🌾 DIRT';
color = '#ddaa66';
break;
case 'grass':
text = '🌿 GRASS';
color = '#66bb44';
break;
case 'mud':
text = '💧 MUD';
color = '#886644';
break;
}
if (!onTrack) {
text += ' ⚠️ OFF-TRACK';
color = '#ff6644';
}
this.surfaceIndicatorEl.textContent = text;
this.surfaceIndicatorEl.style.color = color;
}
formatTime(ms) {
const totalSec = ms / 1000;
const min = Math.floor(totalSec / 60);
const sec = Math.floor(totalSec % 60);
const millis = Math.floor(ms % 1000);
return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}.${String(millis).padStart(3, '0')}`;
}
}
exports.HUDManager = HUDManager;
};
// ── module: src/gameState.ts ──
__mods["src/gameState.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameManager = void 0;
// ============================================================
// Game state management
// Lap tracking, timing, checkpoint validation, race state machine
// ============================================================
const types_1 = require("./types");
class GameManager {
get state() { return this._state; }
constructor() {
this._state = types_1.GameState.COUNTDOWN;
// Timing
this.raceStartTime = 0;
this.lapStartTime = 0;
this.currentLap = 1;
this.bestLapTime = Infinity;
this.lapTimes = [];
// Checkpoint tracking (prevent cheating)
this.checkpointsPassed = new Set();
this.lastCheckpointIndex = -1;
// Countdown state
this.countdownValue = '3';
this.countdownTimer = 0;
this.reset();
}
reset() {
this._state = types_1.GameState.COUNTDOWN;
this.raceStartTime = 0;
this.lapStartTime = 0;
this.currentLap = 1;
this.bestLapTime = Infinity;
this.lapTimes = [];
this.checkpointsPassed.clear();
this.lastCheckpointIndex = -1;
this.countdownValue = '3';
this.countdownTimer = 0;
}
// Update countdown logic
updateCountdown(dt) {
if (this._state !== types_1.GameState.COUNTDOWN)
return false;
this.countdownTimer += dt * 1000; // Convert to ms
const elapsed = this.countdownTimer / 1000;
if (elapsed < 1) {
this.countdownValue = '3';
}
else if (elapsed < 2) {
this.countdownValue = '2';
}
else if (elapsed < 3) {
this.countdownValue = '1';
}
else if (elapsed < 3.5) {
this.countdownValue = 'GO!';
}
else {
// Countdown complete, start racing
this._state = types_1.GameState.RACING;
this.raceStartTime = performance.now();
this.lapStartTime = this.raceStartTime;
return true; // Just started racing
}
return false;
}
getCountdownValue() { return this.countdownValue; }
// Check if car has passed a checkpoint
checkCheckpoint(checkpointIndex, totalCheckpoints) {
if (this._state !== types_1.GameState.RACING)
return;
const expectedNext = (this.lastCheckpointIndex + 1) % totalCheckpoints;
// Allow some tolerance - must pass checkpoints in order
if (checkpointIndex === expectedNext || checkpointIndex === ((expectedNext + 1) % totalCheckpoints)) {
this.checkpointsPassed.add(checkpointIndex);
this.lastCheckpointIndex = checkpointIndex;
// Check for lap completion: passed start line after going through all checkpoints
if (checkpointIndex === 0 && this.checkpointsPassed.size >= totalCheckpoints - 1) {
this.completeLap();
}
}
}
completeLap() {
const now = performance.now();
const lapTime = now - this.lapStartTime;
this.lapTimes.push(lapTime);
if (lapTime < this.bestLapTime) {
this.bestLapTime = lapTime;
}
this.currentLap++;
this.checkpointsPassed.clear();
this.lastCheckpointIndex = -1;
this.lapStartTime = now;
// Check for race completion
if (this.currentLap > types_1.TOTAL_LAPS) {
this.finishRace();
}
}
finishRace() {
this._state = types_1.GameState.FINISHED;
}
getCurrentTimeMs() {
if (this._state === types_1.GameState.RACING || this._state === types_1.GameState.PAUSED) {
return performance.now() - this.raceStartTime;
}
return 0;
}
getBestLapMs() {
return this.bestLapTime < Infinity ? this.bestLapTime : 0;
}
getCurrentLap() { return this.currentLap; }
togglePause() {
if (this._state === types_1.GameState.RACING) {
this._state = types_1.GameState.PAUSED;
}
else if (this._state === types_1.GameState.PAUSED) {
// Adjust start time to account for pause duration
const now = performance.now();
const pauseDuration = 0; // Simplified - could track pause duration
this.raceStartTime += pauseDuration;
this.lapStartTime += pauseDuration;
this._state = types_1.GameState.RACING;
}
}
isRacing() { return this._state === types_1.GameState.RACING; }
getFinalTimeMs() {
if (this._state === types_1.GameState.FINISHED) {
return performance.now() - this.raceStartTime;
}
return 0;
}
getLastCheckpointIndex() { return this.lastCheckpointIndex; }
}
exports.GameManager = GameManager;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"41f59203-48eb-4a58-931e-c8fe73349c29","tokensIn":2430927,"tokensOut":54922,"tokensTotal":2485849,"turns":60,"toolCalls":59,"failedToolCalls":3,"timestamp":"2026-07-31T01:37:39.617Z"} -->