← Rally results

Rally

Qwen 3.6 35b a3b · 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 42,303 bytes · SHA-256 f0f0defcca89
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rally Rush 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; }
#app { width: 100%; height: 100%; position: relative; }
#gameCanvas { width: 100%; height: 100%; display: block; }
/* ── css: hud ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
#app { width: 100%; height: 100%; position: relative; }
#gameCanvas { width: 100%; height: 100%; display: block; }
#hudCanvas {
  position: absolute; bottom: 10px; right: 10px;
  border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.5);
  image-rendering: auto; pointer-events: none;
}
#touchControls { display: none; position: absolute; bottom: 0; left: 0; width: 100%; height: 200px; pointer-events: none; z-index: 10; }
#touchControls > div { pointer-events: auto; }
#dpad { position: absolute; left: 20px; bottom: 20px; width: 150px; height: 150px; }
#dpad > div { position: absolute; width: 50px; height: 50px; background: rgba(255,255,255,0.2); border-radius: 8px; }
#dpadUp { top: 0; left: 50px; }
#dpadDown { bottom: 0; left: 50px; }
#dpadLeft { top: 50px; left: 0; }
#dpadRight { top: 50px; right: 0; }
#pedals { position: absolute; right: 20px; bottom: 20px; }
#gasBtn { position: absolute; bottom: 0; right: 0; width: 70px; height: 70px; background: rgba(0,255,0,0.25); border-radius: 50%; }
#brakeBtn { position: absolute; bottom: 80px; right: 10px; width: 55px; height: 55px; background: rgba(255,0,0,0.25); border-radius: 50%; }
#hbBtn { position: absolute; bottom: 0; right: 85px; width: 55px; height: 55px; background: rgba(255,255,0,0.25); border-radius: 50%; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<canvas id="hudCanvas" width="512" height="384"></canvas>
<div id="touchControls" style="display:none;position:absolute;bottom:0;left:0;width:100%;height:200px;pointer-events:none;">
  <div id="dpad" style="position:absolute;left:20px;bottom:20px;width:150px;height:150px;pointer-events:auto;">
    <div id="dpadUp" style="position:absolute;top:0;left:50px;width:50px;height:50px;background:rgba(255,255,255,0.2);border-radius:8px;"></div>
    <div id="dpadDown" style="position:absolute;bottom:0;left:50px;width:50px;height:50px;background:rgba(255,255,255,0.2);border-radius:8px;"></div>
    <div id="dpadLeft" style="position:absolute;top:50px;left:0;width:50px;height:50px;background:rgba(255,255,255,0.2);border-radius:8px;"></div>
    <div id="dpadRight" style="position:absolute;top:50px;right:0;width:50px;height:50px;background:rgba(255,255,255,0.2);border-radius:8px;"></div>
  </div>
  <div id="pedals" style="position:absolute;right:20px;bottom:20px;pointer-events:auto;">
    <div id="gasBtn" style="position:absolute;bottom:0;right:0;width:70px;height:70px;background:rgba(0,255,0,0.25);border-radius:50%;"></div>
    <div id="brakeBtn" style="position:absolute;bottom:80px;right:10px;width:55px;height:55px;background:rgba(255,0,0,0.25);border-radius:50%;"></div>
    <div id="hbBtn" style="position:absolute;bottom:0;right:85px;width:55px;height:55px;background:rgba(255,255,255,0.25);border-radius:50%;"></div>
  </div>
</div>
<script>
'use strict';

// ── const: TRACK_POINTS ──
// ===== CONSTANTS =====
const TRACK_POINTS = [
  {x:0, z:0, w:18, h:0},
  {x:40, z:-10, w:18, h:2},
  {x:80, z:5, w:20, h:5},
  {x:110, z:30, w:18, h:8},
  {x:120, z:70, w:16, h:10},
  {x:100, z:110, w:18, h:8},
  {x:60, z:130, w:20, h:5},
  {x:20, z:120, w:18, h:3},
  {x:-10, z:90, w:16, h:0},
  {x:-20, z:50, w:18, h:-2},
  {x:-10, z:10, w:20, h:-5},
  {x:10, z:-20, w:18, h:-3},
  {x:30, z:-35, w:16, h:0},
  {x:55, z:-30, w:18, h:2},
  {x:70, z:-10, w:20, h:4},
  {x:65, z:15, w:18, h:6},
  {x:45, z:35, w:16, h:7},
  {x:20, z:40, w:18, h:5},
  {x:0, z:30, w:20, h:3},
  {x:-15, z:10, w:18, h:1},
];

// ── const: TRACK_SEGMENTS ──
const TRACK_SEGMENTS = 12;

// ── const: TRACK_LENGTH_EST ──
const TRACK_LENGTH_EST = 500;

// ── const: TOTAL_LAPS ──
const TOTAL_LAPS = 3;

// ── const: GRAVITY ──
const GRAVITY = -20;

// ── const: MAX_SPEED_ASPHALT ──
const MAX_SPEED_ASPHALT = 65;

// ── const: MAX_SPEED_DIRT ──
const MAX_SPEED_DIRT = 45;

// ── const: MAX_SPEED_GRASS ──
const MAX_SPEED_GRASS = 30;

// ── const: MAX_SPEED_MUD ──
const MAX_SPEED_MUD = 20;

// ── const: ACCEL ──
const ACCEL = 28;

// ── const: BRAKE ──
const BRAKE = 35;

// ── const: STEER_BASE ──
const STEER_BASE = 2.2;

// ── const: STEER_SPEED_MULT ──
const STEER_SPEED_MULT = 0.008;

// ── const: DRIFT_FACTOR ──
const DRIFT_FACTOR = 0.92;

// ── const: GRIP_ASPHALT ──
const GRIP_ASPHALT = 0.85;

// ── const: GRIP_DIRT ──
const GRIP_DIRT = 0.55;

// ── const: GRIP_GRASS ──
const GRIP_GRASS = 0.35;

// ── const: GRIP_MUD ──
const GRIP_MUD = 0.2;

// ── const: CHECKPOINT_COUNT ──
const CHECKPOINT_COUNT = 12;

// ── const: DUST_PARTICLE_COUNT ──
const DUST_PARTICLE_COUNT = 80;

// ── const: SPARK_COUNT ──
const SPARK_COUNT = 30;

// ── function: catmullRom ──
// ===== TRACK MATH =====
function catmullRom(p0, p1, p2, p3, t) {
  const t2 = t * t, t3 = t2 * t;
  return {
    x: 0.5 * ((2*p1.x) + (-p0.x+p2.x)*t + (2*p0.x-5*p1.x+4*p2.x-p3.x)*t2 + (-p0.x+3*p1.x-3*p2.x+p3.x)*t3),
    z: 0.5 * ((2*p1.z) + (-p0.z+p2.z)*t + (2*p0.z-5*p1.z+4*p2.z-p3.z)*t2 + (-p0.z+3*p1.z-3*p2.z+p3.z)*t3),
    h: 0.5 * ((2*p1.h) + (-p0.h+p2.h)*t + (2*p0.h-5*p1.h+4*p2.h-p3.h)*t2 + (-p0.h+3*p1.h-3*p2.h+p3.h)*t3),
  };
}

// ── function: getTrackPoint ──
function getTrackPoint(t) {
  // t in [0, 1) along the track
  const n = TRACK_POINTS.length;
  const scaledT = ((t % 1) + 1) % 1;
  const segLen = 1 / n;
  const seg = Math.floor(scaledT / segLen) % n;
  const localT = (scaledT - seg * segLen) / segLen;
  const p0 = TRACK_POINTS[(seg - 1 + n) % n];
  const p1 = TRACK_POINTS[seg];
  const p2 = TRACK_POINTS[(seg + 1) % n];
  const p3 = TRACK_POINTS[(seg + 2) % n];
  return catmullRom(p0, p1, p2, p3, localT);
}

// ── function: getTrackDirection ──
function getTrackDirection(t) {
  const eps = 0.001;
  const a = getTrackPoint(t);
  const b = getTrackPoint(t + eps);
  const dx = b.x - a.x, dz = b.z - a.z;
  const len = Math.sqrt(dx*dx + dz*dz) || 1;
  return { x: dx/len, z: dz/len };
}

// ── function: getClosestTrack ──
function getClosestTrack(t) {
  // Find closest point on track to a given position
  let bestT = 0, bestDist = Infinity;
  for (let i = 0; i <= 360; i++) {
    const tt = i / 360;
    const p = getTrackPoint(tt);
    const dx = p.x - t.x, dz = p.z - t.z;
    const d = dx*dx + dz*dz;
    if (d < bestDist) { bestDist = d; bestT = tt; }
  }
  // Refine
  for (let i = 0; i < 20; i++) {
    const delta = 0.001 / (i + 1);
    const a = getTrackPoint(bestT - delta);
    const b = getTrackPoint(bestT + delta);
    const da = (a.x-t.x)**2 + (a.z-t.z)**2;
    const db = (b.x-t.x)**2 + (b.z-t.z)**2;
    if (da < bestDist) { bestDist = da; bestT = bestT - delta; }
    else if (db < bestDist) { bestDist = db; bestT = bestT + delta; }
  }
  return { t: ((bestT % 1) + 1) % 1, dist: Math.sqrt(bestDist) };
}

// ── function: getSurfaceAt ──
function getSurfaceAt(closestT, lateralOffset) {
  const halfW = getTrackPoint(closestT).w / 2;
  const absOff = Math.abs(lateralOffset);
  if (absOff < halfW * 0.4) return { type: 'asphalt', grip: GRIP_ASPHALT, maxSpeed: MAX_SPEED_ASPHALT, color: '#555' };
  if (absOff < halfW * 0.75) return { type: 'dirt', grip: GRIP_DIRT, maxSpeed: MAX_SPEED_DIRT, color: '#8B7355' };
  if (absOff < halfW * 1.5) return { type: 'grass', grip: GRIP_GRASS, maxSpeed: MAX_SPEED_GRASS, color: '#4a7c3f' };
  return { type: 'mud', grip: GRIP_MUD, maxSpeed: MAX_SPEED_MUD, color: '#3d2b1f' };
}

// ── function: getTrackWidthAt ──
function getTrackWidthAt(t) {
  return getTrackPoint(t).w;
}

// ── function: createCar ──
// ===== CAR PHYSICS =====
function createCar() {
  return {
    x: 0, z: 0, y: 0,
    heading: 0,
    speed: 0,
    lateralVel: 0,
    driftAngle: 0,
    onTrack: true,
    surfaceType: 'asphalt',
    gear: 1,
    rpm: 800,
    lap: 0,
    lastCheckpoint: -1,
    checkpointsPassed: [],
    lapStartTime: 0,
    lapTimes: [],
    bestLap: Infinity,
    finished: false,
    totalElapsed: 0,
    bounceTimer: 0,
    skidMarks: [],
  };
}

// ── function: updateCar ──
function updateCar(car, dt, input) {
  const closest = getClosestTrack({x: car.x, z: car.z});
  const trackPt = getTrackPoint(closest.t);
  const dir = getTrackDirection(closest.t);
  // Lateral offset from track center
  const rightX = -dir.z, rightZ = dir.x;
  const toCarX = car.x - trackPt.x, toCarZ = car.z - trackPt.z;
  let latOff = toCarX * rightX + toCarZ * rightZ;
  const surface = getSurfaceAt(closest.t, latOff);
  car.surfaceType = surface.type;
  car.onTrack = Math.abs(latOff) < trackPt.w / 2 * 0.75;

  // Max speed based on surface
  let maxSpd = surface.maxSpeed;
  let grip = surface.grip;

  // Acceleration
  if (input.accel) {
    car.speed += ACCEL * dt * (car.onTrack ? 1 : 0.5);
  }
  if (input.brake) {
    car.speed -= BRAKE * dt;
  }
  if (input.reverse) {
    car.speed -= ACCEL * 0.4 * dt;
  }

  // Handbrake
  if (input.handbrake) {
    grip *= 0.3;
    car.driftAngle *= 1.02;
  }

  // Clamp speed
  car.speed = Math.max(-15, Math.min(maxSpd, car.speed));

  // Steering
  const speedSteer = 1 - Math.abs(car.speed) / (maxSpd + 10);
  const steerAmt = input.steer * STEER_BASE * (0.3 + 0.7 * speedSteer);
  const speedFactor = Math.min(Math.abs(car.speed) / 10, 1) * STEER_SPEED_MULT * 100;
  car.heading += steerAmt * speedFactor * dt * (car.speed >= 0 ? 1 : -1);

  // Drift physics
  const forwardX = Math.cos(car.heading), forwardZ = Math.sin(car.heading);
  const targetVelX = forwardX * car.speed;
  const targetVelZ = forwardZ * car.speed;
  const lateralMix = grip * dt * 8;
  car.lateralVel = car.lateralVel * (1 - lateralMix) + (targetVelX - car.x * 0) * lateralMix;

  // Compute actual velocity
  const vx = forwardX * car.speed + (-forwardZ) * car.lateralVel * 0.3;
  const vz = forwardZ * car.speed + (forwardX) * car.lateralVel * 0.3;

  // Apply handbrake drift
  if (input.handbrake && Math.abs(car.speed) > 10) {
    car.driftAngle += input.steer * 2.5 * dt;
    car.lateralVel += input.steer * 15 * dt;
  }

  // Update position
  car.x += vx * dt;
  car.z += vz * dt;

  // Bounce off track boundaries
  if (Math.abs(latOff) > trackPt.w / 2 * 1.5) {
    const pushDir = latOff > 0 ? -1 : 1;
    car.x += pushDir * 3 * dt;
    car.z += pushDir * (-dir.z) * 3 * dt;
    car.speed *= 0.5;
    car.bounceTimer = 0.2;
  }

  // Elevation
  car.y = trackPt.h + 0.5;

  // Gear simulation
  const absSpd = Math.abs(car.speed);
  if (absSpd < 8) car.gear = 1;
  else if (absSpd < 18) car.gear = 2;
  else if (absSpd < 30) car.gear = 3;
  else if (absSpd < 45) car.gear = 4;
  else car.gear = 5;
  car.rpm = 800 + (absSpd / maxSpd) * 6000 + (car.gear - 1) * 200;

  // Drift angle decay
  car.driftAngle *= 0.95;

  // Bounce timer
  if (car.bounceTimer > 0) car.bounceTimer -= dt;

  // Checkpoints
  const cpDist = Math.abs(closest.t - car.lastCheckpoint);
  if (cpDist > 0.02 || car.lastCheckpoint === -1) {
    // Find nearest checkpoint
    for (let i = 0; i < CHECKPOINT_COUNT; i++) {
      const cpT = i / CHECKPOINT_COUNT;
      const d = Math.abs(closest.t - cpT);
      if (d < 0.03 || d > 0.97) {
        const cpIdx = (i + 1) % CHECKPOINT_COUNT;
        if (!car.checkpointsPassed.includes(cpIdx)) {
          car.checkpointsPassed.push(cpIdx);
          if (car.checkpointsPassed.length === CHECKPOINT_COUNT) {
            // Lap completed
            const now = performance.now();
            const lapTime = (now - car.lapStartTime) / 1000;
            car.lapTimes.push(lapTime);
            if (lapTime < car.bestLap) car.bestLap = lapTime;
            car.checkpointsPassed = [];
            car.lap++;
            car.lapStartTime = now;
            if (car.lap >= TOTAL_LAPS) {
              car.finished = true;
              car.totalElapsed = lapTime + (car.lapTimes.length > 0 ? car.lapTimes.reduce((a,b)=>a+b,0) - car.lapTimes[car.lapTimes.length-1] : 0);
            }
          }
        }
      }
    }
    car.lastCheckpoint = closest.t;
  }

  return surface;
}

// ── function: createCameraState ──
// ===== CAMERA =====
function createCameraState() {
  return {
    x: 0, y: 5, z: 10,
    targetX: 0, targetY: 2, targetZ: 0,
    cinematic: false,
    cinematicAngle: 0,
  };
}

// ── function: updateCamera ──
function updateCamera(cam, car) {
  const behindX = -Math.sin(car.heading);
  const behindZ = -Math.cos(car.heading);
  const dist = cam.cinematic ? 25 : 10;
  const height = cam.cinematic ? 15 : 5;

  cam.targetX = car.x + behindX * dist;
  cam.targetY = car.y + height;
  cam.targetZ = car.z + behindZ * dist;

  const lerp = 0.06;
  cam.x += (cam.targetX - cam.x) * lerp;
  cam.y += (cam.targetY - cam.y) * lerp;
  cam.z += (cam.targetZ - cam.z) * lerp;

  if (cam.cinematic) {
    cam.cinematicAngle += 0.002;
  }
}

// ── function: createParticleSystem ──
// ===== PARTICLE SYSTEM =====
function createParticleSystem() {
  const dust = [];
  const sparks = [];
  for (let i = 0; i < DUST_PARTICLE_COUNT; i++) dust.push({active: false, life: 0, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, size: 0});
  for (let i = 0; i < SPARK_COUNT; i++) sparks.push({active: false, life: 0, x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, size: 0});
  return { dust, sparks };
}

// ── function: spawnDust ──
function spawnDust(particles, x, y, z, intensity) {
  for (let i = 0; i < particles.dust.length; i++) {
    if (!particles.dust[i].active) {
      const p = particles.dust[i];
      p.active = true;
      p.life = 0.5 + Math.random() * 0.5;
      p.x = x + (Math.random() - 0.5) * 2;
      p.y = y;
      p.z = z + (Math.random() - 0.5) * 2;
      p.vx = (Math.random() - 0.5) * 3;
      p.vy = Math.random() * 2 + 1;
      p.vz = (Math.random() - 0.5) * 3;
      p.size = 0.3 + Math.random() * 0.5;
      break;
    }
  }
}

// ── function: spawnSparks ──
function spawnSparks(particles, x, y, z) {
  for (let i = 0; i < particles.sparks.length; i++) {
    if (!particles.sparks[i].active) {
      const p = particles.sparks[i];
      p.active = true;
      p.life = 0.3 + Math.random() * 0.3;
      p.x = x; p.y = y; p.z = z;
      p.vx = (Math.random() - 0.5) * 10;
      p.vy = Math.random() * 8 + 3;
      p.vz = (Math.random() - 0.5) * 10;
      p.size = 0.15;
      break;
    }
  }
}

// ── function: updateParticles ──
function updateParticles(particles, dt) {
  for (const p of particles.dust) {
    if (!p.active) continue;
    p.life -= dt;
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.z += p.vz * dt;
    p.vy -= 3 * dt;
    p.size += dt * 0.5;
    if (p.life <= 0) p.active = false;
  }
  for (const p of particles.sparks) {
    if (!p.active) continue;
    p.life -= dt;
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.z += p.vz * dt;
    p.vy -= 15 * dt;
    if (p.life <= 0) p.active = false;
  }
}

// ── function: createAudioManager ──
// ===== AUDIO MANAGER =====
function createAudioManager() {
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  let engineOsc = null, engineGain = null, engineFilter = null;
  let noiseNode = null, noiseGain = null;

  function initEngine() {
    engineOsc = ctx.createOscillator();
    engineGain = ctx.createGain();
    engineFilter = ctx.createBiquadFilter();
    engineOsc.type = 'sawtooth';
    engineOsc.frequency.value = 80;
    engineFilter.type = 'lowpass';
    engineFilter.frequency.value = 400;
    engineGain.gain.value = 0;
    engineOsc.connect(engineFilter);
    engineFilter.connect(engineGain);
    engineGain.connect(ctx.destination);
    engineOsc.start();

    // Noise for skid/gravel
    const bufferSize = ctx.sampleRate * 2;
    const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
    noiseNode = ctx.createBufferSource();
    noiseNode.buffer = buffer;
    noiseNode.loop = true;
    noiseGain = ctx.createGain();
    noiseGain.gain.value = 0;
    const noiseFilter = ctx.createBiquadFilter();
    noiseFilter.type = 'bandpass';
    noiseFilter.frequency.value = 800;
    noiseFilter.Q.value = 0.5;
    noiseNode.connect(noiseFilter);
    noiseFilter.connect(noiseGain);
    noiseGain.connect(ctx.destination);
    noiseNode.start();
  }

  function updateEngine(speed, gear) {
    if (!engineOsc) return;
    const rpm = 800 + Math.abs(speed) * 120;
    engineOsc.frequency.value = Math.min(rpm / 6, 600);
    const vol = Math.min(0.08, Math.abs(speed) / 100);
    engineGain.gain.value = vol;
    engineFilter.frequency.value = 300 + Math.abs(speed) * 10;
  }

  function updateSkid(onDirt, onGrass, isDrifting) {
    if (!noiseGain) return;
    const vol = (onDirt ? 0.02 : 0) + (onGrass ? 0.03 : 0) + (isDrifting ? 0.04 : 0);
    noiseGain.gain.value = Math.min(vol, 0.06);
  }

  function playCountdownBeep(high) {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sine';
    osc.frequency.value = high ? 880 : 440;
    gain.gain.value = 0.15;
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start();
    osc.stop(ctx.currentTime + 0.3);
  }

  function playGo() {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'square';
    osc.frequency.value = 660;
    gain.gain.value = 0.1;
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start();
    osc.stop(ctx.currentTime + 0.5);
  }

  function playCrash() {
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.value = 60;
    gain.gain.value = 0.2;
    gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
    osc.connect(gain);
    gain.connect(ctx.destination);
    osc.start();
    osc.stop(ctx.currentTime + 0.3);
  }

  function playFinish() {
    [523, 659, 784].forEach((freq, i) => {
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = 'sine';
      osc.frequency.value = freq;
      gain.gain.value = 0.12;
      gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4 + i * 0.15);
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.start(ctx.currentTime + i * 0.15);
      osc.stop(ctx.currentTime + 0.4 + i * 0.15);
    });
  }

  return { initEngine, updateEngine, updateSkid, playCountdownBeep, playGo, playCrash, playFinish, ctx };
}

// ── function: createHUD ──
// ===== HUD RENDERER =====
function createHUD() {
  const canvas = document.createElement('canvas');
  canvas.id = 'hudCanvas';
  canvas.width = 512;
  canvas.height = 384;
  canvas.style.position = 'absolute';
  canvas.style.bottom = '10px';
  canvas.style.right = '10px';
  canvas.style.borderRadius = '8px';
  canvas.style.boxShadow = '0 0 20px rgba(0,0,0,0.5)';
  canvas.style.imageRendering = 'auto';
  return canvas;
}

// ── function: formatTime ──
function formatTime(seconds) {
  const m = Math.floor(seconds / 60);
  const s = Math.floor(seconds % 60);
  const ms = Math.floor((seconds % 1) * 100);
  return `${m}:${s.toString().padStart(2,'0')}.${ms.toString().padStart(2,'0')}`;
}

// ── function: renderHUD ──
function renderHUD(hudCanvas, car, gameState) {
  const ctx = hudCanvas.getContext('2d');
  const W = hudCanvas.width, H = hudCanvas.height;
  ctx.clearRect(0, 0, W, H);

  // Background
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.fillRect(0, 0, W, H);

  // Speed
  const speed = Math.abs(car.speed);
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 48px monospace';
  ctx.textAlign = 'center';
  ctx.fillText(Math.floor(speed), W/2, 70);
  ctx.font = '16px monospace';
  ctx.fillStyle = '#aaa';
  ctx.fillText('km/h', W/2, 95);

  // Speed bar
  const maxSpd = MAX_SPEED_ASPHALT;
  const barW = 200;
  const barH = 8;
  ctx.fillStyle = '#333';
  ctx.fillRect(W/2 - barW/2, 105, barW, barH);
  const ratio = Math.min(speed / maxSpd, 1);
  const color = ratio > 0.8 ? '#ff4444' : ratio > 0.5 ? '#ffaa00' : '#44ff44';
  ctx.fillStyle = color;
  ctx.fillRect(W/2 - barW/2, 105, barW * ratio, barH);

  // Gear / RPM
  ctx.font = 'bold 20px monospace';
  ctx.fillStyle = '#ffcc00';
  ctx.fillText(`G${car.gear}`, W/2, 145);
  ctx.fillStyle = '#888';
  ctx.font = '14px monospace';
  ctx.fillText(`${Math.floor(car.rpm)} RPM`, W/2, 165);

  // Lap info
  ctx.textAlign = 'left';
  ctx.font = 'bold 18px monospace';
  ctx.fillStyle = '#fff';
  ctx.fillText(`Lap ${Math.min(car.lap + 1, TOTAL_LAPS)}/${TOTAL_LAPS}`, 15, 30);

  ctx.font = '14px monospace';
  ctx.fillStyle = '#aaa';
  ctx.fillText(`Time: ${formatTime(gameState.elapsed)}`, 15, 55);

  if (car.lapTimes.length > 0) {
    ctx.fillStyle = '#4f4';
    ctx.fillText(`Best: ${formatTime(car.bestLap)}`, 15, 75);
  }

  // Surface indicator
  const surfColors = {asphalt: '#666', dirt: '#8B7355', grass: '#4a7c3f', mud: '#3d2b1f'};
  ctx.fillStyle = surfColors[car.surfaceType] || '#666';
  ctx.fillRect(15, 85, 12, 12);
  ctx.fillStyle = '#ccc';
  ctx.font = '12px monospace';
  ctx.fillText(car.surfaceType.toUpperCase(), 32, 96);

  // Minimap
  const mmX = W - 160, mmY = H - 150, mmW = 145, mmH = 135;
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
  ctx.fillRect(mmX, mmY, mmW, mmH);
  ctx.strokeStyle = '#555';
  ctx.strokeRect(mmX, mmY, mmW, mmH);

  // Track on minimap
  ctx.beginPath();
  ctx.strokeStyle = '#888';
  ctx.lineWidth = 3;
  for (let i = 0; i <= 360; i++) {
    const tt = i / 360;
    const p = getTrackPoint(tt);
    const mx = mmX + (p.x + 80) / 200 * mmW;
    const my = mmY + (p.z + 80) / 200 * mmH;
    if (i === 0) ctx.moveTo(mx, my);
    else ctx.lineTo(mx, my);
  }
  ctx.closePath();
  ctx.stroke();

  // Player on minimap
  const pmx = mmX + (car.x + 80) / 200 * mmW;
  const pmy = mmY + (car.z + 80) / 200 * mmH;
  ctx.beginPath();
  ctx.arc(pmx, pmy, 4, 0, Math.PI * 2);
  ctx.fillStyle = '#ff4444';
  ctx.fill();

  // Countdown overlay
  if (gameState.countdown > 0) {
    ctx.textAlign = 'center';
    ctx.font = 'bold 80px monospace';
    const num = gameState.countdown > 0 ? Math.ceil(gameState.countdown) : 'GO!';
    ctx.fillStyle = gameState.countdown > 0 ? '#ffcc00' : '#44ff44';
    ctx.globalAlpha = 0.9;
    ctx.fillText(num, W/2, H/2 + 20);
    ctx.globalAlpha = 1;
  }

  // Pause overlay
  if (gameState.paused) {
    ctx.fillStyle = 'rgba(0,0,0,0.7)';
    ctx.fillRect(0, 0, W, H);
    ctx.textAlign = 'center';
    ctx.font = 'bold 40px monospace';
    ctx.fillStyle = '#fff';
    ctx.fillText('PAUSED', W/2, H/2 - 20);
    ctx.font = '18px monospace';
    ctx.fillStyle = '#aaa';
    ctx.fillText('Press P to resume', W/2, H/2 + 20);
  }

  // Finish overlay
  if (gameState.finished) {
    ctx.fillStyle = 'rgba(0,0,0,0.8)';
    ctx.fillRect(0, 0, W, H);
    ctx.textAlign = 'center';
    ctx.font = 'bold 36px monospace';
    ctx.fillStyle = '#ffcc00';
    ctx.fillText('FINISHED!', W/2, H/2 - 50);
    ctx.font = '20px monospace';
    ctx.fillStyle = '#fff';
    ctx.fillText(`Time: ${formatTime(gameState.elapsed)}`, W/2, H/2);
    ctx.fillText(`Best Lap: ${formatTime(car.bestLap)}`, W/2, H/2 + 30);
    ctx.font = '16px monospace';
    ctx.fillStyle = '#aaa';
    ctx.fillText('Press R to restart', W/2, H/2 + 65);
  }
}

// ── function: createInput ──
// ===== INPUT =====
function createInput() {
  return {
    accel: false, brake: false, steer: 0, reverse: false, handbrake: false,
    restart: false, pause: false, camToggle: false,
  };
}

// ── function: updateInput ──
function updateInput(input, keys, touch) {
  input.accel = keys['ArrowUp'] || keys['KeyW'] || touch.accel;
  input.brake = keys['ArrowDown'] || keys['KeyS'] || touch.brake;
  input.steer = (keys['ArrowRight'] || keys['KeyD'] ? 1 : 0) - (keys['ArrowLeft'] || keys['KeyA'] ? 1 : 0) + touch.steer;
  input.reverse = keys['ArrowDown'] && !input.accel;
  input.handbrake = keys['Space'] || touch.handbrake;
  input.restart = keys['KeyR'];
  input.pause = keys['KeyP'];
  input.camToggle = keys['KeyC'];
}

// ── main ──
// ===== MAIN =====
(function() {
  // --- Three.js Setup ---
  const canvas = document.getElementById('gameCanvas');
  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.setClearColor(0x87CEEB);

  const scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x87CEEB, 0.004);

  const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 500);

  // --- Lighting ---
  const ambientLight = new THREE.AmbientLight(0x6688aa, 0.5);
  scene.add(ambientLight);

  const sunLight = new THREE.DirectionalLight(0xffeedd, 1.0);
  sunLight.position.set(50, 80, 30);
  sunLight.castShadow = true;
  sunLight.shadow.mapSize.width = 2048;
  sunLight.shadow.mapSize.height = 2048;
  sunLight.shadow.camera.near = 1;
  sunLight.shadow.camera.far = 200;
  sunLight.shadow.camera.left = -60;
  sunLight.shadow.camera.right = 60;
  sunLight.shadow.camera.top = 60;
  sunLight.shadow.camera.bottom = -60;
  scene.add(sunLight);
  scene.add(sunLight.target);

  const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x444422, 0.4);
  scene.add(hemiLight);

  // --- Ground Plane ---
  const groundGeo = new THREE.BufferGeometry();
  const gSize = 300;
  const gVerts = new Float32Array([-gSize,-0.5,-gSize, gSize,-0.5,-gSize, gSize,-0.5,gSize, -gSize,-0.5,gSize]);
  groundGeo.setAttribute('position', new THREE.Float32BufferAttribute(gVerts, 3));
  groundGeo.setIndex([0,1,2, 0,2,3]);
  groundGeo.computeVertexNormals();
  const groundMat = new THREE.MeshLambertMaterial({ color: 0x3a6b2a });
  const ground = new THREE.Mesh(groundGeo, groundMat);
  ground.receiveShadow = true;
  scene.add(ground);

  // --- Track Geometry ---
  const trackCurve = new THREE.CatmullRomCurve3(
    TRACK_POINTS.map(p => new THREE.Vector3(p.x, p.h + 0.3, p.z)),
    true, 'catmullrom', 0.5
  );

  const trackSegments = 400;
  const trackVerts = [], trackUVs = [], trackIndices = [];
  const trackColors = [];

  for (let i = 0; i <= trackSegments; i++) {
    const t = i / trackSegments;
    const pos = trackCurve.getPointAt(t);
    const tangent = trackCurve.getTangentAt(t);
    const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
    const w = getTrackWidthAt(t);
    if (isNaN(normal.x) || isNaN(normal.y) || isNaN(normal.z)) continue;
    if (isNaN(pos.x) || isNaN(pos.z)) continue;

    const left = pos.clone().add(normal.clone().multiplyScalar(w / 2));
    const right = pos.clone().add(normal.clone().multiplyScalar(-w / 2));

    trackVerts.push(left.x, left.y, left.z);
    trackVerts.push(right.x, right.y, right.z);
    trackUVs.push(0, t * 20);
    trackUVs.push(1, t * 20);

    const isEdge = i % 30 < 3;
    if (isEdge) {
      trackColors.push(0.55, 0.45, 0.33, 0.55, 0.45, 0.33);
    } else {
      trackColors.push(0.33, 0.33, 0.33, 0.33, 0.33, 0.33);
    }

    if (i < trackSegments) {
      const base = i * 2;
      trackIndices.push(base, base + 1, base + 2);
      trackIndices.push(base + 1, base + 3, base + 2);
    }
  }

  const trackGeo = new THREE.BufferGeometry();
  trackGeo.setAttribute('position', new THREE.Float32BufferAttribute(trackVerts, 3));
  trackGeo.setAttribute('uv', new THREE.Float32BufferAttribute(trackUVs, 2));
  trackGeo.setAttribute('color', new THREE.Float32BufferAttribute(trackColors, 3));
  trackGeo.setIndex(trackIndices);
  try { trackGeo.computeVertexNormals(); } catch(e) {}

  const trackMat = new THREE.MeshLambertMaterial({ vertexColors: true });
  const trackMesh = new THREE.Mesh(trackGeo, trackMat);
  trackMesh.receiveShadow = true;
  scene.add(trackMesh);

  // --- Track edge markers (posts) ---
  const postMat = new THREE.MeshLambertMaterial({ color: 0xff6600 });
  for (let side = -1; side <= 1; side += 2) {
    for (let i = 0; i < trackSegments; i += 20) {
      const t = i / trackSegments;
      const pos = trackCurve.getPointAt(t);
      const tangent = trackCurve.getTangentAt(t);
      const normal = new THREE.Vector3(-tangent.z, 0, tangent.x).normalize();
      const w = getTrackWidthAt(t);
      if (isNaN(normal.x)) continue;
      const offset = side * (w / 2 + 0.5);
      const p = pos.clone().add(normal.clone().multiplyScalar(offset));
      if (isNaN(p.x) || isNaN(p.z)) continue;

      const postGeo = new THREE.CylinderGeometry(0.08, 0.08, 1.5, 4);
      const post = new THREE.Mesh(postGeo, postMat);
      post.position.set(p.x, p.y + 0.75, p.z);
      post.castShadow = true;
      scene.add(post);

      const stripeGeo = new THREE.CylinderGeometry(0.1, 0.1, 0.3, 4);
      const stripeMat = new THREE.MeshLambertMaterial({ color: i % 40 < 20 ? 0xffffff : 0xff0000 });
      const stripe = new THREE.Mesh(stripeGeo, stripeMat);
      stripe.position.set(p.x, p.y + 0.75, p.z);
      scene.add(stripe);
    }
  }

  // --- Scenery ---
  const sceneryGroup = new THREE.Group();
  scene.add(sceneryGroup);

  const treeMat = new THREE.MeshLambertMaterial({ color: 0x2d5a1e });
  const trunkMat = new THREE.MeshLambertMaterial({ color: 0x5a3a1a });
  const rockMat = new THREE.MeshLambertMaterial({ color: 0x666666 });
  const signMat = new THREE.MeshLambertMaterial({ color: 0xff4444 });

  const n = TRACK_POINTS.length;
  for (let i = 0; i < n; i++) {
    const pt = TRACK_POINTS[i];
    const dir = getTrackDirection(i / n);
    const rightX = -dir.z, rightZ = dir.x;
    const halfW = pt.w / 2 + 3;

    for (let side = -1; side <= 1; side += 2) {
      const offset = side * (halfW + 3 + Math.random() * 8);
      const tx = pt.x + rightX * offset;
      const tz = pt.z + rightZ * offset;
      const sc = 0.7 + Math.random() * 0.6;

      const trunkGeo = new THREE.CylinderGeometry(0.2 * sc, 0.3 * sc, 2 * sc, 6);
      const trunk = new THREE.Mesh(trunkGeo, trunkMat);
      trunk.position.set(tx, pt.h + sc, tz);
      trunk.castShadow = true;
      sceneryGroup.add(trunk);

      const leavesGeo = new THREE.ConeGeometry(1.5 * sc, 3 * sc, 6);
      const leaves = new THREE.Mesh(leavesGeo, treeMat);
      leaves.position.set(tx, pt.h + 2 * sc + 1.5 * sc, tz);
      leaves.castShadow = true;
      sceneryGroup.add(leaves);
    }

    if (Math.random() < 0.4) {
      const side = Math.random() < 0.5 ? -1 : 1;
      const offset = side * (halfW + 1 + Math.random() * 4);
      const sc = 0.5 + Math.random() * 1.2;
      const geo = new THREE.IcosahedronGeometry(sc, 0);
      const mesh = new THREE.Mesh(geo, rockMat);
      mesh.position.set(pt.x + rightX * offset, pt.h + sc * 0.5, pt.z + rightZ * offset);
      mesh.rotation.set(Math.random(), Math.random(), Math.random());
      mesh.castShadow = true;
      sceneryGroup.add(mesh);
    }

    if (Math.random() < 0.15) {
      const side = Math.random() < 0.5 ? -1 : 1;
      const offset = side * (halfW + 1);
      const poleGeo = new THREE.CylinderGeometry(0.05, 0.05, 2, 4);
      const pole = new THREE.Mesh(poleGeo, new THREE.MeshLambertMaterial({ color: 0x888888 }));
      pole.position.set(pt.x + rightX * offset, pt.h + 1, pt.z + rightZ * offset);
      sceneryGroup.add(pole);

      const boardGeo = new THREE.BoxGeometry(1.2, 0.6, 0.05);
      const board = new THREE.Mesh(boardGeo, signMat);
      board.position.set(pt.x + rightX * offset, pt.h + 2.2, pt.z + rightZ * offset);
      sceneryGroup.add(board);
    }
  }

  // --- Car Model ---
  const carGroup = new THREE.Group();
  scene.add(carGroup);

  const bodyGeo = new THREE.BoxGeometry(2, 0.6, 4);
  const bodyMat = new THREE.MeshLambertMaterial({ color: 0xcc2222 });
  const body = new THREE.Mesh(bodyGeo, bodyMat);
  body.position.y = 0.5;
  body.castShadow = true;
  carGroup.add(body);

  const cabinGeo = new THREE.BoxGeometry(1.6, 0.5, 1.8);
  const cabinMat = new THREE.MeshLambertMaterial({ color: 0x333333 });
  const cabin = new THREE.Mesh(cabinGeo, cabinMat);
  cabin.position.set(0, 1.05, -0.3);
  carGroup.add(cabin);

  const spoilerGeo = new THREE.BoxGeometry(2.2, 0.08, 0.4);
  const spoilerMat = new THREE.MeshLambertMaterial({ color: 0x111111 });
  const spoiler = new THREE.Mesh(spoilerGeo, spoilerMat);
  spoiler.position.set(0, 1.3, 1.8);
  carGroup.add(spoiler);
  [-0.8, 0.8].forEach(x => {
    const leg = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.3, 0.1), spoilerMat);
    leg.position.set(x, 1.15, 1.8);
    carGroup.add(leg);
  });

  const wheelGeo = new THREE.CylinderGeometry(0.35, 0.35, 0.25, 8);
  const wheelMat = new THREE.MeshLambertMaterial({ color: 0x222222 });
  const wheelPositions = [[-1.0, 0.35, 1.3], [1.0, 0.35, 1.3], [-1.0, 0.35, -1.3], [1.0, 0.35, -1.3]];
  const wheels = [];
  wheelPositions.forEach(([x, y, z]) => {
    const wheel = new THREE.Mesh(wheelGeo, wheelMat);
    wheel.rotation.z = Math.PI / 2;
    wheel.position.set(x, y, z);
    wheel.castShadow = true;
    carGroup.add(wheel);
    wheels.push(wheel);
  });

  const hlMat = new THREE.MeshLambertMaterial({ color: 0xffffcc, emissive: 0xffffcc, emissiveIntensity: 0.5 });
  [-0.7, 0.7].forEach(x => {
    const hl = new THREE.Mesh(new THREE.SphereGeometry(0.15, 6, 6), hlMat);
    hl.position.set(x, 0.5, -2.0);
    carGroup.add(hl);
  });

  const tlMat = new THREE.MeshLambertMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 0.3 });
  [-0.7, 0.7].forEach(x => {
    const tl = new THREE.Mesh(new THREE.SphereGeometry(0.15, 6, 6), tlMat);
    tl.position.set(x, 0.5, 2.0);
    carGroup.add(tl);
  });

  // --- Particle Meshes ---
  const dustGroup = new THREE.Group();
  scene.add(dustGroup);
  const dustMat = new THREE.MeshBasicMaterial({ color: 0x8B7355, transparent: true, opacity: 0.4, depthWrite: false });
  const dustMeshes = [];
  for (let i = 0; i < 30; i++) {
    const m = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), dustMat.clone());
    m.visible = false;
    dustGroup.add(m);
    dustMeshes.push(m);
  }

  const sparkGroup = new THREE.Group();
  scene.add(sparkGroup);
  const sparkMat = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
  const sparkMeshes = [];
  for (let i = 0; i < 15; i++) {
    const m = new THREE.Mesh(new THREE.SphereGeometry(0.05, 6, 6), sparkMat.clone());
    m.visible = false;
    sparkGroup.add(m);
    sparkMeshes.push(m);
  }

  // --- Skid marks ---
  const skidGroup = new THREE.Group();
  scene.add(skidGroup);
  const skidMat = new THREE.MeshBasicMaterial({ color: 0x111111, transparent: true, opacity: 0.5, depthWrite: false });
  const skidMeshes = [];
  for (let i = 0; i < 40; i++) {
    const m = new THREE.Mesh(new THREE.PlaneGeometry(0.15, 2), skidMat.clone());
    m.visible = false;
    skidGroup.add(m);
    skidMeshes.push(m);
  }

  // --- Game State ---
  const car = createCar();
  const cam = createCameraState();
  const particles = createParticleSystem();
  const audio = createAudioManager();
  const input = createInput();
  const keys = {};
  const touch = { accel: false, brake: false, steer: 0, handbrake: false };

  let gameState = {
    countdown: 3.5,
    paused: false,
    finished: false,
    elapsed: 0,
    started: false,
  };

  let dustIdx = 0, sparkIdx = 0, skidIdx = 0;
  let lastTime = performance.now();
  let audioInitialized = false;

  // --- Input ---
  document.addEventListener('keydown', e => {
    keys[e.code] = true;
    if (e.code === 'KeyR' && (gameState.finished || gameState.countdown <= 0)) resetGame();
    if (e.code === 'KeyP' && gameState.countdown <= 0) gameState.paused = !gameState.paused;
    if (e.code === 'KeyC') cam.cinematic = !cam.cinematic;
  });
  document.addEventListener('keyup', e => { keys[e.code] = false; });

  function setupTouch(el, prop, val) {
    el.addEventListener('touchstart', e => { e.preventDefault(); touch[prop] = val; });
    el.addEventListener('touchend', e => { e.preventDefault(); touch[prop] = 0; });
    el.addEventListener('touchcancel', e => { touch[prop] = 0; });
  }
  const tc = document.getElementById('touchControls');
  if (tc) {
    setupTouch(document.getElementById('dpadUp'), 'accel', true);
    setupTouch(document.getElementById('dpadDown'), 'brake', true);
    setupTouch(document.getElementById('dpadLeft'), 'steer', -1);
    setupTouch(document.getElementById('dpadRight'), 'steer', 1);
    setupTouch(document.getElementById('gasBtn'), 'accel', true);
    setupTouch(document.getElementById('brakeBtn'), 'brake', true);
    setupTouch(document.getElementById('hbBtn'), 'handbrake', true);
  }

  function resetGame() {
    const startPt = getTrackPoint(0);
    const dir = getTrackDirection(0);
    car.x = startPt.x; car.z = startPt.z; car.y = startPt.h;
    car.heading = Math.atan2(dir.x, dir.z);
    car.speed = 0; car.lateralVel = 0; car.driftAngle = 0;
    car.lap = 0; car.lastCheckpoint = -1; car.checkpointsPassed = [];
    car.lapTimes = []; car.bestLap = Infinity; car.finished = false;
    car.totalElapsed = 0; car.bounceTimer = 0;
    gameState = { countdown: 3.5, paused: false, finished: false, elapsed: 0, started: false };
    cam.cinematic = false;
    dustMeshes.forEach(m => m.visible = false);
    sparkMeshes.forEach(m => m.visible = false);
    skidMeshes.forEach(m => m.visible = false);
  }

  window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  });

  // --- Game Loop ---
  function gameLoop() {
    requestAnimationFrame(gameLoop);

    const now = performance.now();
    let dt = (now - lastTime) / 1000;
    lastTime = now;
    dt = Math.min(dt, 0.05);

    updateInput(input, keys, touch);

    if (!audioInitialized && (input.accel || input.brake)) {
      audio.initEngine();
      audioInitialized = true;
    }

    if (gameState.countdown > 0) {
      gameState.countdown -= dt;
      if (gameState.countdown <= 0) {
        gameState.started = true;
        gameState.elapsed = 0;
      }
      if (Math.ceil(gameState.countdown + dt) !== Math.ceil(gameState.countdown) && gameState.countdown > 0) {
        audio.playCountdownBeep(gameState.countdown < 1.5);
      }
    }

    if (gameState.started && !gameState.paused && !gameState.finished) {
      gameState.elapsed += dt;
      const surface = updateCar(car, dt, input);
      const absSpd = Math.abs(car.speed);

      if (absSpd > 5 && (surface.type === 'dirt' || surface.type === 'grass' || input.handbrake)) {
        spawnDust(particles, car.x, car.y - 0.3, car.z, absSpd);
      }
      if (car.bounceTimer > 0) spawnSparks(particles, car.x, car.y, car.z);
      if (input.handbrake && absSpd > 10) {
        const skid = skidMeshes[skidIdx % skidMeshes.length];
        skid.visible = true;
        skid.position.set(car.x - 0.4, car.y - 0.05, car.z + 0.5);
        skid.rotation.y = car.heading;
        skidIdx++;
        const skid2 = skidMeshes[skidIdx % skidMeshes.length];
        skid2.visible = true;
        skid2.position.set(car.x + 0.4, car.y - 0.05, car.z + 0.5);
        skid2.rotation.y = car.heading;
        skidIdx++;
      }

      carGroup.position.set(car.x, car.y, car.z);
      carGroup.rotation.y = car.heading + car.driftAngle;

      wheels.forEach((w, i) => {
        w.rotation.x += car.speed * dt * 0.5;
        if (i >= 2) w.rotation.y = input.steer * 0.3;
      });

      updateCamera(cam, car);
      camera.position.set(cam.x, cam.y, cam.z);
      if (cam.cinematic) {
        const lookX = car.x + Math.sin(cam.cinematicAngle) * 5;
        const lookZ = car.z + Math.cos(cam.cinematicAngle) * 5;
        camera.lookAt(lookX, car.y, lookZ);
      } else {
        camera.lookAt(car.x, car.y + 1, car.z);
      }

      sunLight.position.set(car.x + 50, 80, car.z + 30);
      sunLight.target.position.set(car.x, car.y, car.z);

      updateParticles(particles, dt);
      particles.dust.forEach((p, i) => {
        if (p.active && dustMeshes[i]) {
          dustMeshes[i].visible = true;
          dustMeshes[i].position.set(p.x, p.y, p.z);
          dustMeshes[i].scale.setScalar(p.size);
          dustMeshes[i].material.opacity = p.life * 0.4;
          dustMeshes[i].lookAt(camera.position);
        } else if (dustMeshes[i]) {
          dustMeshes[i].visible = false;
        }
      });
      particles.sparks.forEach((p, i) => {
        if (p.active && sparkMeshes[i]) {
          sparkMeshes[i].visible = true;
          sparkMeshes[i].position.set(p.x, p.y, p.z);
          sparkMeshes[i].scale.setScalar(p.size);
        } else if (sparkMeshes[i]) {
          sparkMeshes[i].visible = false;
        }
      });

      skidMeshes.forEach((m, i) => {
        if (m.visible && Math.random() < 0.005) m.visible = false;
      });

      if (audioInitialized) {
        audio.updateEngine(car.speed, car.gear);
        audio.updateSkid(surface.type === 'dirt', surface.type === 'grass', input.handbrake);
      }
    }

    const hudCanvas = document.getElementById('hudCanvas');
    renderHUD(hudCanvas, car, gameState);
    renderer.render(scene, camera);
  }

  resetGame();
  gameLoop();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"a9ebff1e-6cc8-4a27-a612-11cb20e74f25","tokensIn":1552026,"tokensOut":70483,"tokensTotal":1622509,"turns":37,"toolCalls":36,"failedToolCalls":1,"timestamp":"2026-07-20T23:31:59.530Z"} -->