← Rally results

Rally

Qwen 3.6 27b MTP · typescript

Initial view of Rally

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"} -->