← Rally results

Rally

Qwen 3.6 27b MTP · composer

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 82,342 bytes · SHA-256 96fed4a3d022
<!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; }
#app { position: relative; width: 100%; height: 100%; }
#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; }
.hud-value { font-size: 42px; font-weight: bold; color: #fff; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); letter-spacing: 1px; }
.hud-label { font-size: 13px; color: #aaa; text-transform: uppercase; letter-spacing: 2px; }

#speedometer { position: absolute; bottom: 20px; right: 20px; text-align: center; background: rgba(0,0,0,0.5); padding: 10px 20px; border-radius: 10px; }
.gear-display { font-size: 28px; color: #ffcc00; font-weight: bold; margin-top: -4px; text-shadow: 1px 1px 3px rgba(0,0,0,0.8); }
#rpmBar { display: block; margin: 6px auto 0; border-radius: 3px; background: #222; }

#lapInfo { position: absolute; top: 15px; left: 15px; background: rgba(0,0,0,0.5); padding: 12px 18px; border-radius: 10px; min-width: 160px; }
.hud-row { display: flex; justify-content: space-between; margin-bottom: 4px; align-items: baseline; }
.hud-row:last-child { margin-bottom: 0; }

#minimap { position: absolute; bottom: 20px; left: 15px; border-radius: 8px; background: rgba(0,0,0,0.4); border: 2px solid rgba(255,255,255,0.2); }

/* Countdown */
#countdownOverlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 20; pointer-events: none; }
#countdownText { font-size: 120px; font-weight: bold; color: #fff; text-shadow: 4px 4px 8px rgba(0,0,0,0.9); opacity: 0; transition: opacity 0.2s; }

/* 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.7); z-index: 30; }
#pauseMenu h1 { font-size: 64px; color: #fff; margin-bottom: 30px; text-shadow: 3px 3px 6px rgba(0,0,0,0.8); }

/* 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.8); z-index: 30; }
#finishScreen h1 { font-size: 52px; color: #ffcc00; margin-bottom: 20px; text-shadow: 3px 3px 6px rgba(0,0,0,0.8); }
.stat-row { display: flex; justify-content: space-between; width: 300px; font-size: 24px; color: #fff; margin-bottom: 10px; padding: 5px 0; border-bottom: 1px solid rgba(255,255,255,0.2); }
.stat-row span:first-child { color: #aaa; }

/* Start Screen */
#startScreen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); z-index: 40; }
#startScreen h1 { font-size: 56px; color: #ffcc00; margin-bottom: 15px; text-shadow: 3px 3px 8px rgba(0,0,0,0.9); letter-spacing: 3px; }
.instructions { color: #ccc; font-size: 16px; margin-bottom: 40px; text-align: center; line-height: 1.8; }

/* Buttons */
button { pointer-events: auto; padding: 12px 36px; font-size: 20px; border: none; border-radius: 8px; cursor: pointer; margin: 6px; transition: transform 0.1s, background 0.2s; }
button:hover { transform: scale(1.05); }
button:active { transform: scale(0.97); }
#startBtn { background: #ffcc00; color: #1a1a2e; font-weight: bold; padding: 16px 48px; font-size: 24px; }
#resumeBtn, #restartPauseBtn, #restartFinishBtn { background: rgba(255,255,255,0.15); color: #fff; border: 2px solid rgba(255,255,255,0.3); }
#resumeBtn:hover, #restartPauseBtn:hover, #restartFinishBtn:hover { background: rgba(255,255,255,0.25); }

/* Touch Controls */
#touchControls { position: absolute; bottom: 0; left: 0; width: 100%; height: 100%; z-index: 15; pointer-events: none; }
#dpad { position: absolute; bottom: 20px; left: 20px; width: 140px; height: 140px; pointer-events: auto; }
.dpad-btn { background: rgba(255,255,255,0.2); color: #fff; border: 2px solid rgba(255,255,255,0.3); font-size: 24px; width: 46px; height: 46px; position: absolute; display: flex; align-items: center; justify-content: center; padding: 0; border-radius: 8px; }
.dpad-up { top: 0; left: 50%; transform: translateX(-50%); }
.dpad-down { bottom: 0; left: 50%; transform: translateX(-50%); }
.dpad-left { top: 50%; left: 0; transform: translateY(-50%); }
.dpad-right { top: 50%; right: 0; transform: translateY(-50%); }

#actionBtns { position: absolute; bottom: 20px; right: 15px; display: flex; flex-direction: column; gap: 8px; pointer-events: auto; }
.action-btn { width: 70px; height: 50px; font-size: 14px; font-weight: bold; padding: 0; border-radius: 8px; }
.gas-btn { background: rgba(0,200,0,0.3); color: #0f0; border: 2px solid rgba(0,255,0,0.4); }
.brake-btn { background: rgba(200,0,0,0.3); color: #f66; border: 2px solid rgba(255,100,100,0.4); }
.handbrake-btn { background: rgba(255,200,0,0.3); color: #fc0; border: 2px solid rgba(255,255,100,0.4); }

@media (max-width: 768px) {
  .hud-value { font-size: 28px !important; }
  #speedometer { bottom: 10px; right: 10px; padding: 6px 12px; }
  #lapInfo { top: 8px; left: 8px; padding: 8px 12px; min-width: 130px; }
  .hud-row .hud-label { font-size: 10px !important; }
  .hud-row .hud-value { font-size: 16px !important; }
  #minimap { width: 120px; height: 120px; bottom: 10px; left: 8px; }
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>

<!-- HUD Overlay -->
<div id="hud" style="display:none;">
  <div id="speedometer">
    <div class="hud-value" id="speedValue">0</div>
    <div class="hud-label">KM/H</div>
    <div class="gear-display"><span id="gearValue">N</span></div>
    <canvas id="rpmBar" width="200" height="16"></canvas>
  </div>
  
  <div id="lapInfo">
    <div class="hud-row"><span class="hud-label">LAP</span><span class="hud-value" id="lapValue">1/3</span></div>
    <div class="hud-row"><span class="hud-label">TIME</span><span class="hud-value" id="timeValue">0:00.00</span></div>
    <div class="hud-row"><span class="hud-label">BEST LAP</span><span class="hud-value" id="bestLapValue">--:--.--</span></div>
  </div>
  
  <canvas id="minimap" width="180" height="180"></canvas>
</div>

<!-- Countdown Overlay -->
<div id="countdownOverlay">
  <div id="countdownText"></div>
</div>

<!-- Pause Menu -->
<div id="pauseMenu" style="display:none;">
  <h1>PAUSED</h1>
  <button id="resumeBtn">RESUME</button>
  <button id="restartPauseBtn">RESTART</button>
</div>

<!-- Finish Screen -->
<div id="finishScreen" style="display:none;">
  <h1>RACE COMPLETE!</h1>
  <div class="stat-row"><span>Total Time:</span><span id="finalTime"></span></div>
  <div class="stat-row"><span>Best Lap:</span><span id="finalBestLap"></span></div>
  <button id="restartFinishBtn">RACE AGAIN</button>
</div>

<!-- Touch Controls -->
<div id="touchControls" style="display:none;">
  <div id="dpad">
    <button class="dpad-btn dpad-up" data-dir="up">▲</button>
    <button class="dpad-btn dpad-left" data-dir="left">◀</button>
    <button class="dpad-btn dpad-right" data-dir="right">▶</button>
    <button class="dpad-btn dpad-down" data-dir="down">▼</button>
  </div>
  <div id="actionBtns">
    <button class="action-btn gas-btn" data-action="gas">GAS</button>
    <button class="action-btn brake-btn" data-action="brake">BRAKE</button>
    <button class="action-btn handbrake-btn" data-action="handbrake">HB</button>
  </div>
</div>

<!-- Start Screen -->
<div id="startScreen">
  <h1>RALLY RACING 3D</h1>
  <p class="instructions">Arrow Keys / WASD to drive<br>Space = Handbrake | P = Pause | C = Camera Toggle | R = Restart</p>
  <button id="startBtn">START RACE</button>
</div>
<script>
'use strict';

// ── const: RallyGame ──
// ============================================================
// RALLY RACING 3D - Main Game Module
// ============================================================

const RallyGame = {
  // === CONSTANTS ===
  TOTAL_LAPS: 3,
  TRACK_WIDTH: 12,
  CAR_ACCEL: 18.0,
  CAR_BRAKE: 25.0,
  CAR_MAX_SPEED: 45.0,       // m/s (~162 km/h) on asphalt
  CAR_REVERSE_MAX: 10.0,
  STEER_SENSITIVITY: 2.8,    // radians per second at full steer
  DRAG_COEFFICIENT: 0.3,     // air drag factor
  SURFACE_FRICTION: {
    asphalt: { grip: 1.0, speedMult: 1.0, color: 0x444444 },
    dirt:    { grip: 0.65, speedMult: 0.75, color: 0x8B7355 },
    grass:   { grip: 0.45, speedMult: 0.55, color: 0x4a7c3f },
    mud:     { grip: 0.25, speedMult: 0.35, color: 0x5c4033 }
  },

  // === GAME STATE ===
  state: 'menu', // menu | countdown | racing | paused | finished
  scene: null, camera: null, renderer: null, clock: null,
  
  car: {
    position: new THREE.Vector3(),
    velocity: new THREE.Vector3(),
    heading: 0,           // radians, 0 = +Z direction
    speed: 0,             // scalar speed (signed)
    angularVel: 0,        // yaw rotation rate
    gear: 1,              // 1-5 + N for neutral/reverse
    rpm: 800,
    driftAngle: 0,
    surfaceType: 'asphalt',
    onTrack: true,
    handbrake: false
  },

  input: { up: false, down: false, left: false, right: false, handbrake: false },
  
  race: {
    lap: 1,
    checkpointsPassed: [],
    currentCheckpoint: 0,
    startTime: 0,
    elapsedTime: 0,
    lapStartTime: 0,
    bestLapTime: Infinity,
    countdownValue: 3,
    countdownTimer: 0
  },

  cameraMode: 'chase', // chase | cinematic
  cameraPos: new THREE.Vector3(),
  cameraTarget: new THREE.Vector3(),
  
  trackPoints: [],       // Array of {x, y, z} control points
  trackCurve: null,      // THREE.CatmullRomCurve3
  checkpoints: [],       // positions along curve (0-1)
  obstacles: [],         // {mesh, position, radius}
  
  particles: [],         // active particle systems
  skidMarks: [],         // accumulated skid mark geometries
  
  audioCtx: null,
  engineOsc: null,
  engineGain: null,
  noiseNode: null,
  noiseGain: null,

  minimapCtx: null,
  
  // Scene references for cleanup
  groundMesh: null,
  trackMesh: null,
  carGroup: null,
  sceneryObjects: [],
  checkpointGates: []
};

// ── function: generateTrack ──
// ============================================================
// TRACK GENERATION - Procedural rally circuit
// ============================================================

function generateTrack() {
  // Define control points for a winding rally track with hills and dips
  const rawPoints = [
    new THREE.Vector3(0, 0, 0),
    new THREE.Vector3(40, 1, -20),
    new THREE.Vector3(80, 3, -15),
    new THREE.Vector3(110, 5, 10),
    new THREE.Vector3(120, 7, 50),
    new THREE.Vector3(100, 6, 90),
    new THREE.Vector3(60, 4, 110),
    new THREE.Vector3(20, 2, 100),
    new THREE.Vector3(-20, 1, 85),
    new THREE.Vector3(-50, 3, 60),
    new THREE.Vector3(-70, 5, 30),
    new THREE.Vector3(-80, 4, -10),
    new THREE.Vector3(-70, 2, -45),
    new THREE.Vector3(-45, 1, -65),
    new THREE.Vector3(-15, 0.5, -70),
    new THREE.Vector3(10, 0, -55)
  ];

  const curve = new THREE.CatmullRomCurve3(rawPoints, true, 'catmullrom', 0.5);
  
  // Sample points along the curve for track geometry and checkpoints
  const numSamples = 400;
  const sampledPoints = [];
  for (let i = 0; i < numSamples; i++) {
    const t = i / numSamples;
    const point = curve.getPointAt(t);
    const tangent = curve.getTangentAt(t);
    sampledPoints.push({
      pos: point.clone(),
      tangent: tangent.clone(),
      normal: new THREE.Vector3(-tangent.z, 0, tangent.x).normalize(),
      t: t
    });
  }

  RallyGame.trackCurve = curve;
  RallyGame.trackPoints = sampledPoints;

  // Define checkpoint positions (evenly spaced around the track)
  const numCheckpoints = 8;
  RallyGame.checkpoints = [];
  for (let i = 0; i < numCheckpoints; i++) {
    RallyGame.checkpoints.push(i / numCheckpoints);
  }

  // Build track mesh using custom geometry
  buildTrackMesh(sampledPoints, curve);
  
  return sampledPoints;
}

// ── function: addTrackEdges ──
function addTrackEdges(points) {
  const width = RallyGame.TRACK_WIDTH / 2 + 0.3;
  
  for (let side = -1; side <= 1; side += 2) {
    const edgePoints = [];
    for (let i = 0; i < points.length; i++) {
      const p = points[i];
      const n = p.normal.clone().multiplyScalar(width * side);
      edgePoints.push(new THREE.Vector3(p.pos.x + n.x, p.pos.y + 0.25, p.pos.z + n.z));
    }

    const lineGeo = new THREE.BufferGeometry().setFromPoints(edgePoints);
    const lineMat = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 1 });
    const line = new THREE.Line(lineGeo, lineMat);
    RallyGame.scene.add(line);
  }

  // Center dashed line
  const centerPoints = [];
  for (let i = 0; i < points.length; i += 4) {
    const p = points[i];
    centerPoints.push(new THREE.Vector3(p.pos.x, p.pos.y + 0.22, p.pos.z));
  }
  // Add gaps by duplicating some points
  const dashedPoints = [];
  for (let i = 0; i < centerPoints.length - 1; i++) {
    if (i % 3 !== 2) {
      dashedPoints.push(centerPoints[i]);
    } else {
      // Gap: add same point twice to create a break
      dashedPoints.push(centerPoints[i].clone());
      dashedPoints.push(centerPoints[i + 1].clone());
    }
  }
  
  const centerGeo = new THREE.BufferGeometry().setFromPoints(dashedPoints);
  const centerMat = new THREE.LineBasicMaterial({ color: 0xffff00, linewidth: 2 });
  const centerLine = new THREE.Line(centerGeo, centerMat);
  RallyGame.scene.add(centerLine);
}

// ── function: updateCarPhysics ──
// ============================================================
// CAR PHYSICS - Rally-style driving dynamics
// ============================================================

function updateCarPhysics(dt) {
  const car = RallyGame.car;
  const input = RallyGame.input;
  
  // Determine surface properties
  const trackInfo = getTrackInfo(car.position);
  car.surfaceType = trackInfo.surfaceType;
  car.onTrack = trackInfo.distance <= RallyGame.TRACK_WIDTH / 2 + 0.5;
  
  const surface = RallyGame.SURFACE_FRICTION[car.surfaceType];
  const gripFactor = surface.grip * (car.handbrake ? 0.3 : 1.0);
  const speedLimit = RallyGame.CAR_MAX_SPEED * surface.speedMult;

  // === ACCELERATION / BRAKING ===
  let accelForce = 0;
  
  if (input.up) {
    if (car.speed < speedLimit) {
      accelForce = RallyGame.CAR_ACCEL * surface.grip;
    } else {
      accelForce = -5.0; // slight brake at max speed
    }
  }
  
  if (input.down) {
    if (car.speed > 0.5) {
      accelForce = -RallyGame.CAR_BRAKE;
    } else {
      accelForce = -RallyGame.CAR_ACCEL * 0.4; // reverse
    }
  }

  // Apply acceleration along heading direction
  const headingX = Math.sin(car.heading);
  const headingZ = Math.cos(car.heading);
  
  car.velocity.x += headingX * accelForce * dt;
  car.velocity.z += headingZ * accelForce * dt;

  // === STEERING ===
  let steerInput = 0;
  if (input.left) steerInput -= 1;
  if (input.right) steerInput += 1;

  // Speed-dependent steering: tighter at low speed, stable at high speed
  const absSpeed = Math.abs(car.speed);
  const speedFactor = Math.max(0.3, 1.0 - absSpeed / RallyGame.CAR_MAX_SPEED * 0.5);
  const steerRate = RallyGame.STEER_SENSITIVITY * steerInput * speedFactor;
  
  // Only steer when moving
  if (absSpeed > 0.5) {
    car.heading += steerRate * dt * Math.sign(car.speed);
  }

  // === DRAG & FRICTION ===
  const dragForce = RallyGame.DRAG_COEFFICIENT * absSpeed;
  const surfaceDrag = car.onTrack ? 1.0 : 2.5; // extra drag off-track
  
  if (car.velocity.lengthSq() > 0.01) {
    const velMag = car.velocity.length();
    const totalDrag = Math.min(dragForce * surfaceDrag + absSpeed * 0.5, velMag);
    
    const dragX = (car.velocity.x / velMag) * totalDrag;
    const dragZ = (car.velocity.z / velMag) * totalDrag;
    
    car.velocity.x -= dragX * dt;
    car.velocity.z -= dragZ * dt;
  }

  // === DRIFT MECHANICS ===
  // Calculate lateral velocity component
  const forwardVel = car.velocity.x * headingX + car.velocity.z * headingZ;
  const rightX = Math.cos(car.heading);
  const rightZ = -Math.sin(car.heading);
  const lateralVel = car.velocity.x * rightX + car.velocity.z * rightZ;

  // Apply grip to reduce lateral velocity (grip pulls car toward heading direction)
  const gripForce = gripFactor * 8.0;
  car.velocity.x -= rightX * lateralVel * Math.min(gripForce * dt, 1.0);
  car.velocity.z -= rightZ * lateralVel * Math.min(gripForce * dt, 1.0);

  // Calculate drift angle for visual feedback and particle effects
  const velocityHeading = Math.atan2(car.velocity.x, car.velocity.z);
  car.driftAngle = Math.abs(normalizeAngle(car.heading - velocityHeading));

  // === UPDATE POSITION ===
  car.position.x += car.velocity.x * dt;
  car.position.z += car.velocity.z * dt;

  // Get track elevation at current position
  const elevPoint = RallyGame.trackCurve.getPointAt(trackInfo.closestT);
  const distFromCenter = trackInfo.distance;
  
  // Height follows track with some falloff for off-track areas
  const heightFactor = Math.max(0, 1.0 - distFromCenter / (RallyGame.TRACK_WIDTH * 3));
  car.position.y = elevPoint.y + heightFactor * 0.5 + 0.2;

  // === SPEED & GEAR CALCULATION ===
  car.speed = forwardVel;
  
  const absSpeedKmh = Math.abs(car.speed) * 3.6; // m/s to km/h
  
  // Simulated gear system (5 gears + reverse)
  if (car.speed < -0.5) {
    car.gear = 'R';
  } else if (absSpeed < 1) {
    car.gear = 'N';
  } else if (absSpeedKmh < 40) {
    car.gear = 1;
  } else if (absSpeedKmh < 80) {
    car.gear = 2;
  } else if (absSpeedKmh < 130) {
    car.gear = 3;
  } else if (absSpeedKmh < 180) {
    car.gear = 4;
  } else {
    car.gear = 5;
  }

  // RPM simulation based on speed and gear
  const gearNum = typeof car.gear === 'number' ? car.gear : (car.gear === 'R' ? -1 : 0);
  if (gearNum !== 0) {
    const gearRatio = [0, 3.5, 2.2, 1.6, 1.2, 0.9][Math.abs(gearNum)] || 1;
    car.rpm = Math.max(800, Math.min(7500, absSpeed * gearRatio * 120 + (input.up ? 1500 : 0)));
  } else {
    car.rpm = input.up ? 3000 : 900;
  }

  // === OBSTACLE COLLISIONS ===
  checkObstacleCollisions();

  // === PARTICLE EFFECTS ===
  updateParticles(dt, absSpeedKmh);

  // === CHECKPOINT SYSTEM ===
  updateCheckpoints(trackInfo.closestT);
}

// ── function: normalizeAngle ──
function normalizeAngle(angle) {
  while (angle > Math.PI) angle -= Math.PI * 2;
  while (angle < -Math.PI) angle += Math.PI * 2;
  return angle;
}

// ── function: checkObstacleCollisions ──
function checkObstacleCollisions() {
  const car = RallyGame.car;
  const carRadius = 1.5;
  
  for (const obstacle of RallyGame.obstacles) {
    const dx = car.position.x - obstacle.position.x;
    const dz = car.position.z - obstacle.position.z;
    const dist = Math.sqrt(dx * dx + dz * dz);
    
    if (dist < carRadius + obstacle.radius) {
      // Collision response: bounce and slow down
      const normalX = dx / dist;
      const normalZ = dz / dist;
      
      // Reflect velocity
      const dot = car.velocity.x * normalX + car.velocity.z * normalZ;
      if (dot < 0) { // Only if moving toward obstacle
        car.velocity.x -= 2 * dot * normalX * 0.5;
        car.velocity.z -= 2 * dot * normalZ * 0.5;
        
        // Kill some momentum
        car.velocity.multiplyScalar(0.6);
      }

      // Push car out of obstacle
      const pushDist = (carRadius + obstacle.radius - dist) + 0.1;
      car.position.x += normalX * pushDist;
      car.position.z += normalZ * pushDist;

      // Spawn collision sparks
      spawnCollisionSparks(car.position.clone());
      
      // Play crash sound
      playCrashSound();
    }
  }
}

// ── function: updateCheckpoints ──
function updateCheckpoints(trackT) {
  const race = RallyGame.race;
  if (race.currentCheckpoint >= RallyGame.checkpoints.length) return;
  
  const cpT = RallyGame.checkpoints[race.currentCheckpoint];
  let diff = trackT - cpT;
  
  // Handle wrap-around near end of lap
  if (diff < -0.5) diff += 1.0;
  if (diff > 0.5) diff -= 1.0;

  if (Math.abs(diff) < 0.04) {
    race.currentCheckpoint++;
    
    // Check for lap completion
    if (race.currentCheckpoint >= RallyGame.checkpoints.length) {
      completeLap();
    }
  }
}

// ── function: completeLap ──
function completeLap() {
  const race = RallyGame.race;
  
  const now = performance.now();
  const lapTime = (now - race.lapStartTime) / 1000;
  
  if (lapTime < race.bestLapTime && race.lap > 0) {
    race.bestLapTime = lapTime;
  }

  race.lap++;
  race.currentCheckpoint = 0;
  race.lapStartTime = now;

  // Play lap beep
  playBeep(880, 0.15);

  if (race.lap > RallyGame.TOTAL_LAPS) {
    finishRace();
  }
}

// ── function: finishRace ──
function finishRace() {
  RallyGame.state = 'finished';
  
  const totalTime = RallyGame.race.elapsedTime;
  const bestLap = RallyGame.race.bestLapTime;
  
  document.getElementById('finalTime').textContent = formatTime(totalTime);
  document.getElementById('finalBestLap').textContent = 
    isFinite(bestLap) ? formatTime(bestLap) : '--:--.--';
  document.getElementById('finishScreen').style.display = 'flex';

  // Play finish tone
  playFinishTone();
}

// ── function: updateCamera ──
// ============================================================
// CAMERA SYSTEM - Chase camera with dynamic lag
// ============================================================

function updateCamera(dt) {
  const car = RallyGame.car;
  const mode = RallyGame.cameraMode;
  
  let targetPos, lookAt;
  
  if (mode === 'chase') {
    // Behind and above the car
    const camDist = 8 + Math.abs(car.speed) * 0.05; // Pull back at high speed
    const camHeight = 3.5 + Math.abs(car.speed) * 0.02;
    
    targetPos = new THREE.Vector3(
      car.position.x - Math.sin(car.heading) * camDist,
      car.position.y + camHeight,
      car.position.z - Math.cos(car.heading) * camDist
    );
    
    lookAt = new THREE.Vector3(
      car.position.x + Math.sin(car.heading) * 5,
      car.position.y + 1.0,
      car.position.z + Math.cos(car.heading) * 5
    );
  } else {
    // Cinematic: higher and wider angle
    const camDist = 20;
    const camHeight = 12;
    
    targetPos = new THREE.Vector3(
      car.position.x - Math.sin(car.heading + 0.3) * camDist,
      car.position.y + camHeight,
      car.position.z - Math.cos(car.heading + 0.3) * camDist
    );
    
    lookAt = new THREE.Vector3(
      car.position.x,
      car.position.y + 0.5,
      car.position.z
    );
  }

  // Smooth camera follow with lag for dynamic feel
  const lerpFactor = mode === 'chase' ? Math.min(dt * 4.0, 1) : Math.min(dt * 2.5, 1);
  
  RallyGame.cameraPos.lerp(targetPos, lerpFactor);
  RallyGame.cameraTarget.lerp(lookAt, lerpFactor);

  // Add slight camera shake at high speed or when drifting
  if (Math.abs(car.speed) > 20 || car.driftAngle > 0.3) {
    const shakeIntensity = Math.min(Math.abs(car.speed) * 0.001 + car.driftAngle * 0.05, 0.1);
    RallyGame.cameraPos.x += (Math.random() - 0.5) * shakeIntensity;
    RallyGame.cameraPos.y += (Math.random() - 0.5) * shakeIntensity * 0.5;
  }

  RallyGame.camera.position.copy(RallyGame.cameraPos);
  RallyGame.camera.lookAt(RallyGame.cameraTarget);
}

// ── function: generateScenery ──
// ============================================================
// SCENERY GENERATION - Trees, rocks, signs, barriers
// ============================================================

function generateScenery() {
  const scene = RallyGame.scene;
  
  // Ground plane (large terrain)
  const groundGeo = new THREE.PlaneGeometry(600, 600);
  const groundMat = new THREE.MeshLambertMaterial({ color: 0x3d5c2a });
  const ground = new THREE.Mesh(groundGeo, groundMat);
  ground.rotation.x = -Math.PI / 2;
  ground.position.y = -1;
  scene.add(ground);
  RallyGame.groundMesh = ground;

  // Generate trees along the track
  generateTrees();
  
  // Generate rocks scattered around
  generateRocks();
  
  // Generate signs and banners
  generateSigns();
  
  // Generate guard rails on sharp curves
  generateGuardRails();
  
  // Generate checkpoint gates (invisible but visible markers)
  generateCheckpointMarkers();

  // Add some distant hills/mountains for atmosphere
  generateDistantHills();
}

// ── function: generateTrees ──
function generateTrees() {
  const scene = RallyGame.scene;
  const treeCount = 200;
  
  // Pre-create geometries and materials for reuse
  const trunkGeo = new THREE.CylinderGeometry(0.15, 0.25, 2, 6);
  const trunkMat = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
  
  // Different tree types
  const pineConeGeo = new THREE.ConeGeometry(1.5, 3, 7);
  const pineMat = new THREE.MeshLambertMaterial({ color: 0x2d6b1e });
  
  const roundCrownGeo = new THREE.SphereGeometry(1.2, 8, 6);
  const roundMat = new THREE.MeshLambertMaterial({ color: 0x3a7d2c });

  for (let i = 0; i < treeCount; i++) {
    // Place trees along the track but off to the sides
    const t = Math.random();
    const point = RallyGame.trackCurve.getPointAt(t);
    const tangent = RallyGame.trackCurve.getTangentAt(t);
    const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
    
    // Random offset from track center (keep off the road)
    const side = Math.random() > 0.5 ? 1 : -1;
    const distance = RallyGame.TRACK_WIDTH * 0.8 + Math.random() * 40;
    
    const treeX = point.x + normal.x * distance * side;
    const treeZ = point.z + normal.z * distance * side;
    const treeY = -0.5 + Math.random() * 2; // slight elevation variation
    
    const treeGroup = new THREE.Group();
    
    // Trunk
    const trunk = new THREE.Mesh(trunkGeo, trunkMat);
    trunk.position.y = 1;
    treeGroup.add(trunk);
    
    // Foliage (random type)
    if (Math.random() > 0.4) {
      // Pine tree
      const cone = new THREE.Mesh(pineConeGeo, pineMat);
      cone.position.y = 3 + Math.random();
      treeGroup.add(cone);
      
      // Second layer for taller pines
      if (Math.random() > 0.5) {
        const cone2 = new THREE.Mesh(
          new THREE.ConeGeometry(1.2, 2.5, 7), 
          pineMat
        );
        cone2.position.y = 4.5 + Math.random();
        treeGroup.add(cone2);
      }
    } else {
      // Round deciduous tree
      const crown = new THREE.Mesh(roundCrownGeo, roundMat);
      crown.position.y = 3;
      treeGroup.add(crown);
    }
    
    // Random scale variation
    const scale = 0.7 + Math.random() * 0.6;
    treeGroup.scale.set(scale, scale, scale);
    
    treeGroup.position.set(treeX, treeY, treeZ);
    scene.add(treeGroup);
    
    RallyGame.sceneryObjects.push({
      mesh: treeGroup,
      position: new THREE.Vector3(treeX, treeY, treeZ),
      radius: 0.5 * scale
    });
  }
}

// ── function: generateRocks ──
function generateRocks() {
  const scene = RallyGame.scene;
  const rockCount = 60;
  
  for (let i = 0; i < rockCount; i++) {
    const t = Math.random();
    const point = RallyGame.trackCurve.getPointAt(t);
    const tangent = RallyGame.trackCurve.getTangentAt(t);
    const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
    
    const side = Math.random() > 0.5 ? 1 : -1;
    const distance = RallyGame.TRACK_WIDTH * 0.6 + Math.random() * 25;
    
    // Create irregular rock shape
    const rockGeo = new THREE.DodecahedronGeometry(0.4 + Math.random() * 0.8, 1);
    
    // Distort vertices for natural look
    const positions = rockGeo.attributes.position;
    for (let j = 0; j < positions.count; j++) {
      positions.setY(j, positions.getY(j) * (0.5 + Math.random() * 0.5));
    }
    rockGeo.computeVertexNormals();
    
    const shade = 0.3 + Math.random() * 0.2;
    const rockMat = new THREE.MeshLambertMaterial({ 
      color: new THREE.Color(shade, shade - 0.05, shade - 0.1) 
    });
    
    const rock = new THREE.Mesh(rockGeo, rockMat);
    rock.position.set(
      point.x + normal.x * distance * side,
      point.y - 0.2,
      point.z + normal.z * distance * side
    );
    rock.rotation.set(Math.random(), Math.random(), Math.random());
    
    scene.add(rock);
    
    RallyGame.sceneryObjects.push({
      mesh: rock,
      position: rock.position.clone(),
      radius: 0.5
    });
  }
}

// ── function: generateSigns ──
function generateSigns() {
  const scene = RallyGame.scene;
  
  // Directional arrow signs at curves
  const curvePoints = [0.15, 0.35, 0.55, 0.75];
  
  for (const t of curvePoints) {
    const point = RallyGame.trackCurve.getPointAt(t);
    const tangent = RallyGame.trackCurve.getTangentAt(t);
    const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
    
    // Sign post
    const postGeo = new THREE.CylinderGeometry(0.05, 0.05, 2.5, 6);
    const postMat = new THREE.MeshLambertMaterial({ color: 0x888888 });
    const post = new THREE.Mesh(postGeo, postMat);
    
    // Sign board (arrow)
    const boardGeo = new THREE.BoxGeometry(1.2, 0.6, 0.05);
    const canvas = document.createElement('canvas');
    canvas.width = 128;
    canvas.height = 64;
    const ctx = canvas.getContext('2d');
    
    // Yellow warning sign
    ctx.fillStyle = '#ffcc00';
    ctx.fillRect(0, 0, 128, 64);
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 4;
    ctx.strokeRect(4, 4, 120, 56);
    
    // Arrow
    ctx.fillStyle = '#333';
    ctx.beginPath();
    ctx.moveTo(90, 32);
    ctx.lineTo(40, 15);
    ctx.lineTo(40, 49);
    ctx.closePath();
    ctx.fill();
    
    const texture = new THREE.CanvasTexture(canvas);
    const boardMat = new THREE.MeshBasicMaterial({ map: texture });
    const board = new THREE.Mesh(boardGeo, boardMat);
    board.position.y = 1.5;
    
    // Place sign on the outside of the curve
    const signGroup = new THREE.Group();
    signGroup.add(post);
    signGroup.add(board);
    
    signGroup.position.set(
      point.x + normal.x * (RallyGame.TRACK_WIDTH / 2 + 3),
      point.y,
      point.z + normal.z * (RallyGame.TRACK_WIDTH / 2 + 3)
    );
    
    // Face the car approaching direction
    signGroup.lookAt(point.x, point.y, point.z);
    
    scene.add(signGroup);
  }

  // Start/finish banner
  const startBanner = createBanner('START', '#ff0000');
  const startPoint = RallyGame.trackCurve.getPointAt(0.01);
  const startTangent = RallyGame.trackCurve.getTangentAt(0.01);
  
  startBanner.position.set(startPoint.x, startPoint.y + 5, startPoint.z);
  startBanner.lookAt(startPoint.x + startTangent.x * 10, startPoint.y + 5, startPoint.z + startTangent.z * 10);
  scene.add(startBanner);
}

// ── function: createBanner ──
function createBanner(text, bgColor) {
  const group = new THREE.Group();
  
  // Banner fabric
  const canvas = document.createElement('canvas');
  canvas.width = 256;
  canvas.height = 80;
  const ctx = canvas.getContext('2d');
  
  ctx.fillStyle = bgColor || '#ffffff';
  ctx.fillRect(0, 0, 256, 80);
  ctx.strokeStyle = '#333';
  ctx.lineWidth = 4;
  ctx.strokeRect(4, 4, 248, 72);
  
  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 40px Arial';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(text, 128, 42);
  
  const texture = new THREE.CanvasTexture(canvas);
  const bannerGeo = new THREE.PlaneGeometry(6, 2);
  const bannerMat = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide });
  const banner = new THREE.Mesh(bannerGeo, bannerMat);
  
  // Support poles
  const poleGeo = new THREE.CylinderGeometry(0.08, 0.08, 5, 6);
  const poleMat = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
  
  const leftPole = new THREE.Mesh(poleGeo, poleMat);
  leftPole.position.set(-3, -2.5, 0);
  group.add(leftPole);
  
  const rightPole = new THREE.Mesh(poleGeo, poleMat);
  rightPole.position.set(3, -2.5, 0);
  group.add(rightPole);
  
  group.add(banner);
  return group;
}

// ── function: generateGuardRails ──
function generateGuardRails() {
  const scene = RallyGame.scene;
  
  // Place guard rails on the outside of sharp curves
  const railSegments = [
    { start: 0.1, end: 0.2 },
    { start: 0.3, end: 0.4 },
    { start: 0.55, end: 0.65 },
    { start: 0.78, end: 0.88 }
  ];

  const postGeo = new THREE.CylinderGeometry(0.04, 0.04, 1, 4);
  const postMat = new THREE.MeshLambertMaterial({ color: 0xcccccc });
  
  for (const seg of railSegments) {
    const steps = Math.floor((seg.end - seg.start) * RallyGame.trackPoints.length);
    
    for (let i = 0; i < steps; i += 3) {
      const t = seg.start + (i / RallyGame.trackPoints.length);
      if (t > seg.end) break;
      
      const point = RallyGame.trackCurve.getPointAt(t);
      const tangent = RallyGame.trackCurve.getTangentAt(t);
      const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
      
      // Post on outside of curve
      const post = new THREE.Mesh(postGeo, postMat);
      post.position.set(
        point.x + normal.x * (RallyGame.TRACK_WIDTH / 2 + 1.5),
        point.y + 0.5,
        point.z + normal.z * (RallyGame.TRACK_WIDTH / 2 + 1.5)
      );
      
      scene.add(post);
    }

    // Rail bar between posts
    const railPoints = [];
    for (let i = 0; i <= steps; i += 3) {
      const t = seg.start + (i / RallyGame.trackPoints.length);
      if (t > seg.end) break;
      
      const point = RallyGame.trackCurve.getPointAt(t);
      const tangent = RallyGame.trackCurve.getTangentAt(t);
      const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
      
      railPoints.push(new THREE.Vector3(
        point.x + normal.x * (RallyGame.TRACK_WIDTH / 2 + 1.5),
        point.y + 0.8,
        point.z + normal.z * (RallyGame.TRACK_WIDTH / 2 + 1.5)
      ));
    }

    if (railPoints.length > 1) {
      const railGeo = new THREE.BufferGeometry().setFromPoints(railPoints);
      const railMat = new THREE.LineBasicMaterial({ color: 0xdddddd });
      scene.add(new THREE.Line(railGeo, railMat));
    }
  }
}

// ── function: generateCheckpointMarkers ──
function generateCheckpointMarkers() {
  // Visual markers for checkpoints (small flags)
  for (let i = 0; i < RallyGame.checkpoints.length; i++) {
    const t = RallyGame.checkpoints[i];
    const point = RallyGame.trackCurve.getPointAt(t);
    
    const flagGeo = new THREE.PlaneGeometry(1, 1.5);
    const canvas = document.createElement('canvas');
    canvas.width = 64;
    canvas.height = 96;
    const ctx = canvas.getContext('2d');
    
    // Checkered pattern
    const sqSize = 8;
    for (let x = 0; x < 64; x += sqSize) {
      for (let y = 0; y < 96; y += sqSize) {
        ctx.fillStyle = ((x / sqSize + y / sqSize) % 2 === 0) ? '#fff' : '#333';
        ctx.fillRect(x, y, sqSize, sqSize);
      }
    }
    
    const texture = new THREE.CanvasTexture(canvas);
    const flagMat = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide });
    const flag = new THREE.Mesh(flagGeo, flagMat);
    
    flag.position.set(point.x, point.y + 3, point.z);
    RallyGame.scene.add(flag);
  }
}

// ── function: generateDistantHills ──
function generateDistantHills() {
  // Simple distant mountains for atmosphere
  const hillCount = 12;
  
  for (let i = 0; i < hillCount; i++) {
    const angle = (i / hillCount) * Math.PI * 2;
    const distance = 150 + Math.random() * 80;
    
    const height = 15 + Math.random() * 30;
    const radius = 20 + Math.random() * 25;
    
    const hillGeo = new THREE.ConeGeometry(radius, height, 6);
    const shade = 0.15 + Math.random() * 0.1;
    const hillMat = new THREE.MeshLambertMaterial({ 
      color: new THREE.Color(shade, shade + 0.1, shade - 0.05) 
    });
    
    const hill = new THREE.Mesh(hillGeo, hillMat);
    hill.position.set(
      Math.cos(angle) * distance,
      height / 2 - 5,
      Math.sin(angle) * distance
    );
    
    RallyGame.scene.add(hill);
  }
}

// ── function: updateParticles ──
// ============================================================
// PARTICLE EFFECTS - Dust, skid marks, sparks
// ============================================================

function updateParticles(dt, speedKmh) {
  const car = RallyGame.car;
  
  // Remove expired particles
  RallyGame.particles = RallyGame.particles.filter(p => p.life > 0);
  
  for (const particle of RallyGame.particles) {
    particle.life -= dt;
    
    if (particle.type === 'dust') {
      particle.mesh.position.y += dt * 1.5; // rise
      particle.mesh.scale.multiplyScalar(1 + dt * 2); // expand
      const alpha = Math.max(0, particle.life / particle.maxLife);
      particle.mesh.material.opacity = alpha * 0.4;
    } else if (particle.type === 'spark') {
      particle.mesh.position.add(particle.velocity.clone().multiplyScalar(dt));
      particle.velocity.y -= dt * 15; // gravity
      const alpha = Math.max(0, particle.life / particle.maxLife);
      particle.mesh.material.opacity = alpha;
    } else if (particle.type === 'smoke') {
      particle.mesh.position.y += dt * 2;
      particle.mesh.scale.multiplyScalar(1 + dt * 3);
      const alpha = Math.max(0, particle.life / particle.maxLife);
      particle.mesh.material.opacity = alpha * 0.3;
    }
  }

  // Spawn dust when drifting or off-road
  if (car.driftAngle > 0.2 || !car.onTrack) {
    if (Math.random() < 0.6) {
      spawnDustParticle(car.position.clone());
    }
  }

  // Spawn smoke from rear wheels at high speed with handbrake
  if (car.handbrake && Math.abs(car.speed) > 10) {
    if (Math.random() < 0.8) {
      spawnSmokeParticle(car.position.clone());
    }
  }
}

// ── function: spawnDustParticle ──
function spawnDustParticle(position) {
  const size = 0.3 + Math.random() * 0.5;
  
  // Create a simple sprite for dust
  const canvas = document.createElement('canvas');
  canvas.width = 64;
  canvas.height = 64;
  const ctx = canvas.getContext('2d');
  
  const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  gradient.addColorStop(0, 'rgba(180, 160, 130, 0.6)');
  gradient.addColorStop(0.5, 'rgba(150, 140, 120, 0.3)');
  gradient.addColorStop(1, 'rgba(120, 110, 90, 0)');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 64, 64);
  
  const texture = new THREE.CanvasTexture(canvas);
  const mat = new THREE.SpriteMaterial({ 
    map: texture, 
    transparent: true, 
    depthWrite: false 
  });
  
  const sprite = new THREE.Sprite(mat);
  sprite.position.set(
    position.x + (Math.random() - 0.5) * 2,
    position.y + 0.3,
    position.z + (Math.random() - 0.5) * 2
  );
  sprite.scale.set(size, size, 1);
  
  RallyGame.scene.add(sprite);
  
  RallyGame.particles.push({
    mesh: sprite,
    type: 'dust',
    life: 1.0 + Math.random(),
    maxLife: 2.0
  });
}

// ── function: spawnSmokeParticle ──
function spawnSmokeParticle(position) {
  const size = 0.5 + Math.random() * 0.3;
  
  const canvas = document.createElement('canvas');
  canvas.width = 64;
  canvas.height = 64;
  const ctx = canvas.getContext('2d');
  
  const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  gradient.addColorStop(0, 'rgba(100, 100, 100, 0.5)');
  gradient.addColorStop(0.7, 'rgba(80, 80, 80, 0.2)');
  gradient.addColorStop(1, 'rgba(60, 60, 60, 0)');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 64, 64);
  
  const texture = new THREE.CanvasTexture(canvas);
  const mat = new THREE.SpriteMaterial({ 
    map: texture, 
    transparent: true, 
    depthWrite: false 
  });
  
  const sprite = new THREE.Sprite(mat);
  
  // Position at rear wheels
  const carHeading = RallyGame.car.heading;
  const offsetX = Math.cos(carHeading) * (Math.random() - 0.5) * 1.5;
  const offsetZ = -Math.sin(carHeading) * (Math.random() - 0.5) * 1.5;
  
  sprite.position.set(
    position.x + offsetX,
    position.y + 0.2,
    position.z + offsetZ
  );
  sprite.scale.set(size, size, 1);
  
  RallyGame.scene.add(sprite);
  
  RallyGame.particles.push({
    mesh: sprite,
    type: 'smoke',
    life: 1.5 + Math.random(),
    maxLife: 2.5
  });
}

// ── function: spawnCollisionSparks ──
function spawnCollisionSparks(position) {
  const sparkCount = 8;
  
  for (let i = 0; i < sparkCount; i++) {
    const canvas = document.createElement('canvas');
    canvas.width = 32;
    canvas.height = 32;
    const ctx = canvas.getContext('2d');
    
    const gradient = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
    gradient.addColorStop(0, 'rgba(255, 255, 100, 1)');
    gradient.addColorStop(0.3, 'rgba(255, 200, 50, 0.8)');
    gradient.addColorStop(1, 'rgba(255, 100, 0, 0)');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 32, 32);
    
    const texture = new THREE.CanvasTexture(canvas);
    const mat = new THREE.SpriteMaterial({ 
      map: texture, 
      transparent: true, 
      depthWrite: false 
    });
    
    const sprite = new THREE.Sprite(mat);
    sprite.position.copy(position);
    sprite.scale.set(0.2, 0.2, 1);
    
    RallyGame.scene.add(sprite);
    
    RallyGame.particles.push({
      mesh: sprite,
      type: 'spark',
      life: 0.3 + Math.random() * 0.4,
      maxLife: 0.7,
      velocity: new THREE.Vector3(
        (Math.random() - 0.5) * 8,
        Math.random() * 6 + 2,
        (Math.random() - 0.5) * 8
      )
    });
  }
}

// ── function: cleanupParticles ──
// Clean up old particles to prevent memory leaks
function cleanupParticles() {
  for (const particle of RallyGame.particles) {
    if (particle.life <= 0 && particle.mesh.parent) {
      RallyGame.scene.remove(particle.mesh);
      if (particle.mesh.material.map) {
        particle.mesh.material.map.dispose();
      }
      particle.mesh.material.dispose();
    }
  }
}

// ── function: initAudio ──
// ============================================================
// AUDIO SYSTEM - Synthesized with Web Audio API
// ============================================================

function initAudio() {
  try {
    RallyGame.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    
    // Engine sound: oscillator + gain for pitch/volume control
    RallyGame.engineOsc = RallyGame.audioCtx.createOscillator();
    RallyGame.engineGain = RallyGame.audioCtx.createGain();
    RallyGame.engineFilter = RallyGame.audioCtx.createBiquadFilter();
    
    RallyGame.engineOsc.type = 'sawtooth';
    RallyGame.engineOsc.frequency.value = 80;
    
    RallyGame.engineFilter.type = 'lowpass';
    RallyGame.engineFilter.frequency.value = 400;
    RallyGame.engineFilter.Q.value = 2;
    
    RallyGame.engineGain.gain.value = 0;
    
    RallyGame.engineOsc.connect(RallyGame.engineFilter);
    RallyGame.engineFilter.connect(RallyGame.engineGain);
    RallyGame.engineGain.connect(RallyGame.audioCtx.destination);
    RallyGame.engineOsc.start();

    // Noise for skid/gravel sounds
    const bufferSize = RallyGame.audioCtx.sampleRate * 2;
    const noiseBuffer = RallyGame.audioCtx.createBuffer(1, bufferSize, RallyGame.audioCtx.sampleRate);
    const output = noiseBuffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      output[i] = Math.random() * 2 - 1;
    }

    RallyGame.noiseSource = RallyGame.audioCtx.createBufferSource();
    RallyGame.noiseSource.buffer = noiseBuffer;
    RallyGame.noiseSource.loop = true;
    
    RallyGame.noiseFilter = RallyGame.audioCtx.createBiquadFilter();
    RallyGame.noiseFilter.type = 'bandpass';
    RallyGame.noiseFilter.frequency.value = 800;
    RallyGame.noiseFilter.Q.value = 1;
    
    RallyGame.noiseGain = RallyGame.audioCtx.createGain();
    RallyGame.noiseGain.gain.value = 0;
    
    RallyGame.noiseSource.connect(RallyGame.noiseFilter);
    RallyGame.noiseFilter.connect(RallyGame.noiseGain);
    RallyGame.noiseGain.connect(RallyGame.audioCtx.destination);
    RallyGame.noiseSource.start();

  } catch (e) {
    console.warn('Audio initialization failed:', e);
  }
}

// ── function: updateEngineSound ──
function updateEngineSound() {
  if (!RallyGame.audioCtx || !RallyGame.engineOsc) return;
  
  const car = RallyGame.car;
  const absSpeed = Math.abs(car.speed);
  
  // Engine pitch based on RPM
  const targetFreq = 60 + (car.rpm / 7500) * 200;
  RallyGame.engineOsc.frequency.setTargetAtTime(targetFreq, RallyGame.audioCtx.currentTime, 0.05);
  
  // Volume based on throttle and speed
  let volume = 0.03;
  if (RallyGame.input.up) {
    volume = 0.06 + (absSpeed / RallyGame.CAR_MAX_SPEED) * 0.12;
  } else if (car.rpm > 1500) {
    volume = 0.04;
  }
  
  // Muffle when off-track (engine struggling)
  if (!car.onTrack) {
    RallyGame.engineFilter.frequency.setTargetAtTime(200, RallyGame.audioCtx.currentTime, 0.1);
  } else {
    RallyGame.engineFilter.frequency.setTargetAtTime(300 + absSpeed * 5, RallyGame.audioCtx.currentTime, 0.1);
  }
  
  RallyGame.engineGain.gain.setTargetAtTime(volume, RallyGame.audioCtx.currentTime, 0.05);

  // Skid/gravel noise
  const isDrifting = car.driftAngle > 0.2;
  const isOffRoad = !car.onTrack;
  
  let noiseVol = 0;
  if (isDrifting) {
    noiseVol = Math.min(car.driftAngle * 0.15, 0.1);
    RallyGame.noiseFilter.frequency.setTargetAtTime(600, RallyGame.audioCtx.currentTime, 0.1);
  }
  if (isOffRoad && absSpeed > 3) {
    noiseVol = Math.min(absSpeed * 0.005, 0.08);
    RallyGame.noiseFilter.frequency.setTargetAtTime(400 + absSpeed * 20, RallyGame.audioCtx.currentTime, 0.1);
  }
  
  RallyGame.noiseGain.gain.setTargetAtTime(noiseVol, RallyGame.audioCtx.currentTime, 0.05);
}

// ── function: playBeep ──
function playBeep(frequency, duration) {
  if (!RallyGame.audioCtx) return;
  
  const osc = RallyGame.audioCtx.createOscillator();
  const gain = RallyGame.audioCtx.createGain();
  
  osc.type = 'sine';
  osc.frequency.value = frequency || 440;
  
  gain.gain.setValueAtTime(0.3, RallyGame.audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.01, RallyGame.audioCtx.currentTime + (duration || 0.2));
  
  osc.connect(gain);
  gain.connect(RallyGame.audioCtx.destination);
  
  osc.start();
  osc.stop(RallyGame.audioCtx.currentTime + (duration || 0.2) + 0.05);
}

// ── function: playCrashSound ──
function playCrashSound() {
  if (!RallyGame.audioCtx) return;
  
  // Short noise burst for crash
  const bufferSize = RallyGame.audioCtx.sampleRate * 0.3;
  const buffer = RallyGame.audioCtx.createBuffer(1, bufferSize, RallyGame.audioCtx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
  }
  
  const source = RallyGame.audioCtx.createBufferSource();
  source.buffer = buffer;
  
  const filter = RallyGame.audioCtx.createBiquadFilter();
  filter.type = 'lowpass';
  filter.frequency.value = 1500;
  
  const gain = RallyGame.audioCtx.createGain();
  gain.gain.value = 0.2;
  
  source.connect(filter);
  filter.connect(gain);
  gain.connect(RallyGame.audioCtx.destination);
  
  source.start();
}

// ── function: playFinishTone ──
function playFinishTone() {
  if (!RallyGame.audioCtx) return;
  
  // Victory arpeggio
  const notes = [523, 659, 784, 1047]; // C E G C
  notes.forEach((freq, i) => {
    setTimeout(() => playBeep(freq, 0.3), i * 150);
  });
}

// ── function: resumeAudioContext ──
function resumeAudioContext() {
  if (RallyGame.audioCtx && RallyGame.audioCtx.state === 'suspended') {
    RallyGame.audioCtx.resume();
  }
}

// ── function: setupInput ──
// ============================================================
// INPUT HANDLING - Keyboard + Touch controls
// ============================================================

function setupInput() {
  // === KEYBOARD CONTROLS ===
  document.addEventListener('keydown', (e) => {
    resumeAudioContext();
    
    switch(e.code) {
      case 'ArrowUp':
      case 'KeyW':
        RallyGame.input.up = true;
        e.preventDefault();
        break;
      case 'ArrowDown':
      case 'KeyS':
        RallyGame.input.down = true;
        e.preventDefault();
        break;
      case 'ArrowLeft':
      case 'KeyA':
        RallyGame.input.left = true;
        e.preventDefault();
        break;
      case 'ArrowRight':
      case 'KeyD':
        RallyGame.input.right = true;
        e.preventDefault();
        break;
      case 'Space':
        RallyGame.car.handbrake = true;
        RallyGame.input.handbrake = true;
        e.preventDefault();
        break;
      case 'KeyP':
        togglePause();
        e.preventDefault();
        break;
      case 'KeyC':
        toggleCamera();
        e.preventDefault();
        break;
      case 'KeyR':
        if (RallyGame.state === 'racing' || RallyGame.state === 'paused') {
          restartRace();
        }
        e.preventDefault();
        break;
    }
  });

  document.addEventListener('keyup', (e) => {
    switch(e.code) {
      case 'ArrowUp':
      case 'KeyW':
        RallyGame.input.up = false;
        break;
      case 'ArrowDown':
      case 'KeyS':
        RallyGame.input.down = false;
        break;
      case 'ArrowLeft':
      case 'KeyA':
        RallyGame.input.left = false;
        break;
      case 'ArrowRight':
      case 'KeyD':
        RallyGame.input.right = false;
        break;
      case 'Space':
        RallyGame.car.handbrake = false;
        RallyGame.input.handbrake = false;
        break;
    }
  });

  // === TOUCH CONTROLS (Mobile) ===
  setupTouchControls();
  
  // Show touch controls on mobile devices
  if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
    document.getElementById('touchControls').style.display = 'block';
  }
}

// ── function: setupTouchControls ──
function setupTouchControls() {
  const dpadBtns = document.querySelectorAll('.dpad-btn');
  
  dpadBtns.forEach(btn => {
    const dir = btn.dataset.dir;
    
    btn.addEventListener('touchstart', (e) => {
      e.preventDefault();
      resumeAudioContext();
      
      switch(dir) {
        case 'up': RallyGame.input.up = true; break;
        case 'down': RallyGame.input.down = true; break;
        case 'left': RallyGame.input.left = true; break;
        case 'right': RallyGame.input.right = true; break;
      }
    }, { passive: false });

    btn.addEventListener('touchend', (e) => {
      e.preventDefault();
      
      switch(dir) {
        case 'up': RallyGame.input.up = false; break;
        case 'down': RallyGame.input.down = false; break;
        case 'left': RallyGame.input.left = false; break;
        case 'right': RallyGame.input.right = false; break;
      }
    }, { passive: false });

    btn.addEventListener('touchcancel', (e) => {
      switch(dir) {
        case 'up': RallyGame.input.up = false; break;
        case 'down': RallyGame.input.down = false; break;
        case 'left': RallyGame.input.left = false; break;
        case 'right': RallyGame.input.right = false; break;
      }
    });
  });

  const actionBtns = document.querySelectorAll('.action-btn');
  
  actionBtns.forEach(btn => {
    const action = btn.dataset.action;
    
    btn.addEventListener('touchstart', (e) => {
      e.preventDefault();
      resumeAudioContext();
      
      switch(action) {
        case 'gas': RallyGame.input.up = true; break;
        case 'brake': RallyGame.input.down = true; break;
        case 'handbrake': 
          RallyGame.car.handbrake = true; 
          RallyGame.input.handbrake = true; 
          break;
      }
    }, { passive: false });

    btn.addEventListener('touchend', (e) => {
      e.preventDefault();
      
      switch(action) {
        case 'gas': RallyGame.input.up = false; break;
        case 'brake': RallyGame.input.down = false; break;
        case 'handbrake': 
          RallyGame.car.handbrake = false; 
          RallyGame.input.handbrake = false; 
          break;
      }
    }, { passive: false });
  });
}

// ── function: togglePause ──
function togglePause() {
  if (RallyGame.state === 'racing') {
    RallyGame.state = 'paused';
    document.getElementById('pauseMenu').style.display = 'flex';
  } else if (RallyGame.state === 'paused') {
    resumeRace();
  }
}

// ── function: toggleCamera ──
function toggleCamera() {
  RallyGame.cameraMode = RallyGame.cameraMode === 'chase' ? 'cinematic' : 'chase';
}

// ── function: resetInput ──
function resetInput() {
  RallyGame.input = { up: false, down: false, left: false, right: false, handbrake: false };
  RallyGame.car.handbrake = false;
}

// ── function: updateHUD ──
// ============================================================
// HUD & UI UPDATES - Speedometer, lap info, minimap
// ============================================================

function updateHUD() {
  const car = RallyGame.car;
  const race = RallyGame.race;
  
  // Speed (km/h)
  const speedKmh = Math.abs(car.speed * 3.6).toFixed(0);
  document.getElementById('speedValue').textContent = speedKmh;
  
  // Gear display
  document.getElementById('gearValue').textContent = car.gear;
  
  // RPM bar
  drawRPMBar();
  
  // Lap info
  const currentLap = Math.min(race.lap, RallyGame.TOTAL_LAPS);
  document.getElementById('lapValue').textContent = `${currentLap}/${RallyGame.TOTAL_LAPS}`;
  
  // Total time
  if (race.startTime > 0) {
    race.elapsedTime = (performance.now() - race.startTime) / 1000;
    document.getElementById('timeValue').textContent = formatTime(race.elapsedTime);
  }
  
  // Best lap
  if (isFinite(race.bestLapTime)) {
    document.getElementById('bestLapValue').textContent = formatTime(race.bestLapTime);
  }

  // Draw minimap
  drawMinimap();
}

// ── function: drawRPMBar ──
function drawRPMBar() {
  const canvas = document.getElementById('rpmBar');
  if (!canvas) return;
  
  const ctx = canvas.getContext('2d');
  const w = canvas.width;
  const h = canvas.height;
  
  ctx.clearRect(0, 0, w, h);
  
  // Background gradient (green -> yellow -> red)
  const rpmRatio = RallyGame.car.rpm / 7500;
  const fillWidth = w * Math.min(rpmRatio, 1.0);
  
  // Draw segments
  for (let x = 0; x < w; x++) {
    const ratio = x / w;
    let color;
    
    if (ratio < 0.5) {
      color = `rgb(${Math.floor(ratio * 2 * 180)}, ${Math.floor(200 - ratio * 2 * 40)}, 0)`;
    } else if (ratio < 0.75) {
      const t = (ratio - 0.5) / 0.25;
      color = `rgb(${Math.floor(180 + t * 75)}, ${Math.floor(160 - t * 140)}, 0)`;
    } else {
      const t = (ratio - 0.75) / 0.25;
      color = `rgb(${Math.floor(255 - t * 50)}, ${Math.floor(20 + t * 0)}, ${Math.floor(t * 30)})`;
    }
    
    if (x < fillWidth) {
      ctx.fillStyle = color;
      ctx.fillRect(x, 0, 1, h);
    } else {
      ctx.fillStyle = 'rgba(40,40,40,0.5)';
      ctx.fillRect(x, 0, 1, h);
    }
  }

  // Redline zone marker
  const redlineX = w * (6500 / 7500);
  ctx.fillStyle = 'rgba(255,0,0,0.3)';
  ctx.fillRect(redlineX, 0, w - redlineX, h);
}

// ── function: drawMinimap ──
function drawMinimap() {
  const canvas = document.getElementById('minimap');
  if (!canvas) return;
  
  const ctx = canvas.getContext('2d');
  const w = canvas.width;
  const h = canvas.height;
  
  // Clear with semi-transparent background for trail effect
  ctx.fillStyle = 'rgba(0, 30, 10, 0.85)';
  ctx.fillRect(0, 0, w, h);

  if (!RallyGame.trackCurve) return;

  // Calculate track bounds
  let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
  
  for (let i = 0; i < RallyGame.trackPoints.length; i += 5) {
    const p = RallyGame.trackPoints[i].pos;
    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 = 20;
  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 outline
  ctx.strokeStyle = 'rgba(100, 100, 100, 0.6)';
  ctx.lineWidth = RallyGame.TRACK_WIDTH * scale * 0.8;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  
  ctx.beginPath();
  for (let i = 0; i <= RallyGame.trackPoints.length; i++) {
    const idx = i % RallyGame.trackPoints.length;
    const p = RallyGame.trackPoints[idx].pos;
    const x = (p.x - minX) * scale + offsetX;
    const z = (p.z - minZ) * scale + offsetZ;
    
    if (i === 0) ctx.moveTo(x, z);
    else ctx.lineTo(x, z);
  }
  ctx.stroke();

  // Draw track center line
  ctx.strokeStyle = 'rgba(180, 180, 180, 0.4)';
  ctx.lineWidth = 2;
  
  ctx.beginPath();
  for (let i = 0; i <= RallyGame.trackPoints.length; i++) {
    const idx = i % RallyGame.trackPoints.length;
    const p = RallyGame.trackPoints[idx].pos;
    const x = (p.x - minX) * scale + offsetX;
    const z = (p.z - minZ) * scale + offsetZ;
    
    if (i === 0) ctx.moveTo(x, z);
    else ctx.lineTo(x, z);
  }
  ctx.stroke();

  // Draw start/finish line marker
  const startPoint = RallyGame.trackCurve.getPointAt(0.01);
  const sx = (startPoint.x - minX) * scale + offsetX;
  const sz = (startPoint.z - minZ) * scale + offsetZ;
  
  ctx.strokeStyle = '#fff';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(sx - 4, sz);
  ctx.lineTo(sx + 4, sz);
  ctx.stroke();

  // Draw car position (with heading indicator)
  const carPos = RallyGame.car.position;
  const cx = (carPos.x - minX) * scale + offsetX;
  const cz = (carPos.z - minZ) * scale + offsetZ;

  // Car dot
  ctx.fillStyle = '#ff3333';
  ctx.beginPath();
  ctx.arc(cx, cz, 4, 0, Math.PI * 2);
  ctx.fill();
  
  // Heading indicator line
  const headLen = 10;
  ctx.strokeStyle = '#ffcc00';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(cx, cz);
  ctx.lineTo(
    cx + Math.sin(RallyGame.car.heading) * headLen,
    cz + Math.cos(RallyGame.car.heading) * headLen
  );
  ctx.stroke();

  // Car glow effect
  ctx.fillStyle = 'rgba(255, 50, 50, 0.3)';
  ctx.beginPath();
  ctx.arc(cx, cz, 8, 0, Math.PI * 2);
  ctx.fill();
}

// ── function: formatTime ──
function formatTime(seconds) {
  if (!isFinite(seconds)) return '--:--.--';
  
  const mins = Math.floor(seconds / 60);
  const secs = seconds % 60;
  return `${mins}:${secs.toFixed(2).padStart(5, '0')}`;
}

// ── function: updateCountdown ──
// Countdown display
function updateCountdown(dt) {
  RallyGame.race.countdownTimer += dt;
  
  if (RallyGame.race.countdownTimer >= 1.0) {
    RallyGame.race.countdownTimer = 0;
    RallyGame.race.countdownValue--;
    
    const textEl = document.getElementById('countdownText');
    
    if (RallyGame.race.countdownValue > 0) {
      textEl.textContent = RallyGame.race.countdownValue.toString();
      textEl.style.opacity = '1';
      textEl.style.color = RallyGame.race.countdownValue === 1 ? '#ffcc00' : '#ffffff';
      
      playBeep(440 + (3 - RallyGame.race.countdownValue) * 220, 0.2);
    } else {
      textEl.textContent = 'GO!';
      textEl.style.opacity = '1';
      textEl.style.color = '#00ff00';
      
      playBeep(880, 0.3);
      
      // Start the race after GO!
      setTimeout(() => {
        RallyGame.state = 'racing';
        RallyGame.race.startTime = performance.now();
        RallyGame.race.lapStartTime = performance.now();
        document.getElementById('countdownText').style.opacity = '0';
        document.getElementById('hud').style.display = 'block';
      }, 500);
      
      return; // Stop countdown updates
    }
    
    // Fade out after a moment
    setTimeout(() => {
      textEl.style.opacity = '0';
    }, 700);
  }
}

// ── function: initScene ──
// ============================================================
// SCENE SETUP - Three.js renderer, lighting, fog
// ============================================================

function initScene() {
  // Renderer
  const canvas = document.getElementById('gameCanvas');
  RallyGame.renderer = new THREE.WebGLRenderer({ 
    canvas: canvas, 
    antialias: true,
    alpha: false
  });
  RallyGame.renderer.setSize(window.innerWidth, window.innerHeight);
  RallyGame.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  RallyGame.renderer.shadowMap.enabled = true;
  RallyGame.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  RallyGame.renderer.toneMapping = THREE.ACESFilmicToneMapping;
  RallyGame.toneMappingExposure = 1.0;

  // Scene
  RallyGame.scene = new THREE.Scene();
  
  // Sky color (gradient-like via background)
  RallyGame.scene.background = new THREE.Color(0x87CEEB);
  
  // Fog for distance fade
  RallyGame.scene.fog = new THREE.FogExp2(0xc4d8e8, 0.006);

  // Camera
  RallyGame.camera = new THREE.PerspectiveCamera(
    65, 
    window.innerWidth / window.innerHeight, 
    0.1, 
    500
  );

  // === LIGHTING ===
  
  // Ambient light (soft fill)
  const ambientLight = new THREE.AmbientLight(0x6688aa, 0.5);
  RallyGame.scene.add(ambientLight);

  // Hemisphere light (sky/ground color blend)
  const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x3d5c2a, 0.4);
  RallyGame.scene.add(hemiLight);

  // Directional sun light with shadows
  const sunLight = new THREE.DirectionalLight(0xffeedd, 1.0);
  sunLight.position.set(50, 80, 30);
  sunLight.castShadow = true;
  
  // Configure shadow properties
  sunLight.shadow.mapSize.width = 2048;
  sunLight.shadow.mapSize.height = 2048;
  sunLight.shadow.camera.near = 1;
  sunLight.shadow.camera.far = 250;
  sunLight.shadow.camera.left = -60;
  sunLight.shadow.camera.right = 60;
  sunLight.shadow.camera.top = 60;
  sunLight.shadow.camera.bottom = -60;
  sunLight.shadow.bias = -0.001;
  
  RallyGame.scene.add(sunLight);
  RallyGame.sunLight = sunLight;

  // Secondary fill light from opposite side
  const fillLight = new THREE.DirectionalLight(0x8899bb, 0.3);
  fillLight.position.set(-30, 40, -20);
  RallyGame.scene.add(fillLight);

  // === RESIZE HANDLER ===
  window.addEventListener('resize', onWindowResize);
  
  // Clock for delta time
  RallyGame.clock = new THREE.Clock();
}

// ── function: onWindowResize ──
function onWindowResize() {
  if (!RallyGame.camera || !RallyGame.renderer) return;
  
  RallyGame.camera.aspect = window.innerWidth / window.innerHeight;
  RallyGame.camera.updateProjectionMatrix();
  RallyGame.renderer.setSize(window.innerWidth, window.innerHeight);
}

// ── function: initGame ──
// ============================================================
// GAME INITIALIZATION & RESET
// ============================================================

function initGame() {
  // Initialize Three.js scene
  initScene();
  
  // Generate track and scenery
  generateTrack();
  generateScenery();
  
  // Create car model
  RallyGame.carGroup = createCarModel();
  RallyGame.scene.add(RallyGame.carGroup);
  
  // Position car at start of track
  resetCarPosition();
  
  // Setup input handlers
  setupInput();
  
  // Initialize audio (will activate on first user interaction)
  initAudio();
  
  // Get minimap context
  RallyGame.minimapCtx = document.getElementById('minimap').getContext('2d');
  
  // Setup UI button handlers
  document.getElementById('startBtn').addEventListener('click', startRace);
  document.getElementById('resumeBtn').addEventListener('click', resumeRace);
  document.getElementById('restartPauseBtn').addEventListener('click', restartRace);
  document.getElementById('restartFinishBtn').addEventListener('click', () => {
    document.getElementById('finishScreen').style.display = 'none';
    restartRace();
  });

  // Start render loop
  requestAnimationFrame(gameLoop);
}

// ── function: resetCarPosition ──
function resetCarPosition() {
  const startPoint = RallyGame.trackCurve.getPointAt(0.01);
  const tangent = RallyGame.trackCurve.getTangentAt(0.01);
  
  RallyGame.car.position.copy(startPoint);
  RallyGame.car.position.y += 0.5;
  RallyGame.car.velocity.set(0, 0, 0);
  RallyGame.car.heading = Math.atan2(tangent.x, tangent.z);
  RallyGame.car.speed = 0;
  RallyGame.car.angularVel = 0;
  RallyGame.car.driftAngle = 0;
  RallyGame.car.gear = 'N';
  RallyGame.car.rpm = 800;
  RallyGame.car.handbrake = false;

  // Update car mesh position
  updateCarMesh();
  
  // Reset camera position to behind car
  const camDist = 12;
  RallyGame.cameraPos.set(
    RallyGame.car.position.x - Math.sin(RallyGame.car.heading) * camDist,
    RallyGame.car.position.y + 4,
    RallyGame.car.position.z - Math.cos(RallyGame.car.heading) * camDist
  );
  RallyGame.cameraTarget.copy(RallyGame.car.position);
}

// ── function: updateCarMesh ──
function updateCarMesh() {
  if (!RallyGame.carGroup) return;
  
  const car = RallyGame.car;
  
  // Position
  RallyGame.carGroup.position.copy(car.position);
  
  // Rotation (heading + slight tilt based on drift/steering)
  RallyGame.carGroup.rotation.y = car.heading;
  
  // Body roll during turns
  const steerInput = (RallyGame.input.left ? -1 : 0) + (RallyGame.input.right ? 1 : 0);
  const speedFactor = Math.abs(car.speed) / RallyGame.CAR_MAX_SPEED;
  RallyGame.carGroup.rotation.z = -steerInput * car.driftAngle * 0.3;
  
  // Wheel rotation based on speed
  if (RallyGame.wheels) {
    const wheelRot = car.speed * 0.5;
    RallyGame.wheels.forEach((wheel, i) => {
      wheel.children[0].rotation.x += wheelRot * 0.167; // approximate per-frame rotation
      
      // Front wheels steer
      if (i < 2) {
        const steerAngle = steerInput * 0.35 * Math.min(speedFactor + 0.2, 1);
        wheel.rotation.y = -steerAngle;
      }
    });
  }

  // Update shadow light to follow car
  if (RallyGame.sunLight && RallyGame.sunLight.target) {
    RallyGame.sunLight.position.set(
      car.position.x + 50, 
      80, 
      car.position.z + 30
    );
    RallyGame.sunLight.target.position.copy(car.position);
    RallyGame.sunLight.target.updateMatrixWorld();
  }
}

// ── function: startRace ──
function startRace() {
  document.getElementById('startScreen').style.display = 'none';
  
  // Reset race state
  RallyGame.race.lap = 1;
  RallyGame.race.currentCheckpoint = 0;
  RallyGame.race.startTime = 0;
  RallyGame.race.elapsedTime = 0;
  RallyGame.race.bestLapTime = Infinity;
  RallyGame.race.countdownValue = 3;
  RallyGame.race.countdownTimer = 0;

  resetCarPosition();
  resetInput();
  
  // Start countdown
  RallyGame.state = 'countdown';
  
  const textEl = document.getElementById('countdownText');
  textEl.textContent = '3';
  textEl.style.opacity = '1';
  textEl.style.color = '#ffffff';
}

// ── function: resumeRace ──
function resumeRace() {
  RallyGame.state = 'racing';
  document.getElementById('pauseMenu').style.display = 'none';
  
  // Resume audio context if needed
  resumeAudioContext();
}

// ── function: restartRace ──
function restartRace() {
  // Clean up particles
  for (const particle of RallyGame.particles) {
    if (particle.mesh.parent) {
      RallyGame.scene.remove(particle.mesh);
      if (particle.mesh.material.map) particle.mesh.material.map.dispose();
      particle.mesh.material.dispose();
    }
  }
  RallyGame.particles = [];

  document.getElementById('pauseMenu').style.display = 'none';
  document.getElementById('finishScreen').style.display = 'none';
  
  startRace();
}

// ── let: lastParticleCleanup ──
// ============================================================
// MAIN GAME LOOP - Render + update cycle
// ============================================================

let lastParticleCleanup = 0;

// ── function: gameLoop ──
function gameLoop() {
  requestAnimationFrame(gameLoop);
  
  const dt = Math.min(RallyGame.clock.getDelta(), 0.05); // Cap delta time
  
  if (RallyGame.state === 'countdown') {
    updateCountdown(dt);
    
    // Still render the scene during countdown
    updateCarMesh();
    updateCamera(dt);
    RallyGame.renderer.render(RallyGame.scene, RallyGame.camera);
    return;
  }

  if (RallyGame.state === 'paused' || RallyGame.state === 'finished') {
    // Just render without updating physics
    RallyGame.renderer.render(RallyGame.scene, RallyGame.camera);
    return;
  }

  if (RallyGame.state !== 'racing') return;

  // === UPDATE PHYSICS ===
  updateCarPhysics(dt);
  
  // === UPDATE CAR MESH ===
  updateCarMesh();
  
  // === UPDATE CAMERA ===
  updateCamera(dt);
  
  // === UPDATE PARTICLES ===
  const speedKmh = Math.abs(RallyGame.car.speed * 3.6);
  updateParticles(dt, speedKmh);
  
  // Periodic particle cleanup (every second)
  lastParticleCleanup += dt;
  if (lastParticleCleanup > 1.0) {
    lastParticleCleanup = 0;
    cleanupParticles();
  }

  // === UPDATE AUDIO ===
  updateEngineSound();
  
  // === UPDATE HUD ===
  updateHUD();
  
  // === RENDER ===
  RallyGame.renderer.render(RallyGame.scene, RallyGame.camera);
}

// ── function: buildTrackMesh ──
function buildTrackMesh(points, curve) {
  const width = RallyGame.TRACK_WIDTH;
  const segments = points.length;
  const vertices = [];
  const uvs = [];
  const indices = [];
  const colors = [];

  for (let i = 0; i < segments; i++) {
    const p = points[i];
    const n = p.normal.clone().multiplyScalar(width / 2);
    
    // Left and right edge vertices (track surface)
    const left = new THREE.Vector3(p.pos.x - n.x, p.pos.y + 0.15, p.pos.z - n.z);
    const right = new THREE.Vector3(p.pos.x + n.x, p.pos.y + 0.15, p.pos.z + n.z);
    
    // Shoulder vertices (wider)
    const shoulderWidth = width * 0.8;
    const sn = p.normal.clone().multiplyScalar(shoulderWidth / 2);
    const leftShoulder = new THREE.Vector3(p.pos.x - sn.x, p.pos.y + 0.05, p.pos.z - sn.z);
    const rightShoulder = new THREE.Vector3(p.pos.x + sn.x, p.pos.y + 0.05, p.pos.z + sn.z);

    // Main track surface (asphalt)
    vertices.push(left.x, left.y, left.z);
    vertices.push(right.x, right.y, right.z);
    
    const v = i / segments;
    uvs.push(0, v * 20);
    uvs.push(1, v * 20);
    
    // Asphalt color with slight variation
    const shade = 0.3 + Math.random() * 0.05;
    colors.push(shade, shade, shade + 0.02);
    colors.push(shade, shade, shade + 0.02);

    // Shoulder (dirt/gravel)
    vertices.push(leftShoulder.x, leftShoulder.y, leftShoulder.z);
    vertices.push(rightShoulder.x, rightShoulder.y, rightShoulder.z);
    
    uvs.push(0, v * 20);
    uvs.push(1, v * 20);
    
    colors.push(0.55, 0.45, 0.35);
    colors.push(0.55, 0.45, 0.35);

    // Indices for track surface quad (vertices 0,1 of current and next)
    const base = i * 4;
    const nextBase = ((i + 1) % segments) * 4;
    
    // Track surface triangles
    indices.push(base, base + 1, nextBase);
    indices.push(base + 1, nextBase + 1, nextBase);
    
    // Left shoulder strip (2 triangles)
    indices.push(base, nextBase, base + 2);
    indices.push(nextBase, nextBase + 2, base + 2);
    
    // Right shoulder strip (2 triangles)  
    indices.push(base + 1, nextBase + 3, nextBase + 1);
    indices.push(nextBase + 1, nextBase + 3, base + 3);
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
  geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
  geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
  geometry.setIndex(indices);
  geometry.computeVertexNormals();

  const material = new THREE.MeshLambertMaterial({ 
    vertexColors: true,
    side: THREE.DoubleSide
  });

  RallyGame.trackMesh = new THREE.Mesh(geometry, material);
  RallyGame.scene.add(RallyGame.trackMesh);

  // Add track edge lines (white dashed)
  addTrackEdges(points);
}

// ── function: getTrackInfo ──
// Find closest point on track and determine surface type
function getTrackInfo(position) {
  const curve = RallyGame.trackCurve;
  
  // Binary search for closest t value using the curve's built-in methods
  let bestT = 0;
  let bestDistSq = Infinity;
  
  // Coarse search: sample at regular intervals
  const coarseSteps = 100;
  for (let i = 0; i <= coarseSteps; i++) {
    const t = i / coarseSteps;
    const point = curve.getPointAt(t);
    
    const dx = position.x - point.x;
    const dz = position.z - point.z;
    const distSq = dx * dx + dz * dz;
    
    if (distSq < bestDistSq) {
      bestDistSq = distSq;
      bestT = t;
    }
  }

  // Fine search: refine around the best coarse result
  let fineBestT = bestT;
  let fineBestDistSq = bestDistSq;
  
  const step = 1 / (coarseSteps * 20); // Much finer resolution
  for (let t = Math.max(0, bestT - step * 5); t <= Math.min(1, bestT + step * 5); t += step) {
    const point = curve.getPointAt(t);
    
    const dx = position.x - point.x;
    const dz = position.z - point.z;
    const distSq = dx * dx + dz * dz;
    
    if (distSq < fineBestDistSq) {
      fineBestDistSq = distSq;
      fineBestT = t;
    }
  }

  const distance = Math.sqrt(fineBestDistSq);
  const halfWidth = RallyGame.TRACK_WIDTH / 2;
  
  let surfaceType = 'asphalt';
  if (distance > halfWidth && distance <= halfWidth * 1.6) {
    surfaceType = 'dirt';
  } else if (distance > halfWidth * 1.6 && distance <= halfWidth * 2.5) {
    surfaceType = 'grass';
  } else if (distance > halfWidth * 2.5) {
    surfaceType = 'mud';
  }

  return { distance, closestT: fineBestT, surfaceType };
}

// ── function: createCarModel ──
// ============================================================
// CAR MODEL - Low-poly rally car built from primitives
// ============================================================

function createCarModel() {
  const group = new THREE.Group();

  // Main body (lower) - bright rally red
  const bodyGeo = new THREE.BoxGeometry(2.0, 0.65, 4.2);
  const bodyMat = new THREE.MeshLambertMaterial({ color: 0xdd2222 });
  const body = new THREE.Mesh(bodyGeo, bodyMat);
  body.position.y = 0.5;
  group.add(body);

  // Body side stripes (white racing stripe)
  const stripeGeo = new THREE.BoxGeometry(2.02, 0.15, 4.0);
  const stripeMat = new THREE.MeshLambertMaterial({ color: 0xffffff });
  const stripe = new THREE.Mesh(stripeGeo, stripeMat);
  stripe.position.y = 0.3;
  group.add(stripe);

  // Cabin (upper) - dark tinted glass look
  const cabinGeo = new THREE.BoxGeometry(1.75, 0.6, 2.1);
  const cabinMat = new THREE.MeshLambertMaterial({ color: 0x2a2a3a });
  const cabin = new THREE.Mesh(cabinGeo, cabinMat);
  cabin.position.set(0, 1.12, -0.25);
  group.add(cabin);

  // Windshield (front glass)
  const windshieldGeo = new THREE.BoxGeometry(1.6, 0.55, 0.08);
  const glassMat = new THREE.MeshLambertMaterial({ 
    color: 0x99bbdd, transparent: true, opacity: 0.5 
  });
  const windshield = new THREE.Mesh(windshieldGeo, glassMat);
  windshield.position.set(0, 1.1, 0.78);
  windshield.rotation.x = -0.3;
  group.add(windshield);

  // Rear window
  const rearWindow = new THREE.Mesh(windshieldGeo.clone(), glassMat);
  rearWindow.position.set(0, 1.1, -1.25);
  rearWindow.rotation.x = 0.3;
  group.add(rearWindow);

  // Side windows
  const sideWinGeo = new THREE.BoxGeometry(0.08, 0.45, 1.6);
  
  const leftSideWin = new THREE.Mesh(sideWinGeo, glassMat);
  leftSideWin.position.set(-0.87, 1.12, -0.25);
  group.add(leftSideWin);
  
  const rightSideWin = new THREE.Mesh(sideWinGeo.clone(), glassMat);
  rightSideWin.position.set(0.87, 1.12, -0.25);
  group.add(rightSideWin);

  // Spoiler arms and wing
  const spoilerArmGeo = new THREE.BoxGeometry(0.1, 0.45, 0.1);
  const spoilerMat = new THREE.MeshLambertMaterial({ color: 0x222222 });
  
  const leftArm = new THREE.Mesh(spoilerArmGeo, spoilerMat);
  leftArm.position.set(-0.75, 1.35, -1.85);
  group.add(leftArm);
  
  const rightArm = new THREE.Mesh(spoilerArmGeo, spoilerMat);
  rightArm.position.set(0.75, 1.35, -1.85);
  group.add(rightArm);

  const spoilerWingGeo = new THREE.BoxGeometry(2.4, 0.08, 0.45);
  const spoilerWingMat = new THREE.MeshLambertMaterial({ color: 0x1a1a1a });
  const spoilerWing = new THREE.Mesh(spoilerWingGeo, spoilerWingMat);
  spoilerWing.position.set(0, 1.58, -1.85);
  group.add(spoilerWing);

  // Wheels with detailed rims
  const wheelGeo = new THREE.CylinderGeometry(0.38, 0.38, 0.28, 16);
  const tireMat = new THREE.MeshLambertMaterial({ color: 0x1a1a1a });
  const rimGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.3, 8);
  const rimMat = new THREE.MeshLambertMaterial({ color: 0xcccccc });

  const wheelPositions = [
    { x: -1.15, y: 0.38, z: 1.4 },   // front left
    { x: 1.15, y: 0.38, z: 1.4 },    // front right
    { x: -1.15, y: 0.38, z: -1.4 },  // rear left
    { x: 1.15, y: 0.38, z: -1.4 }    // rear right
  ];

  RallyGame.wheels = [];
  wheelPositions.forEach((pos) => {
    const wheelGroup = new THREE.Group();
    
    const tire = new THREE.Mesh(wheelGeo, tireMat);
    tire.rotation.z = Math.PI / 2;
    wheelGroup.add(tire);
    
    const rim = new THREE.Mesh(rimGeo, rimMat);
    rim.rotation.z = Math.PI / 2;
    wheelGroup.add(rim);
    
    wheelGroup.position.set(pos.x, pos.y, pos.z);
    group.add(wheelGroup);
    RallyGame.wheels.push(wheelGroup);
  });

  // Headlights (front) - bright with glow effect
  const headlightGeo = new THREE.BoxGeometry(0.4, 0.25, 0.12);
  const headlightMat = new THREE.MeshLambertMaterial({ 
    color: 0xffffee, emissive: 0xffffaa, emissiveIntensity: 0.8 
  });
  
  const leftHeadlight = new THREE.Mesh(headlightGeo, headlightMat);
  leftHeadlight.position.set(-0.65, 0.55, 2.14);
  group.add(leftHeadlight);
  
  const rightHeadlight = new THREE.Mesh(headlightGeo.clone(), headlightMat);
  rightHeadlight.position.set(0.65, 0.55, 2.14);
  group.add(rightHeadlight);

  // Headlight housing (chrome)
  const housingGeo = new THREE.BoxGeometry(0.5, 0.35, 0.08);
  const chromeMat = new THREE.MeshLambertMaterial({ color: 0xaaaaaa });
  
  const leftHousing = new THREE.Mesh(housingGeo, chromeMat);
  leftHousing.position.set(-0.65, 0.55, 2.18);
  group.add(leftHousing);
  
  const rightHousing = new THREE.Mesh(housingGeo.clone(), chromeMat);
  rightHousing.position.set(0.65, 0.55, 2.18);
  group.add(rightHousing);

  // Taillights (rear) - bright red with glow
  const taillightGeo = new THREE.BoxGeometry(0.35, 0.2, 0.12);
  const taillightMat = new THREE.MeshLambertMaterial({ 
    color: 0xff0000, emissive: 0xff2200, emissiveIntensity: 0.6 
  });
  
  const leftTaillight = new THREE.Mesh(taillightGeo, taillightMat);
  leftTaillight.position.set(-0.7, 0.55, -2.14);
  group.add(leftTaillight);
  
  const rightTaillight = new THREE.Mesh(taillightGeo.clone(), taillightMat);
  rightTaillight.position.set(0.7, 0.55, -2.14);
  group.add(rightTaillight);

  // Rally number on sides (both sides)
  const numberCanvas = document.createElement('canvas');
  numberCanvas.width = 128;
  numberCanvas.height = 96;
  const nctx = numberCanvas.getContext('2d');
  nctx.fillStyle = '#ffffff';
  nctx.fillRect(0, 0, 128, 96);
  nctx.strokeStyle = '#cc2222';
  nctx.lineWidth = 5;
  nctx.strokeRect(4, 4, 120, 88);
  nctx.fillStyle = '#cc2222';
  nctx.font = 'bold 60px Arial';
  nctx.textAlign = 'center';
  nctx.textBaseline = 'middle';
  nctx.fillText('7', 64, 52);
  
  const numberTexture = new THREE.CanvasTexture(numberCanvas);
  const numberMat = new THREE.MeshBasicMaterial({ map: numberTexture });
  const numberGeo = new THREE.PlaneGeometry(0.9, 0.65);
  
  const leftNumber = new THREE.Mesh(numberGeo, numberMat);
  leftNumber.position.set(-1.03, 0.7, -0.25);
  leftNumber.rotation.y = -Math.PI / 2;
  group.add(leftNumber);

  const rightNumber = new THREE.Mesh(numberGeo.clone(), numberMat);
  rightNumber.position.set(1.03, 0.7, -0.25);
  rightNumber.rotation.y = Math.PI / 2;
  group.add(rightNumber);

  // Front and rear bumpers (black)
  const bumperGeo = new THREE.BoxGeometry(2.15, 0.18, 0.15);
  const bumperMat = new THREE.MeshLambertMaterial({ color: 0x333333 });
  
  const frontBumper = new THREE.Mesh(bumperGeo, bumperMat);
  frontBumper.position.set(0, 0.28, 2.17);
  group.add(frontBumper);

  const rearBumper = new THREE.Mesh(bumperGeo.clone(), bumperMat);
  rearBumper.position.set(0, 0.28, -2.17);
  group.add(rearBumper);

  // Roof rack (silver bars)
  const roofRackGeo = new THREE.BoxGeometry(1.5, 0.06, 1.7);
  const roofRackMat = new THREE.MeshLambertMaterial({ color: 0x888888 });
  const roofRack = new THREE.Mesh(roofRackGeo, roofRackMat);
  roofRack.position.set(0, 1.45, -0.25);
  group.add(roofRack);

  // Fire extinguisher on roof rack (red cylinder)
  const extGeo = new THREE.CylinderGeometry(0.07, 0.07, 0.55, 8);
  const extMat = new THREE.MeshLambertMaterial({ color: 0xff3300 });
  const extinguisher = new THREE.Mesh(extGeo, extMat);
  extinguisher.position.set(0.45, 1.62, -0.55);
  group.add(extinguisher);

  // Exhaust pipe (rear)
  const exhaustGeo = new THREE.CylinderGeometry(0.06, 0.08, 0.3, 8);
  const exhaustMat = new THREE.MeshLambertMaterial({ color: 0x555555 });
  const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
  exhaust.rotation.x = Math.PI / 2;
  exhaust.position.set(0.6, 0.3, -2.25);
  group.add(exhaust);

  // Side mirrors
  const mirrorGeo = new THREE.BoxGeometry(0.15, 0.12, 0.08);
  const mirrorMat = new THREE.MeshLambertMaterial({ color: 0x333333 });
  
  const leftMirror = new THREE.Mesh(mirrorGeo, mirrorMat);
  leftMirror.position.set(-1.05, 1.0, 0.6);
  group.add(leftMirror);
  
  const rightMirror = new THREE.Mesh(mirrorGeo.clone(), mirrorMat);
  rightMirror.position.set(1.05, 1.0, 0.6);
  group.add(rightMirror);

  return group;
}

// ── main ──
// ============================================================
// MAIN ENTRY POINT - Initialize and start the game
// ============================================================

(function() {
  // Wait for Three.js to be loaded before initializing
  if (typeof THREE === 'undefined') {
    console.error('Three.js not loaded!');
    return;
  }

  // Start the game
  initGame();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"55ba0957-6332-4ba6-8e90-11e2fcadc26e","tokensIn":1376407,"tokensOut":38271,"tokensTotal":1414678,"turns":42,"toolCalls":41,"failedToolCalls":1,"timestamp":"2026-07-16T04:05:37.057Z"} -->