← Rally results

Rally

Gemini 3.7 Flash · full

◈Preview unavailable — try playing the result

Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.

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 84,676 bytes · SHA-256 b5db1b936e0d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Apex Rally 3D - Procedural Rally Championship</title>
<!-- Three.js r128 from CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    user-select: none;
    -webkit-user-select: none;
  }
  html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: #0b0f19;
    font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
    color: #fff;
  }
  #game-container {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  canvas#webgl-canvas {
    width: 100%;
    height: 100%;
    display: block;
  }

  /* HUD Styling */
  #hud-layer {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding: 20px;
    z-index: 10;
  }

  .hud-top {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    width: 100%;
  }

  .hud-card {
    background: rgba(12, 18, 28, 0.75);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 12px;
    padding: 12px 18px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
  }

  .timer-box {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 160px;
  }
  .timer-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    font-family: 'Courier New', Courier, monospace;
  }
  .timer-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #94a3b8;
    font-family: 'Segoe UI', sans-serif;
    font-weight: 700;
  }
  .timer-val {
    font-size: 20px;
    font-weight: 900;
    color: #f8fafc;
    letter-spacing: 1px;
  }
  .timer-best {
    color: #38bdf8;
    font-size: 15px;
  }
  .timer-last {
    color: #fbbf24;
    font-size: 13px;
  }

  .lap-box {
    text-align: center;
    min-width: 110px;
  }
  .lap-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: #94a3b8;
    font-weight: 700;
  }
  .lap-counter {
    font-size: 28px;
    font-weight: 900;
    color: #f43f5e;
    font-style: italic;
  }
  .surface-tag {
    margin-top: 4px;
    font-size: 10px;
    font-weight: 800;
    text-transform: uppercase;
    padding: 2px 6px;
    border-radius: 4px;
    background: #22c55e;
    color: #000;
    letter-spacing: 0.5px;
    display: inline-block;
  }
  .surface-tag.dirt { background: #d97706; color: #fff; }
  .surface-tag.offroad { background: #ef4444; color: #fff; }

  /* Minimap */
  .minimap-container {
    position: relative;
    width: 140px;
    height: 140px;
    background: rgba(12, 18, 28, 0.85);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 50%;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  canvas#minimap-canvas {
    width: 100%;
    height: 100%;
  }

  /* Bottom HUD */
  .hud-bottom {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    width: 100%;
  }

  .gauge-cluster {
    display: flex;
    align-items: flex-end;
    gap: 15px;
  }

  /* Speedometer Circular SVG / Canvas */
  .tacho-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    background: rgba(12, 18, 28, 0.8);
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 16px;
    padding: 12px 18px;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  }
  .speed-main {
    display: flex;
    align-items: baseline;
    gap: 4px;
  }
  .speed-val {
    font-size: 48px;
    font-weight: 900;
    font-family: 'Segoe UI', sans-serif;
    color: #fff;
    line-height: 1;
  }
  .speed-unit {
    font-size: 14px;
    font-weight: 800;
    color: #94a3b8;
    text-transform: uppercase;
  }
  .rpm-bar-wrap {
    width: 140px;
    height: 10px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 5px;
    margin-top: 8px;
    overflow: hidden;
    position: relative;
  }
  .rpm-bar-fill {
    height: 100%;
    width: 0%;
    background: linear-gradient(90deg, #38bdf8 0%, #fbbf24 70%, #ef4444 95%);
    transition: width 0.05s ease;
  }
  .gear-display {
    margin-top: 6px;
    font-size: 18px;
    font-weight: 900;
    color: #38bdf8;
    letter-spacing: 1px;
  }

  /* Status message alert (Wrong way, checkpoints, etc) */
  #center-banner {
    position: absolute;
    top: 25%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    pointer-events: none;
    transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
    opacity: 0;
  }
  #center-banner.active {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1.1);
  }
  .banner-text {
    font-size: 48px;
    font-weight: 900;
    font-style: italic;
    text-transform: uppercase;
    color: #facc15;
    text-shadow: 0 0 20px rgba(250, 204, 21, 0.8), 0 4px 10px rgba(0,0,0,0.8);
    letter-spacing: 2px;
  }
  .banner-sub {
    font-size: 18px;
    font-weight: 700;
    color: #ffffff;
    text-shadow: 0 2px 8px rgba(0,0,0,0.8);
    margin-top: 4px;
  }

  /* Help hints on bottom right */
  .hud-hints {
    font-size: 11px;
    color: rgba(255, 255, 255, 0.6);
    line-height: 1.6;
    text-align: right;
  }
  .hud-hints kbd {
    background: rgba(255,255,255,0.2);
    border-radius: 4px;
    padding: 1px 5px;
    font-weight: bold;
    color: #fff;
  }

  /* Mobile Touch Controls Overlay */
  #touch-controls {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    display: none;
    z-index: 20;
  }
  .touch-zone-left {
    position: absolute;
    bottom: 25px;
    left: 25px;
    width: 160px;
    height: 160px;
    pointer-events: auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  .touch-zone-right {
    position: absolute;
    bottom: 25px;
    right: 25px;
    width: 180px;
    height: 160px;
    pointer-events: auto;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: flex-end;
  }
  .touch-btn {
    width: 70px;
    height: 70px;
    background: rgba(15, 23, 42, 0.65);
    border: 2px solid rgba(255, 255, 255, 0.3);
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: 900;
    font-size: 20px;
    color: #fff;
    touch-action: none;
    box-shadow: 0 4px 15px rgba(0,0,0,0.4);
    user-select: none;
    backdrop-filter: blur(4px);
  }
  .touch-btn:active, .touch-btn.active {
    background: rgba(244, 63, 94, 0.7);
    border-color: #fff;
    transform: scale(0.92);
  }
  .touch-btn-gas {
    background: rgba(34, 197, 94, 0.6);
    width: 80px;
    height: 80px;
  }
  .touch-btn-brake {
    background: rgba(239, 68, 68, 0.6);
    width: 70px;
    height: 70px;
  }
  .touch-btn-handbrake {
    background: rgba(234, 179, 8, 0.6);
    font-size: 12px;
    width: 60px;
    height: 60px;
  }

  /* Modals (Menu, Finish, Countdown, Audio Prompt) */
  .modal-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(5, 8, 15, 0.85);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 50;
    transition: opacity 0.3s ease;
  }
  .modal-box {
    background: #0f172a;
    border: 2px solid #38bdf8;
    border-radius: 20px;
    padding: 35px 45px;
    max-width: 480px;
    width: 90%;
    text-align: center;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8), 0 0 30px rgba(56, 189, 248, 0.2);
    animation: popIn 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  }
  @keyframes popIn {
    0% { transform: scale(0.8); opacity: 0; }
    100% { transform: scale(1); opacity: 1; }
  }
  .modal-title {
    font-size: 32px;
    font-weight: 900;
    font-style: italic;
    letter-spacing: 1.5px;
    color: #fff;
    margin-bottom: 8px;
    text-transform: uppercase;
  }
  .modal-subtitle {
    font-size: 14px;
    color: #94a3b8;
    margin-bottom: 25px;
  }
  .modal-stats {
    background: rgba(255,255,255,0.05);
    border-radius: 12px;
    padding: 16px;
    margin-bottom: 25px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  .stat-row {
    display: flex;
    justify-content: space-between;
    font-size: 15px;
  }
  .stat-row .val {
    font-weight: 800;
    font-family: 'Courier New', Courier, monospace;
    color: #38bdf8;
  }
  .btn-primary {
    background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%);
    color: #fff;
    border: none;
    border-radius: 12px;
    padding: 14px 28px;
    font-size: 16px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 1px;
    cursor: pointer;
    box-shadow: 0 6px 20px rgba(244, 63, 94, 0.4);
    transition: all 0.2s;
    outline: none;
    width: 100%;
    margin-bottom: 10px;
  }
  .btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 10px 25px rgba(244, 63, 94, 0.6);
  }
  .btn-secondary {
    background: rgba(255,255,255,0.1);
    color: #e2e8f0;
    border: 1px solid rgba(255,255,255,0.2);
    border-radius: 12px;
    padding: 12px 24px;
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
    transition: background 0.2s;
    width: 100%;
  }
  .btn-secondary:hover {
    background: rgba(255,255,255,0.2);
  }

  /* Countdown big overlay */
  #countdown-screen {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 40;
  }
  .countdown-num {
    font-size: 120px;
    font-weight: 900;
    font-style: italic;
    color: #fff;
    text-shadow: 0 0 40px rgba(244, 63, 94, 0.8), 0 8px 20px rgba(0,0,0,0.9);
    transform: scale(0.3);
    opacity: 0;
    transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  }
  .countdown-num.show {
    transform: scale(1.2);
    opacity: 1;
  }
  .countdown-num.go {
    color: #22c55e;
    text-shadow: 0 0 50px rgba(34, 197, 94, 0.9), 0 8px 20px rgba(0,0,0,0.9);
  }

  /* Top bar action buttons (Mute, Pause, Cam) */
  .top-actions {
    display: flex;
    gap: 8px;
    pointer-events: auto;
  }
  .icon-btn {
    background: rgba(12, 18, 28, 0.8);
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 8px;
    color: #fff;
    width: 38px;
    height: 38px;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
    font-size: 16px;
    transition: all 0.15s;
  }
  .icon-btn:hover {
    background: rgba(255, 255, 255, 0.2);
    transform: scale(1.05);
  }

  @media (max-width: 768px) {
    #touch-controls { display: block; }
    .hud-hints { display: none; }
    .gauge-cluster { transform: scale(0.85); transform-origin: bottom left; }
    .minimap-container { width: 100px; height: 100px; }
  }
</style>
</head>
<body>

<div id="game-container">
  <canvas id="webgl-canvas"></canvas>

  <!-- HUD Layer -->
  <div id="hud-layer">
    <div class="hud-top">
      <div class="hud-card timer-box">
        <div class="timer-row">
          <span class="timer-label">TIME</span>
          <span class="timer-val" id="time-current">00:00.000</span>
        </div>
        <div class="timer-row">
          <span class="timer-label">BEST</span>
          <span class="timer-val timer-best" id="time-best">--:--.---</span>
        </div>
        <div class="timer-row">
          <span class="timer-label">LAST</span>
          <span class="timer-val timer-last" id="time-last">--:--.---</span>
        </div>
      </div>

      <div class="hud-card lap-box">
        <div class="lap-label">LAP</div>
        <div class="lap-counter" id="lap-current">1 / 3</div>
        <div class="surface-tag" id="surface-tag">TARMAC</div>
      </div>

      <div class="top-actions">
        <button class="icon-btn" id="btn-cam" title="Toggle Camera (C)">📷</button>
        <button class="icon-btn" id="btn-audio" title="Mute/Unmute Audio (M)">🔊</button>
        <button class="icon-btn" id="btn-pause" title="Pause Game (P)">⏸</button>
      </div>

      <div class="minimap-container">
        <canvas id="minimap-canvas" width="140" height="140"></canvas>
      </div>
    </div>

    <!-- Center Alerts -->
    <div id="center-banner">
      <div class="banner-text" id="banner-main">LAP 2</div>
      <div class="banner-sub" id="banner-sub">Split: +0.42s</div>
    </div>

    <div class="hud-bottom">
      <div class="gauge-cluster">
        <div class="tacho-box">
          <div class="speed-main">
            <span class="speed-val" id="speed-val">0</span>
            <span class="speed-unit">KM/H</span>
          </div>
          <div class="rpm-bar-wrap">
            <div class="rpm-bar-fill" id="rpm-bar"></div>
          </div>
          <div class="gear-display" id="gear-val">N</div>
        </div>
      </div>

      <div class="hud-hints">
        <div><kbd>W</kbd> / <kbd>▲</kbd> Accelerate &bull; <kbd>S</kbd> / <kbd>▼</kbd> Brake / Rev</div>
        <div><kbd>A</kbd> <kbd>D</kbd> / <kbd>◄</kbd> <kbd>►</kbd> Steer &bull; <kbd>SPACE</kbd> Drift</div>
        <div><kbd>C</kbd> Camera &bull; <kbd>R</kbd> Reset Car &bull; <kbd>P</kbd> Pause</div>
      </div>
    </div>
  </div>

  <!-- Countdown Overlay -->
  <div id="countdown-screen">
    <div class="countdown-num" id="countdown-num">3</div>
  </div>

  <!-- Touch Controls for Mobile -->
  <div id="touch-controls">
    <div class="touch-zone-left">
      <div class="touch-btn" id="t-left">◀</div>
      <div class="touch-btn" id="t-right">▶</div>
    </div>
    <div class="touch-zone-right">
      <div class="touch-btn touch-btn-gas" id="t-gas">GAS</div>
      <div style="display: flex; gap: 10px;">
        <div class="touch-btn touch-btn-handbrake" id="t-drift">DRIFT</div>
        <div class="touch-btn touch-btn-brake" id="t-brake">BRAKE</div>
      </div>
    </div>
  </div>

  <!-- Pause Modal -->
  <div class="modal-overlay" id="pause-modal" style="display: none;">
    <div class="modal-box">
      <div class="modal-title">RACE PAUSED</div>
      <div class="modal-subtitle">Take a breather, driver!</div>
      <button class="btn-primary" id="btn-resume">RESUME RACE</button>
      <button class="btn-secondary" id="btn-restart-pause">RESTART RACE</button>
    </div>
  </div>

  <!-- Finish Modal -->
  <div class="modal-overlay" id="finish-modal" style="display: none;">
    <div class="modal-box">
      <div class="modal-title">RALLY COMPLETED! 🏁</div>
      <div class="modal-subtitle" id="finish-grade">OUTSTANDING DRIVING</div>
      <div class="modal-stats">
        <div class="stat-row">
          <span>Total Race Time:</span>
          <span class="val" id="stat-total-time">02:14.530</span>
        </div>
        <div class="stat-row">
          <span>Best Lap:</span>
          <span class="val" id="stat-best-lap">00:43.120</span>
        </div>
        <div class="stat-row">
          <span>Top Speed:</span>
          <span class="val" id="stat-top-speed">168 km/h</span>
        </div>
        <div class="stat-row">
          <span>Clean Driving:</span>
          <span class="val" id="stat-clean-laps">3 / 3 Laps</span>
        </div>
      </div>
      <button class="btn-primary" id="btn-restart-finish">RACE AGAIN</button>
    </div>
  </div>

  <!-- Start Welcome / Audio Unlock Overlay -->
  <div class="modal-overlay" id="start-modal">
    <div class="modal-box">
      <div class="modal-title">🌲 APEX RALLY 3D 🌲</div>
      <div class="modal-subtitle">Procedural Alpine Dirt &amp; Asphalt Track</div>
      <div class="modal-stats" style="text-align: left; font-size: 13px; line-height: 1.6;">
        <div>🏆 Complete 3 fast laps against the clock.</div>
        <div>🏎️ Master throttle control &amp; handbrake drifts on dirt corners.</div>
        <div>⚠️ Avoid rocks, barriers &amp; dense forest trees.</div>
      </div>
      <button class="btn-primary" id="btn-start-game">START ENGINE &amp; RACE</button>
    </div>
  </div>
</div>

<script>
/**
 * APEX RALLY 3D - Complete Single File Procedural WebGL Racing Experience
 */

// ==========================================
// 1. SOUND SYNTHESIZER (Web Audio API)
// ==========================================
class SoundController {
  constructor() {
    this.ctx = null;
    this.muted = false;
    this.initialized = false;
    this.engineOsc1 = null;
    this.engineOsc2 = null;
    this.engineGain = null;
    this.skidGain = null;
    this.skidFilter = null;
    this.skidSource = null;
  }

  init() {
    if (this.initialized) return;
    try {
      const AudioCtx = window.AudioContext || window.webkitAudioContext;
      this.ctx = new AudioCtx();
      this.setupEngineSynth();
      this.setupSkidSynth();
      this.initialized = true;
    } catch (e) {
      console.warn("Web Audio API not supported", e);
    }
  }

  setupEngineSynth() {
    if (!this.ctx) return;
    // Primary engine roar
    this.engineOsc1 = this.ctx.createOscillator();
    this.engineOsc1.type = 'sawtooth';
    this.engineOsc1.frequency.setValueAtTime(60, this.ctx.currentTime);

    // Sub rumble oscillator
    this.engineOsc2 = this.ctx.createOscillator();
    this.engineOsc2.type = 'triangle';
    this.engineOsc2.frequency.setValueAtTime(30, this.ctx.currentTime);

    // Lowpass filter for smooth throatiness
    this.engineFilter = this.ctx.createBiquadFilter();
    this.engineFilter.type = 'lowpass';
    this.engineFilter.frequency.setValueAtTime(450, this.ctx.currentTime);
    this.engineFilter.Q.setValueAtTime(2.5, this.ctx.currentTime);

    this.engineGain = this.ctx.createGain();
    this.engineGain.gain.setValueAtTime(0.0, this.ctx.currentTime);

    this.engineOsc1.connect(this.engineFilter);
    this.engineOsc2.connect(this.engineFilter);
    this.engineFilter.connect(this.engineGain);
    this.engineGain.connect(this.ctx.destination);

    this.engineOsc1.start();
    this.engineOsc2.start();
  }

  setupSkidSynth() {
    if (!this.ctx) return;
    // Pink / White noise generator for tire screech and gravel rumble
    const bufferSize = this.ctx.sampleRate * 2;
    const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const output = noiseBuffer.getChannelData(0);
    let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
    for (let i = 0; i < bufferSize; i++) {
      const white = Math.random() * 2 - 1;
      b0 = 0.99886 * b0 + white * 0.0555179;
      b1 = 0.99332 * b1 + white * 0.0750759;
      b2 = 0.96900 * b2 + white * 0.1538520;
      b3 = 0.86650 * b3 + white * 0.3104856;
      b4 = 0.55000 * b4 + white * 0.5329522;
      b5 = -0.7616 * b5 - white * 0.0168980;
      output[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362) * 0.11;
      b6 = white * 0.115926;
    }

    this.skidSource = this.ctx.createBufferSource();
    this.skidSource.buffer = noiseBuffer;
    this.skidSource.loop = true;

    this.skidFilter = this.ctx.createBiquadFilter();
    this.skidFilter.type = 'bandpass';
    this.skidFilter.frequency.setValueAtTime(1100, this.ctx.currentTime);
    this.skidFilter.Q.setValueAtTime(4.0, this.ctx.currentTime);

    this.skidGain = this.ctx.createGain();
    this.skidGain.gain.setValueAtTime(0.0, this.ctx.currentTime);

    this.skidSource.connect(this.skidFilter);
    this.skidFilter.connect(this.skidGain);
    this.skidGain.connect(this.ctx.destination);

    this.skidSource.start();
  }

  updateEngine(rpmRatio, throttle, speedRatio) {
    if (!this.initialized || this.muted || !this.ctx) return;
    const now = this.ctx.currentTime;
    const baseFreq = 45 + rpmRatio * 180 + throttle * 40;
    this.engineOsc1.frequency.setTargetAtTime(baseFreq, now, 0.05);
    this.engineOsc2.frequency.setTargetAtTime(baseFreq * 0.5, now, 0.05);
    this.engineFilter.frequency.setTargetAtTime(350 + rpmRatio * 1200 + throttle * 600, now, 0.05);

    const targetGain = 0.08 + throttle * 0.14 + speedRatio * 0.06;
    this.engineGain.gain.setTargetAtTime(targetGain, now, 0.05);
  }

  updateSkid(intensity, isDirt = false) {
    if (!this.initialized || this.muted || !this.ctx) return;
    const now = this.ctx.currentTime;
    const clamped = Math.min(1.0, Math.max(0, intensity));
    const targetFreq = isDirt ? 600 + clamped * 800 : 1200 + clamped * 1200;
    this.skidFilter.frequency.setTargetAtTime(targetFreq, now, 0.04);
    this.skidGain.gain.setTargetAtTime(clamped * 0.18, now, 0.05);
  }

  playBeep(isHigh = false) {
    if (!this.initialized || this.muted || !this.ctx) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    const freq = isHigh ? 880 : 440;
    const duration = isHigh ? 0.35 : 0.18;
    osc.type = 'sine';
    osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
    gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration);

    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + duration);
  }

  playImpact(strength = 1.0) {
    if (!this.initialized || this.muted || !this.ctx) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(140 * Math.min(strength, 1.5), this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(30, this.ctx.currentTime + 0.25);

    gain.gain.setValueAtTime(Math.min(strength * 0.3, 0.4), this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);

    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.3);
  }

  playFanfare() {
    if (!this.initialized || this.muted || !this.ctx) return;
    const notes = [523.25, 659.25, 783.99, 1046.50];
    notes.forEach((freq, idx) => {
      setTimeout(() => {
        if (!this.ctx) return;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
        gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.4);
        osc.connect(gain);
        gain.connect(this.ctx.destination);
        osc.start();
        osc.stop(this.ctx.currentTime + 0.4);
      }, idx * 120);
    });
  }

  toggleMute() {
    this.muted = !this.muted;
    if (this.engineGain) {
      this.engineGain.gain.setValueAtTime(this.muted ? 0 : 0.1, this.ctx.currentTime);
    }
    if (this.skidGain && this.muted) {
      this.skidGain.gain.setValueAtTime(0, this.ctx.currentTime);
    }
    return !this.muted;
  }
}

// ==========================================
// 2. PROCEDURAL TEXTURES (Pure Canvas Generation)
// ==========================================
const TextureGenerator = {
  createTarmacTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 512;
    canvas.height = 512;
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = '#2b2d30';
    ctx.fillRect(0, 0, 512, 512);

    // High frequency asphalt noise
    for (let i = 0; i < 45000; i++) {
      const v = Math.floor(Math.random() * 40 + 30);
      ctx.fillStyle = `rgba(${v},${v},${v}, 0.6)`;
      ctx.fillRect(Math.random() * 512, Math.random() * 512, 2, 2);
    }

    // Tire grooves
    ctx.strokeStyle = 'rgba(20,20,20,0.25)';
    ctx.lineWidth = 14;
    ctx.beginPath();
    ctx.moveTo(128, 0); ctx.lineTo(128, 512);
    ctx.moveTo(384, 0); ctx.lineTo(384, 512);
    ctx.stroke();

    // Road dashed center line
    ctx.strokeStyle = '#f8fafc';
    ctx.lineWidth = 6;
    ctx.setLineDash([30, 24]);
    ctx.beginPath();
    ctx.moveTo(256, 0);
    ctx.lineTo(256, 512);
    ctx.stroke();

    // Road side edges (solid white / yellow stripes)
    ctx.setLineDash([]);
    ctx.strokeStyle = '#e2e8f0';
    ctx.lineWidth = 5;
    ctx.beginPath();
    ctx.moveTo(35, 0); ctx.lineTo(35, 512);
    ctx.moveTo(477, 0); ctx.lineTo(477, 512);
    ctx.stroke();

    const texture = new THREE.CanvasTexture(canvas);
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    return texture;
  },

  createGrassTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 512;
    canvas.height = 512;
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = '#2d5a27';
    ctx.fillRect(0, 0, 512, 512);

    for (let i = 0; i < 30000; i++) {
      const g = Math.floor(Math.random() * 60 + 60);
      const r = Math.floor(g * 0.45);
      ctx.fillStyle = `rgba(${r},${g},30, 0.4)`;
      ctx.fillRect(Math.random() * 512, Math.random() * 512, 3, 3);
    }

    const texture = new THREE.CanvasTexture(canvas);
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.repeat.set(40, 40);
    return texture;
  },

  createKerbTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 64;
    canvas.height = 64;
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = '#dc2626';
    ctx.fillRect(0, 0, 64, 32);
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 32, 64, 32);
    const texture = new THREE.CanvasTexture(canvas);
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    return texture;
  },

  createSponsorSignTexture(text, color1, color2) {
    const canvas = document.createElement('canvas');
    canvas.width = 512;
    canvas.height = 128;
    const ctx = canvas.getContext('2d');
    const grad = ctx.createLinearGradient(0, 0, 512, 128);
    grad.addColorStop(0, color1);
    grad.addColorStop(1, color2);
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, 512, 128);

    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 8;
    ctx.strokeRect(6, 6, 500, 116);

    ctx.fillStyle = '#ffffff';
    ctx.font = 'bold 54px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(text, 256, 64);

    return new THREE.CanvasTexture(canvas);
  },

  createCheckerTexture() {
    const canvas = document.createElement('canvas');
    canvas.width = 128;
    canvas.height = 128;
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = '#111827';
    ctx.fillRect(0, 0, 128, 128);
    ctx.fillStyle = '#ffffff';
    const s = 32;
    for (let r = 0; r < 4; r++) {
      for (let c = 0; c < 4; c++) {
        if ((r + c) % 2 === 0) {
          ctx.fillRect(c * s, r * s, s, s);
        }
      }
    }
    const texture = new THREE.CanvasTexture(canvas);
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    return texture;
  }
};

// ==========================================
// 3. TRACK & SPLINE GEOMETRY SYSTEM
// ==========================================
class TrackSystem {
  constructor(scene) {
    this.scene = scene;
    this.trackWidth = 13.5;
    this.shoulderWidth = 2.2;
    this.trackLength = 0;
    this.obstacles = []; // Array of { position, radius, type }
    this.checkpoints = []; // Array of { index, position, forward, tangent }
    this.surfaceTypes = []; // Array of surface zones along spline t

    this.buildSplineTrack();
    this.buildTrackGeometry();
    this.buildScenery();
  }

  buildSplineTrack() {
    // 3D Control Points: looping rally track with hills, tight hairpins, sweeping curves, and elevation dips
    const rawPoints = [
      new THREE.Vector3(0, 0, 0),
      new THREE.Vector3(60, 4, 120),
      new THREE.Vector3(140, 12, 230),
      new THREE.Vector3(260, 20, 270),
      new THREE.Vector3(380, 26, 210),
      new THREE.Vector3(450, 18, 90),
      new THREE.Vector3(410, 8, -60),
      new THREE.Vector3(310, -2, -180),
      new THREE.Vector3(230, -8, -270),
      new THREE.Vector3(120, -14, -320),
      new THREE.Vector3(-40, -6, -350),
      new THREE.Vector3(-180, 5, -310),
      new THREE.Vector3(-290, 18, -210),
      new THREE.Vector3(-360, 25, -70),
      new THREE.Vector3(-340, 15, 80),
      new THREE.Vector3(-240, 8, 200),
      new THREE.Vector3(-120, 2, 100),
      new THREE.Vector3(-50, 0, 40)
    ];

    this.curve = new THREE.CatmullRomCurve3(rawPoints, true, 'centripetal', 0.5);
    this.trackLength = this.curve.getLength();

    // Define Checkpoints (10 checkpoints around the loop)
    const numCheckpoints = 12;
    for (let i = 0; i < numCheckpoints; i++) {
      const u = i / numCheckpoints;
      const pt = this.curve.getPointAt(u);
      const tangent = this.curve.getTangentAt(u).normalize();
      this.checkpoints.push({
        id: i,
        u: u,
        position: pt,
        tangent: tangent,
        radius: this.trackWidth * 1.3
      });
    }

    // Define Surface Types: Asphalt vs Dirt/Gravel sections
    // E.g., u between 0.35 and 0.65 is Dirt rally segment
    this.surfaceTypes = [
      { start: 0.00, end: 0.30, type: 'tarmac', friction: 1.0, topSpeed: 175 },
      { start: 0.30, end: 0.65, type: 'dirt', friction: 0.78, topSpeed: 145 },
      { start: 0.65, end: 0.85, type: 'tarmac', friction: 1.0, topSpeed: 175 },
      { start: 0.85, end: 1.00, type: 'tarmac', friction: 1.0, topSpeed: 175 }
    ];
  }

  getSurfaceAt(u) {
    const normU = ((u % 1) + 1) % 1;
    for (let s of this.surfaceTypes) {
      if (normU >= s.start && normU <= s.end) {
        return s;
      }
    }
    return { type: 'tarmac', friction: 1.0, topSpeed: 175 };
  }

  buildTrackGeometry() {
    const numSteps = 700;
    const roadVertices = [];
    const roadUvs = [];
    const roadIndices = [];

    const kerbLeftVertices = [];
    const kerbLeftIndices = [];
    const kerbRightVertices = [];
    const kerbRightIndices = [];

    const up = new THREE.Vector3(0, 1, 0);

    for (let i = 0; i <= numSteps; i++) {
      const u = (i / numSteps) % 1;
      const point = this.curve.getPointAt(u);
      const tangent = this.curve.getTangentAt(u).normalize();
      const normal = new THREE.Vector3().crossVectors(up, tangent).normalize();

      // Road width span
      const halfW = this.trackWidth * 0.5;
      const leftPt = new THREE.Vector3().addVectors(point, normal.clone().multiplyScalar(-halfW));
      const rightPt = new THREE.Vector3().addVectors(point, normal.clone().multiplyScalar(halfW));

      // Slight banking on turns based on curve curvature
      leftPt.y += 0.05;
      rightPt.y += 0.05;

      roadVertices.push(leftPt.x, leftPt.y, leftPt.z);
      roadVertices.push(rightPt.x, rightPt.y, rightPt.z);

      const vProgress = (i / numSteps) * 60; // texture repetition along length
      roadUvs.push(0, vProgress);
      roadUvs.push(1, vProgress);

      // Kerbs / Shoulders
      const kerbW = this.shoulderWidth;
      const outerLeft = new THREE.Vector3().addVectors(leftPt, normal.clone().multiplyScalar(-kerbW));
      outerLeft.y += 0.1;
      kerbLeftVertices.push(leftPt.x, leftPt.y, leftPt.z);
      kerbLeftVertices.push(outerLeft.x, outerLeft.y, outerLeft.z);

      const outerRight = new THREE.Vector3().addVectors(rightPt, normal.clone().multiplyScalar(kerbW));
      outerRight.y += 0.1;
      kerbRightVertices.push(rightPt.x, rightPt.y, rightPt.z);
      kerbRightVertices.push(outerRight.x, outerRight.y, outerRight.z);

      if (i < numSteps) {
        const base = i * 2;
        roadIndices.push(base, base + 1, base + 2);
        roadIndices.push(base + 1, base + 3, base + 2);

        kerbLeftIndices.push(base, base + 1, base + 2);
        kerbLeftIndices.push(base + 1, base + 3, base + 2);

        kerbRightIndices.push(base, base + 1, base + 2);
        kerbRightIndices.push(base + 1, base + 3, base + 2);
      }
    }

    // 1. Road Ribbon Mesh
    const roadGeo = new THREE.BufferGeometry();
    roadGeo.setAttribute('position', new THREE.Float32BufferAttribute(roadVertices, 3));
    roadGeo.setAttribute('uv', new THREE.Float32BufferAttribute(roadUvs, 2));
    roadGeo.setIndex(roadIndices);
    roadGeo.computeVertexNormals();

    const roadMat = new THREE.MeshStandardMaterial({
      map: TextureGenerator.createTarmacTexture(),
      roughness: 0.85,
      metalness: 0.1,
      polygonOffset: true,
      polygonOffsetFactor: -1,
      polygonOffsetUnits: -1
    });
    this.roadMesh = new THREE.Mesh(roadGeo, roadMat);
    this.roadMesh.receiveShadow = true;
    this.scene.add(this.roadMesh);

    // 2. Kerb Meshes
    const kerbMat = new THREE.MeshStandardMaterial({
      map: TextureGenerator.createKerbTexture(),
      roughness: 0.7
    });
    kerbMat.map.repeat.set(1, 120);

    const kerbLeftGeo = new THREE.BufferGeometry();
    kerbLeftGeo.setAttribute('position', new THREE.Float32BufferAttribute(kerbLeftVertices, 3));
    kerbLeftGeo.setIndex(kerbLeftIndices);
    kerbLeftGeo.computeVertexNormals();
    this.scene.add(new THREE.Mesh(kerbLeftGeo, kerbMat));

    const kerbRightGeo = new THREE.BufferGeometry();
    kerbRightGeo.setAttribute('position', new THREE.Float32BufferAttribute(kerbRightVertices, 3));
    kerbRightGeo.setIndex(kerbRightIndices);
    kerbRightGeo.computeVertexNormals();
    this.scene.add(new THREE.Mesh(kerbRightGeo, kerbMat));

    // 3. Start / Finish Gantry
    this.buildStartGantry();

    // 4. Infinite Terrain Grid with Hills
    this.buildTerrain();
  }

  buildStartGantry() {
    const startPt = this.curve.getPointAt(0);
    const startTangent = this.curve.getTangentAt(0).normalize();
    const normal = new THREE.Vector3().crossVectors(new THREE.Vector3(0, 1, 0), startTangent).normalize();
    const gantryGroup = new THREE.Group();

    // Steel Pillars
    const pillarGeo = new THREE.BoxGeometry(0.8, 8, 0.8);
    const pillarMat = new THREE.MeshStandardMaterial({ color: 0x22262d, roughness: 0.4, metalness: 0.8 });

    const pLeft = new THREE.Mesh(pillarGeo, pillarMat);
    pLeft.position.copy(startPt).add(normal.clone().multiplyScalar(-this.trackWidth * 0.65));
    pLeft.position.y += 4;
    gantryGroup.add(pLeft);

    const pRight = new THREE.Mesh(pillarGeo, pillarMat);
    pRight.position.copy(startPt).add(normal.clone().multiplyScalar(this.trackWidth * 0.65));
    pRight.position.y += 4;
    gantryGroup.add(pRight);

    // Cross Beam Banner
    const beamGeo = new THREE.BoxGeometry(this.trackWidth * 1.4, 2.0, 1.2);
    const beamMat = new THREE.MeshStandardMaterial({
      map: TextureGenerator.createSponsorSignTexture('🏁 START / FINISH 🏁', '#dc2626', '#991b1b'),
      roughness: 0.5
    });
    const beam = new THREE.Mesh(beamGeo, beamMat);
    beam.position.copy(startPt);
    beam.position.y += 7.5;
    beam.lookAt(beam.position.clone().add(startTangent));
    gantryGroup.add(beam);

    // Start line checker stripe on asphalt
    const startLineGeo = new THREE.PlaneGeometry(this.trackWidth, 2.5);
    const startLineMat = new THREE.MeshStandardMaterial({
      map: TextureGenerator.createCheckerTexture(),
      roughness: 0.5,
      polygonOffset: true,
      polygonOffsetFactor: -3,
      polygonOffsetUnits: -3
    });
    startLineMat.map.repeat.set(8, 2);
    const startLine = new THREE.Mesh(startLineGeo, startLineMat);
    startLine.position.copy(startPt).add(new THREE.Vector3(0, 0.08, 0));
    startLine.rotation.x = -Math.PI / 2;
    startLine.lookAt(startPt.clone().add(new THREE.Vector3(0, 1, 0)));
    startLine.rotateZ(Math.atan2(startTangent.x, startTangent.z));
    gantryGroup.add(startLine);

    this.scene.add(gantryGroup);
  }

  buildTerrain() {
    const size = 1600;
    const segs = 128;
    const terrainGeo = new THREE.PlaneGeometry(size, size, segs, segs);
    terrainGeo.rotateX(-Math.PI / 2);

    const pos = terrainGeo.attributes.position;
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i);
      const z = pos.getZ(i);

      // Find distance to track spline to carve track depression/smooth bed
      const queryPt = new THREE.Vector3(x, 0, z);
      const { dist, trackHeight } = this.getApproxDistanceToTrack(queryPt);

      // Ambient rolling hills outside track
      let hill = Math.sin(x * 0.008) * Math.cos(z * 0.008) * 35 +
                 Math.sin(x * 0.02 + z * 0.015) * 12 +
                 Math.sin(x * 0.05) * 4;

      // Blend smoothly into track elevation
      if (dist < 40) {
        const factor = Math.max(0, (dist - this.trackWidth * 0.7) / (40 - this.trackWidth * 0.7));
        // Smooth step
        const smoothF = factor * factor * (3 - 2 * factor);
        const finalY = (1 - smoothF) * (trackHeight - 0.25) + smoothF * hill;
        pos.setY(i, finalY);
      } else {
        pos.setY(i, hill);
      }
    }
    terrainGeo.computeVertexNormals();

    const terrainMat = new THREE.MeshStandardMaterial({
      map: TextureGenerator.createGrassTexture(),
      roughness: 0.9,
      metalness: 0.05
    });

    const terrain = new THREE.Mesh(terrainGeo, terrainMat);
    terrain.position.y = -0.2;
    terrain.receiveShadow = true;
    this.scene.add(terrain);
    this.terrainMesh = terrain;
  }

  getApproxDistanceToTrack(point) {
    let minDistSq = Infinity;
    let nearestHeight = 0;
    const testSteps = 100;
    for (let i = 0; i < testSteps; i++) {
      const p = this.curve.getPointAt(i / testSteps);
      const dx = p.x - point.x;
      const dz = p.z - point.z;
      const d2 = dx * dx + dz * dz;
      if (d2 < minDistSq) {
        minDistSq = d2;
        nearestHeight = p.y;
      }
    }
    return { dist: Math.sqrt(minDistSq), trackHeight: nearestHeight };
  }

  buildScenery() {
    // Shared geometries for performance
    const trunkGeo = new THREE.CylinderGeometry(0.3, 0.5, 3.5, 6);
    const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3d271d, roughness: 0.9 });

    const foliageGeo = new THREE.ConeGeometry(2.4, 6.0, 7);
    const foliageMat1 = new THREE.MeshStandardMaterial({ color: 0x1e3a1e, roughness: 0.8, flatShading: true });
    const foliageMat2 = new THREE.MeshStandardMaterial({ color: 0x143414, roughness: 0.8, flatShading: true });
    const foliageMat3 = new THREE.MeshStandardMaterial({ color: 0x2e4e1e, roughness: 0.8, flatShading: true });
    const foliageMats = [foliageMat1, foliageMat2, foliageMat3];

    const rockGeo = new THREE.DodecahedronGeometry(1.5, 1);
    const rockMat = new THREE.MeshStandardMaterial({ color: 0x64748b, roughness: 0.9, flatShading: true });

    // Place trees and rocks around track corridor
    const numItems = 280;
    for (let i = 0; i < numItems; i++) {
      const u = Math.random();
      const pt = this.curve.getPointAt(u);
      const tangent = this.curve.getTangentAt(u).normalize();
      const normal = new THREE.Vector3().crossVectors(new THREE.Vector3(0, 1, 0), tangent).normalize();

      // Side: left (-1) or right (+1)
      const side = Math.random() > 0.5 ? 1 : -1;
      const offsetDist = this.trackWidth * 0.7 + 3.0 + Math.random() * 55;
      const pos = pt.clone().add(normal.clone().multiplyScalar(side * offsetDist));
      pos.y = this.getGroundHeight(pos.x, pos.z);

      if (Math.random() < 0.75) {
        // Place Low-Poly Pine Tree
        const treeGroup = new THREE.Group();
        const trunk = new THREE.Mesh(trunkGeo, trunkMat);
        trunk.position.y = 1.75;
        trunk.castShadow = true;
        treeGroup.add(trunk);

        const folMat = foliageMats[Math.floor(Math.random() * foliageMats.length)];
        const foliage1 = new THREE.Mesh(foliageGeo, folMat);
        foliage1.position.y = 5.0;
        foliage1.scale.set(1, 1, 1);
        foliage1.castShadow = true;
        treeGroup.add(foliage1);

        const foliage2 = new THREE.Mesh(foliageGeo, folMat);
        foliage2.position.y = 7.5;
        foliage2.scale.set(0.75, 0.85, 0.75);
        foliage2.castShadow = true;
        treeGroup.add(foliage2);

        const s = 0.8 + Math.random() * 0.7;
        treeGroup.scale.set(s, s, s);
        treeGroup.position.copy(pos);
        treeGroup.rotation.y = Math.random() * Math.PI * 2;
        this.scene.add(treeGroup);

        this.obstacles.push({
          position: pos,
          radius: 1.2 * s,
          type: 'tree',
          elasticity: 0.2
        });
      } else {
        // Place Low-Poly Boulder / Rock
        const rock = new THREE.Mesh(rockGeo, rockMat);
        const s = 1.0 + Math.random() * 1.5;
        rock.scale.set(s, s * (0.6 + Math.random() * 0.4), s);
        rock.position.copy(pos);
        rock.position.y += s * 0.4;
        rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
        rock.castShadow = true;
        this.scene.add(rock);

        this.obstacles.push({
          position: pos,
          radius: 1.5 * s,
          type: 'rock',
          elasticity: 0.4
        });
      }
    }

    // Place Trackside Sponsor Billboards & Tire Barriers
    this.buildTracksideProps();
  }

  buildTracksideProps() {
    const bannerSponsors = [
      { text: 'PIRELLI RALLY', c1: '#eab308', c2: '#ca8a04' },
      { text: 'BREMBO BRAKES', c1: '#ef4444', c2: '#b91c1c' },
      { text: 'MOTUL SYNTH', c1: '#ef4444', c2: '#1e293b' },
      { text: 'SPARCO RACING', c1: '#3b82f6', c2: '#1d4ed8' },
      { text: 'TURBO BOOST', c1: '#10b981', c2: '#047857' }
    ];

    const billboardGeo = new THREE.BoxGeometry(7, 2.4, 0.3);
    const postGeo = new THREE.CylinderGeometry(0.12, 0.12, 3, 6);
    const postMat = new THREE.MeshStandardMaterial({ color: 0x475569, metalness: 0.7 });

    const numSigns = 14;
    for (let i = 0; i < numSigns; i++) {
      const u = (i / numSigns) + 0.03;
      const pt = this.curve.getPointAt(u);
      const tangent = this.curve.getTangentAt(u).normalize();
      const normal = new THREE.Vector3().crossVectors(new THREE.Vector3(0, 1, 0), tangent).normalize();

      const side = (i % 2 === 0) ? 1 : -1;
      const pos = pt.clone().add(normal.clone().multiplyScalar(side * (this.trackWidth * 0.5 + 2.6)));
      pos.y = pt.y;

      const signGroup = new THREE.Group();
      const sponsor = bannerSponsors[i % bannerSponsors.length];
      const signMat = new THREE.MeshStandardMaterial({
        map: TextureGenerator.createSponsorSignTexture(sponsor.text, sponsor.c1, sponsor.c2),
        roughness: 0.4
      });

      const board = new THREE.Mesh(billboardGeo, signMat);
      board.position.y = 2.2;
      board.castShadow = true;
      signGroup.add(board);

      const p1 = new THREE.Mesh(postGeo, postMat);
      p1.position.set(-2.5, 1.2, 0);
      signGroup.add(p1);
      const p2 = new THREE.Mesh(postGeo, postMat);
      p2.position.set(2.5, 1.2, 0);
      signGroup.add(p2);

      signGroup.position.copy(pos);
      signGroup.lookAt(pt.clone().add(new THREE.Vector3(0, 2, 0)));
      this.scene.add(signGroup);

      this.obstacles.push({
        position: pos,
        radius: 2.2,
        type: 'billboard',
        elasticity: 0.35
      });
    }

    // Tire stack barriers on sharp curve apexes
    const tireGeo = new THREE.CylinderGeometry(0.7, 0.7, 1.2, 10);
    const tireMat = new THREE.MeshStandardMaterial({ color: 0x18181b, roughness: 0.9 });
    for (let i = 0; i < 8; i++) {
      const u = (i * 0.125) + 0.08;
      const pt = this.curve.getPointAt(u);
      const tangent = this.curve.getTangentAt(u).normalize();
      const normal = new THREE.Vector3().crossVectors(new THREE.Vector3(0, 1, 0), tangent).normalize();

      const pos = pt.clone().add(normal.clone().multiplyScalar(this.trackWidth * 0.5 + 1.2));
      pos.y = pt.y + 0.6;

      const tireStack = new THREE.Mesh(tireGeo, tireMat);
      tireStack.position.copy(pos);
      tireStack.castShadow = true;
      this.scene.add(tireStack);

      this.obstacles.push({
        position: pos,
        radius: 1.0,
        type: 'barrier',
        elasticity: 0.6
      });
    }
  }

  getGroundHeight(x, z) {
    const queryPt = new THREE.Vector3(x, 0, z);
    const { dist, trackHeight } = this.getApproxDistanceToTrack(queryPt);
    let hill = Math.sin(x * 0.008) * Math.cos(z * 0.008) * 35 +
               Math.sin(x * 0.02 + z * 0.015) * 12 +
               Math.sin(x * 0.05) * 4;
    if (dist < 40) {
      const factor = Math.max(0, (dist - this.trackWidth * 0.7) / (40 - this.trackWidth * 0.7));
      const smoothF = factor * factor * (3 - 2 * factor);
      return (1 - smoothF) * (trackHeight - 0.25) + smoothF * hill;
    }
    return hill;
  }

  getTrackProjection(carPos) {
    // Find closest point on spline to car position
    const sampleSteps = 200;
    let closestU = 0;
    let minDistSq = Infinity;
    for (let i = 0; i < sampleSteps; i++) {
      const u = i / sampleSteps;
      const p = this.curve.getPointAt(u);
      const distSq = p.distanceToSquared(carPos);
      if (distSq < minDistSq) {
        minDistSq = distSq;
        closestU = u;
      }
    }

    // Refine search around closest sample
    let bestU = closestU;
    const delta = 1.0 / sampleSteps;
    for (let i = -10; i <= 10; i++) {
      const u = ((closestU + i * (delta * 0.1)) % 1 + 1) % 1;
      const p = this.curve.getPointAt(u);
      const dSq = p.distanceToSquared(carPos);
      if (dSq < minDistSq) {
        minDistSq = dSq;
        bestU = u;
      }
    }

    const trackCenter = this.curve.getPointAt(bestU);
    const tangent = this.curve.getTangentAt(bestU).normalize();
    const normal = new THREE.Vector3().crossVectors(new THREE.Vector3(0, 1, 0), tangent).normalize();

    // Lateral distance from centerline
    const offset = new THREE.Vector3().subVectors(carPos, trackCenter);
    const lateralDist = offset.dot(normal);
    const isOffTrack = Math.abs(lateralDist) > (this.trackWidth * 0.5 + this.shoulderWidth);
    const isShoulder = Math.abs(lateralDist) > (this.trackWidth * 0.5) && !isOffTrack;

    return {
      u: bestU,
      trackCenter: trackCenter,
      tangent: tangent,
      lateralDist: lateralDist,
      isOffTrack: isOffTrack,
      isShoulder: isShoulder,
      surface: this.getSurfaceAt(bestU)
    };
  }
}

// ==========================================
// 4. PROCEDURAL RALLY CAR & PHYSICS
// ==========================================
class RallyCar {
  constructor(scene) {
    this.scene = scene;

    // Physical state
    this.position = new THREE.Vector3(0, 1, 0);
    this.velocity = new THREE.Vector3();
    this.speed = 0; // km/h
    this.heading = 0; // radians yaw
    this.steerAngle = 0; // current steering angle
    this.targetSteer = 0;
    this.angularVelocity = 0;
    this.driftFactor = 0;
    this.rpm = 1000;
    this.gear = 1;
    this.isBraking = false;
    this.isHandbrake = false;
    this.throttle = 0;
    this.pitch = 0;
    this.roll = 0;

    // Dimensions
    this.width = 2.0;
    this.length = 4.4;
    this.height = 1.45;
    this.wheelBase = 2.6;

    this.buildCarModel();
  }

  buildCarModel() {
    this.mesh = new THREE.Group();

    // Materials
    const bodyPaintMat = new THREE.MeshStandardMaterial({
      color: 0xef4444, // Racing Red Rally livery
      roughness: 0.25,
      metalness: 0.6
    });

    const liveryWhiteMat = new THREE.MeshStandardMaterial({
      color: 0xf8fafc,
      roughness: 0.3,
      metalness: 0.3
    });

    const windowGlassMat = new THREE.MeshStandardMaterial({
      color: 0x0f172a,
      roughness: 0.1,
      metalness: 0.9,
      transparent: true,
      opacity: 0.85
    });

    const trimDarkMat = new THREE.MeshStandardMaterial({
      color: 0x1e293b,
      roughness: 0.7,
      metalness: 0.4
    });

    // 1. Lower Chassis Base
    const lowerBodyGeo = new THREE.BoxGeometry(1.9, 0.55, 4.2);
    const lowerBody = new THREE.Mesh(lowerBodyGeo, bodyPaintMat);
    lowerBody.position.y = 0.5;
    lowerBody.castShadow = true;
    this.mesh.add(lowerBody);

    // 2. Cabin / Cockpit Roof
    const cabinGeo = new THREE.BoxGeometry(1.6, 0.65, 2.2);
    const cabin = new THREE.Mesh(cabinGeo, windowGlassMat);
    cabin.position.set(0, 1.05, -0.3);
    cabin.castShadow = true;
    this.mesh.add(cabin);

    // Roof Livery Plate
    const roofGeo = new THREE.BoxGeometry(1.62, 0.08, 2.1);
    const roof = new THREE.Mesh(roofGeo, liveryWhiteMat);
    roof.position.set(0, 1.4, -0.3);
    this.mesh.add(roof);

    // 3. Rally Hood Scoop
    const hoodScoopGeo = new THREE.BoxGeometry(0.7, 0.15, 0.8);
    const hoodScoop = new THREE.Mesh(hoodScoopGeo, trimDarkMat);
    hoodScoop.position.set(0, 0.82, 1.1);
    this.mesh.add(hoodScoop);

    // Front Bumper / Splitter
    const splitterGeo = new THREE.BoxGeometry(2.0, 0.25, 0.6);
    const splitter = new THREE.Mesh(splitterGeo, trimDarkMat);
    splitter.position.set(0, 0.25, 2.1);
    splitter.castShadow = true;
    this.mesh.add(splitter);

    // 4. Large Rally Rear Spoiler / Wing
    const wingGeo = new THREE.BoxGeometry(2.1, 0.08, 0.6);
    const wing = new THREE.Mesh(wingGeo, liveryWhiteMat);
    wing.position.set(0, 1.45, -2.05);

    const wingPostGeo = new THREE.BoxGeometry(0.08, 0.45, 0.35);
    const wingPostL = new THREE.Mesh(wingPostGeo, trimDarkMat);
    wingPostL.position.set(-0.7, 1.2, -2.0);
    const wingPostR = new THREE.Mesh(wingPostGeo, trimDarkMat);
    wingPostR.position.set(0.7, 1.2, -2.0);

    this.mesh.add(wing);
    this.mesh.add(wingPostL);
    this.mesh.add(wingPostR);

    // 5. Headlights & Quad Rally Pods
    const lightMat = new THREE.MeshStandardMaterial({
      color: 0xffffff,
      emissive: 0xffffff,
      emissiveIntensity: 0.8
    });
    const headlightGeo = new THREE.BoxGeometry(0.35, 0.2, 0.1);
    const hlLeft = new THREE.Mesh(headlightGeo, lightMat);
    hlLeft.position.set(-0.75, 0.65, 2.12);
    const hlRight = new THREE.Mesh(headlightGeo, lightMat);
    hlRight.position.set(0.75, 0.65, 2.12);
    this.mesh.add(hlLeft);
    this.mesh.add(hlRight);

    // Center Rally Spotlights (4 circular pods)
    const podGeo = new THREE.CylinderGeometry(0.18, 0.18, 0.2, 8);
    podGeo.rotateX(Math.PI / 2);
    for (let i = 0; i < 4; i++) {
      const pod = new THREE.Mesh(podGeo, lightMat);
      pod.position.set(-0.45 + i * 0.3, 0.75, 2.15);
      this.mesh.add(pod);
    }

    // 6. Taillights & Brake Glow
    this.tailLightMat = new THREE.MeshStandardMaterial({
      color: 0xef4444,
      emissive: 0xef4444,
      emissiveIntensity: 0.4
    });
    const tailLightGeo = new THREE.BoxGeometry(0.4, 0.2, 0.1);
    const tlLeft = new THREE.Mesh(tailLightGeo, this.tailLightMat);
    tlLeft.position.set(-0.75, 0.65, -2.12);
    const tlRight = new THREE.Mesh(tailLightGeo, this.tailLightMat);
    tlRight.position.set(0.75, 0.65, -2.12);
    this.mesh.add(tlLeft);
    this.mesh.add(tlRight);

    // 7. Wheels & Rims
    const tireGeo = new THREE.CylinderGeometry(0.42, 0.42, 0.38, 14);
    tireGeo.rotateZ(Math.PI / 2);
    const tireMat = new THREE.MeshStandardMaterial({ color: 0x1c1917, roughness: 0.9 });

    const rimGeo = new THREE.CylinderGeometry(0.24, 0.24, 0.4, 8);
    rimGeo.rotateZ(Math.PI / 2);
    const rimMat = new THREE.MeshStandardMaterial({ color: 0xf59e0b, metalness: 0.8, roughness: 0.3 }); // Gold rally rims

    this.wheels = [];
    const wheelPositions = [
      { x: -0.95, y: 0.42, z: 1.3, isFront: true },  // Front Left
      { x: 0.95, y: 0.42, z: 1.3, isFront: true },   // Front Right
      { x: -0.95, y: 0.42, z: -1.3, isFront: false }, // Rear Left
      { x: 0.95, y: 0.42, z: -1.3, isFront: false }  // Rear Right
    ];

    wheelPositions.forEach((wp) => {
      const wheelGroup = new THREE.Group();
      const tire = new THREE.Mesh(tireGeo, tireMat);
      tire.castShadow = true;
      const rim = new THREE.Mesh(rimGeo, rimMat);
      wheelGroup.add(tire);
      wheelGroup.add(rim);
      wheelGroup.position.set(wp.x, wp.y, wp.z);

      this.mesh.add(wheelGroup);
      this.wheels.push({ group: wheelGroup, isFront: wp.isFront, basePos: new THREE.Vector3(wp.x, wp.y, wp.z) });
    });

    this.scene.add(this.mesh);
  }

  reset(position, heading) {
    this.position.copy(position);
    this.position.y += 0.5;
    this.heading = heading;
    this.velocity.set(0, 0, 0);
    this.speed = 0;
    this.steerAngle = 0;
    this.targetSteer = 0;
    this.angularVelocity = 0;
    this.driftFactor = 0;
    this.rpm = 1000;
    this.gear = 1;
    this.pitch = 0;
    this.roll = 0;
    this.updateMeshTransform();
  }

  updatePhysics(dt, input, trackProjection, trackSystem) {
    // 1. Surface Friction Modifiers
    let surfaceGrip = 1.0;
    let surfaceMaxSpeed = 175;

    if (trackProjection.isOffTrack) {
      surfaceGrip = 0.5;
      surfaceMaxSpeed = 75; // Heavy grass penalty
    } else if (trackProjection.isShoulder) {
      surfaceGrip = 0.75;
      surfaceMaxSpeed = 125;
    } else {
      surfaceGrip = trackProjection.surface.friction;
      surfaceMaxSpeed = trackProjection.surface.topSpeed;
    }

    // 2. Throttle & Braking Dynamics
    this.throttle = input.up ? 1 : 0;
    this.isBraking = input.down;
    this.isHandbrake = input.handbrake;

    // Brake Light visual toggle
    if (this.tailLightMat) {
      this.tailLightMat.emissiveIntensity = (this.isBraking || this.isHandbrake) ? 1.6 : 0.4;
    }

    const accelPower = 58.0 * surfaceGrip; // m/s^2 equivalent acceleration force
    const brakePower = 95.0;
    const handbrakeBrake = 55.0;
    const dragCoeff = 0.0016;
    const rollingResistance = 12.0;

    // Forward direction vector of car
    const forward = new THREE.Vector3(Math.sin(this.heading), 0, Math.cos(this.heading));
    const right = new THREE.Vector3(forward.z, 0, -forward.x);

    // Decompose velocity into forward and lateral components
    const forwardSpeed = this.velocity.dot(forward);
    const lateralSpeed = this.velocity.dot(right);

    // Apply engine drive force
    if (input.up) {
      if (forwardSpeed < (surfaceMaxSpeed / 3.6)) {
        this.velocity.add(forward.clone().multiplyScalar(accelPower * dt));
      }
    } else if (input.down) {
      if (forwardSpeed > 2.0) {
        // Normal foot brake
        this.velocity.sub(forward.clone().multiplyScalar(brakePower * dt));
      } else {
        // Reverse gear
        if (forwardSpeed > -12.0) {
          this.velocity.sub(forward.clone().multiplyScalar(22.0 * dt));
        }
      }
    }

    // Handbrake extra drag
    if (this.isHandbrake) {
      if (Math.abs(forwardSpeed) > 1.0) {
        this.velocity.sub(forward.clone().multiplyScalar(Math.sign(forwardSpeed) * handbrakeBrake * dt));
      }
    }

    // Air Resistance & Rolling Friction
    const currentSpeed = this.velocity.length();
    const drag = currentSpeed * currentSpeed * dragCoeff;
    if (currentSpeed > 0.01) {
      const frictionDir = this.velocity.clone().normalize();
      this.velocity.sub(frictionDir.multiplyScalar((drag + rollingResistance) * dt));
      if (this.velocity.length() < 0.1 && !input.up && !input.down) {
        this.velocity.set(0, 0, 0);
      }
    }

    // 3. Speed-dependent Steering
    const maxSteer = 0.65; // ~37 degrees
    const steerSpeedFactor = 1.0 / (1.0 + Math.abs(forwardSpeed) * 0.045);
    const currentMaxSteer = maxSteer * Math.max(0.28, steerSpeedFactor);

    let targetSteer = 0;
    if (input.left) targetSteer += currentMaxSteer;
    if (input.right) targetSteer -= currentMaxSteer;

    // Smooth steering rack interpolation
    this.steerAngle += (targetSteer - this.steerAngle) * Math.min(1.0, 14.0 * dt);

    // 4. Turning & Drift Physics (Lateral grip & slip curve)
    let lateralGripFactor = 16.0 * surfaceGrip;
    if (this.isHandbrake) {
      lateralGripFactor *= 0.22; // Loss of rear traction for initiating drift
    } else if (trackProjection.surface.type === 'dirt') {
      lateralGripFactor *= 0.72; // Loose dirt sliding
    }

    // Lateral grip restores car to pointing direction
    const lateralDamping = Math.min(1.0, lateralGripFactor * dt);
    const newLateralSpeed = lateralSpeed * (1.0 - lateralDamping);

    // Yaw rotation from steering
    const turnRate = (forwardSpeed / this.wheelBase) * Math.tan(this.steerAngle);
    this.heading += turnRate * dt;

    // Drift factor calculation (angle difference between car heading and movement vector)
    if (currentSpeed > 4.0) {
      const movingAngle = Math.atan2(this.velocity.x, this.velocity.z);
      let angleDiff = Math.abs(movingAngle - this.heading);
      while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
      this.driftFactor = Math.min(1.0, Math.abs(angleDiff) * 2.5 + (this.isHandbrake ? 0.6 : 0.0));
    } else {
      this.driftFactor = 0;
    }

    // Reconstruct velocity from updated forward & lateral speeds
    const updatedForward = new THREE.Vector3(Math.sin(this.heading), 0, Math.cos(this.heading));
    const updatedRight = new THREE.Vector3(updatedForward.z, 0, -updatedForward.x);
    this.velocity.copy(updatedForward.multiplyScalar(forwardSpeed)).add(updatedRight.multiplyScalar(newLateralSpeed));

    // Update position
    this.position.add(this.velocity.clone().multiplyScalar(dt));

    // 5. Track Surface Height & Slope Following
    const groundY = trackSystem.getGroundHeight(this.position.x, this.position.z);
    this.position.y += (groundY + 0.35 - this.position.y) * Math.min(1.0, 18.0 * dt);

    // Pitch and roll suspension movement
    const targetPitch = -(forwardSpeed * 0.012) * (input.up ? 1 : 0) + (this.isBraking ? 0.08 : 0);
    this.pitch += (targetPitch - this.pitch) * 8.0 * dt;
    const targetRoll = -this.steerAngle * (forwardSpeed * 0.02) + (this.driftFactor * 0.08 * (input.left ? -1 : 1));
    this.roll += (targetRoll - this.roll) * 8.0 * dt;

    // Speed in KM/H
    this.speed = Math.max(0, forwardSpeed * 3.6);

    // 6. Gear & RPM Simulation
    this.updateGears();

    // 7. Update 3D Object Transforms
    this.updateMeshTransform();
    this.updateWheelVisuals(forwardSpeed, dt);
  }

  updateGears() {
    const kmh = Math.abs(this.speed);
    if (kmh < 1.0 && this.throttle === 0) {
      this.rpm = 900;
      this.gear = 1;
      return;
    }

    // 6-speed rally gearbox ratios
    const gearRatios = [0, 38, 72, 110, 145, 175, 210];
    let g = 1;
    for (let i = 1; i <= 6; i++) {
      if (kmh >= gearRatios[i - 1] && kmh < gearRatios[i]) {
        g = i;
        break;
      }
    }
    if (kmh >= gearRatios[6]) g = 6;
    this.gear = g;

    const prevGearSpeed = gearRatios[g - 1];
    const nextGearSpeed = gearRatios[g];
    const gearProgress = (kmh - prevGearSpeed) / Math.max(1, (nextGearSpeed - prevGearSpeed));
    this.rpm = 2500 + gearProgress * 5500 + (this.throttle * 800);
    this.rpm = Math.min(8500, Math.max(900, this.rpm));
  }

  updateMeshTransform() {
    this.mesh.position.copy(this.position);
    this.mesh.rotation.set(0, 0, 0);
    this.mesh.rotation.y = this.heading;
    this.mesh.rotation.x = this.pitch;
    this.mesh.rotation.z = this.roll;
  }

  updateWheelVisuals(forwardSpeed, dt) {
    const rollAngle = (forwardSpeed / 0.42) * dt;
    this.wheels.forEach((w) => {
      // Wheel rotation
      w.group.children.forEach(c => c.rotation.x += rollAngle);

      // Front wheel steer
      if (w.isFront) {
        w.group.rotation.y = this.steerAngle * 0.85;
      }
    });
  }

  checkObstacleCollisions(obstacles, soundController) {
    let hitAny = false;
    for (let obs of obstacles) {
      const distSq = this.position.distanceToSquared(obs.position);
      const combinedR = (obs.radius + 1.2);
      if (distSq < combinedR * combinedR) {
        // Resolve collision
        const dist = Math.sqrt(distSq);
        const normal = new THREE.Vector3().subVectors(this.position, obs.position).normalize();
        normal.y = 0;

        // Push car out of obstacle
        const penetration = combinedR - dist;
        this.position.add(normal.clone().multiplyScalar(penetration + 0.05));

        // Bounce velocity
        const speed = this.velocity.length();
        this.velocity.reflect(normal).multiplyScalar(obs.elasticity || 0.4);

        if (speed > 5.0) {
          soundController.playImpact(speed / 15.0);
          hitAny = true;
        }
      }
    }
    return hitAny;
  }
}

// ==========================================
// 5. PARTICLE EFFECTS (Dust, Skids, Sparks)
// ==========================================
class ParticleSystem {
  constructor(scene) {
    this.scene = scene;
    this.dustParticles = [];
    this.skidLines = [];

    // Shared Dust Particle Geometry & Material
    const pGeo = new THREE.DodecahedronGeometry(0.28, 0);
    const pMat = new THREE.MeshBasicMaterial({
      color: 0xca9868,
      transparent: true,
      opacity: 0.65
    });

    this.dustMeshPool = [];
    for (let i = 0; i < 120; i++) {
      const m = new THREE.Mesh(pGeo, pMat.clone());
      m.visible = false;
      this.scene.add(m);
      this.dustMeshPool.push(m);
    }
  }

  spawnDust(position, isDirt = false) {
    // Find inactive mesh from pool
    const mesh = this.dustMeshPool.find(m => !m.visible);
    if (!mesh) return;

    mesh.visible = true;
    mesh.position.copy(position);
    mesh.position.y += 0.15;
    mesh.scale.set(1, 1, 1);
    mesh.material.color.setHex(isDirt ? 0x925b30 : 0xd8b488);
    mesh.material.opacity = 0.7;

    const vel = new THREE.Vector3(
      (Math.random() - 0.5) * 2.0,
      1.5 + Math.random() * 2.0,
      (Math.random() - 0.5) * 2.0
    );

    this.dustParticles.push({
      mesh: mesh,
      velocity: vel,
      life: 1.0,
      maxLife: 0.6 + Math.random() * 0.4
    });
  }

  update(dt) {
    for (let i = this.dustParticles.length - 1; i >= 0; i--) {
      const p = this.dustParticles[i];
      p.life -= dt / p.maxLife;

      if (p.life <= 0) {
        p.mesh.visible = false;
        this.dustParticles.splice(i, 1);
      } else {
        p.mesh.position.add(p.velocity.clone().multiplyScalar(dt));
        p.velocity.y += 0.4 * dt; // slight upward drift
        const currentScale = 1.0 + (1.0 - p.life) * 3.5;
        p.mesh.scale.set(currentScale, currentScale, currentScale);
        p.mesh.material.opacity = p.life * 0.6;
      }
    }
  }
}

// ==========================================
// 6. CAMERA CONTROLLER
// ==========================================
class ChaseCamera {
  constructor(camera) {
    this.camera = camera;
    this.mode = 0; // 0: Dynamic Chase, 1: Close Chase, 2: Bonnet / Hood, 3: High Helicopter
    this.targetPos = new THREE.Vector3();
    this.targetLook = new THREE.Vector3();
    this.smoothPos = new THREE.Vector3();
    this.smoothLook = new THREE.Vector3();
  }

  cycleMode() {
    this.mode = (this.mode + 1) % 4;
    return this.mode;
  }

  update(car, dt) {
    const forward = new THREE.Vector3(Math.sin(car.heading), 0, Math.cos(car.heading));
    const speedRatio = Math.min(1.0, car.speed / 180.0);

    let offset = new THREE.Vector3();
    let lookOffset = new THREE.Vector3(0, 1.2, 0);
    let lagFactor = 12.0;

    switch (this.mode) {
      case 0: // Dynamic Behind Chase
        offset = forward.clone().multiplyScalar(-6.8 - speedRatio * 2.2);
        offset.y = 2.6 + speedRatio * 0.8;
        lookOffset = forward.clone().multiplyScalar(4.0).add(new THREE.Vector3(0, 1.2, 0));
        lagFactor = 8.0;
        break;
      case 1: // Tight Close Chase
        offset = forward.clone().multiplyScalar(-4.8);
        offset.y = 1.9;
        lookOffset = forward.clone().multiplyScalar(5.0).add(new THREE.Vector3(0, 1.0, 0));
        lagFactor = 14.0;
        break;
      case 2: // Bonnet / Hood Cam
        offset = forward.clone().multiplyScalar(0.4);
        offset.y = 1.2;
        lookOffset = forward.clone().multiplyScalar(15.0).add(new THREE.Vector3(0, 0.9, 0));
        lagFactor = 25.0;
        break;
      case 3: // High Helicopter Cam
        offset = forward.clone().multiplyScalar(-14.0);
        offset.y = 9.0;
        lookOffset = forward.clone().multiplyScalar(2.0);
        lagFactor = 5.0;
        break;
    }

    this.targetPos.copy(car.position).add(offset);
    this.targetLook.copy(car.position).add(lookOffset);

    // Exponential smoothing for fluid cinematic chase feel
    this.smoothPos.lerp(this.targetPos, Math.min(1.0, lagFactor * dt));
    this.smoothLook.lerp(this.targetLook, Math.min(1.0, (lagFactor + 4.0) * dt));

    this.camera.position.copy(this.smoothPos);
    this.camera.lookAt(this.smoothLook);
  }
}

// ==========================================
// 7. INPUT HANDLER (Keyboard + Mobile Touch)
// ==========================================
class InputManager {
  constructor() {
    this.up = false;
    this.down = false;
    this.left = false;
    this.right = false;
    this.handbrake = false;

    this.keys = {};
    this.setupKeyboard();
    this.setupTouch();
  }

  setupKeyboard() {
    window.addEventListener('keydown', (e) => {
      this.keys[e.code] = true;
      this.updateState();
    });

    window.addEventListener('keyup', (e) => {
      this.keys[e.code] = false;
      this.updateState();
    });
  }

  updateState() {
    this.up = !!(this.keys['KeyW'] || this.keys['ArrowUp']);
    this.down = !!(this.keys['KeyS'] || this.keys['ArrowDown']);
    this.left = !!(this.keys['KeyA'] || this.keys['ArrowLeft']);
    this.right = !!(this.keys['KeyD'] || this.keys['ArrowRight']);
    this.handbrake = !!(this.keys['Space']);
  }

  setupTouch() {
    const bindBtn = (id, prop) => {
      const el = document.getElementById(id);
      if (!el) return;
      const start = (e) => { e.preventDefault(); this[prop] = true; el.classList.add('active'); };
      const end = (e) => { e.preventDefault(); this[prop] = false; el.classList.remove('active'); };
      el.addEventListener('touchstart', start, { passive: false });
      el.addEventListener('touchend', end, { passive: false });
      el.addEventListener('touchcancel', end, { passive: false });
      el.addEventListener('mousedown', start);
      el.addEventListener('mouseup', end);
      el.addEventListener('mouseleave', end);
    };

    bindBtn('t-left', 'left');
    bindBtn('t-right', 'right');
    bindBtn('t-gas', 'up');
    bindBtn('t-brake', 'down');
    bindBtn('t-drift', 'handbrake');
  }
}

// ==========================================
// 8. MINIMAP OVERLAY
// ==========================================
class MinimapRenderer {
  constructor(canvas, trackCurve) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.trackCurve = trackCurve;
    this.points = [];

    // Precalculate 2D points normalized to minimap canvas
    this.calculateBounds();
  }

  calculateBounds() {
    let minX = Infinity, maxX = -Infinity;
    let minZ = Infinity, maxZ = -Infinity;

    const sampleCount = 120;
    for (let i = 0; i <= sampleCount; i++) {
      const p = this.trackCurve.getPointAt(i / sampleCount);
      minX = Math.min(minX, p.x);
      maxX = Math.max(maxX, p.x);
      minZ = Math.min(minZ, p.z);
      maxZ = Math.max(maxZ, p.z);
    }

    const pad = 30;
    const width = (maxX - minX) + pad * 2;
    const height = (maxZ - minZ) + pad * 2;

    this.mapTransform = {
      minX: minX - pad,
      minZ: minZ - pad,
      scaleX: this.canvas.width / width,
      scaleZ: this.canvas.height / height
    };
  }

  render(carPosition, carHeading) {
    const { ctx, canvas } = this;
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw Track Path
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
    ctx.lineWidth = 6;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.beginPath();

    const sampleCount = 80;
    for (let i = 0; i <= sampleCount; i++) {
      const p = this.trackCurve.getPointAt(i / sampleCount);
      const mx = (p.x - this.mapTransform.minX) * this.mapTransform.scaleX;
      const my = (p.z - this.mapTransform.minZ) * this.mapTransform.scaleZ;
      if (i === 0) ctx.moveTo(mx, my);
      else ctx.lineTo(mx, my);
    }
    ctx.closePath();
    ctx.stroke();

    // Start / Finish Line marker
    const startPt = this.trackCurve.getPointAt(0);
    const smx = (startPt.x - this.mapTransform.minX) * this.mapTransform.scaleX;
    const smy = (startPt.z - this.mapTransform.minZ) * this.mapTransform.scaleZ;
    ctx.fillStyle = '#22c55e';
    ctx.beginPath();
    ctx.arc(smx, smy, 4.5, 0, Math.PI * 2);
    ctx.fill();

    // Draw Player Dot & Direction Arrow
    const px = (carPosition.x - this.mapTransform.minX) * this.mapTransform.scaleX;
    const py = (carPosition.z - this.mapTransform.minZ) * this.mapTransform.scaleZ;

    ctx.save();
    ctx.translate(px, py);
    ctx.rotate(-carHeading + Math.PI / 2);

    // Pulsing outer dot
    ctx.fillStyle = 'rgba(239, 68, 68, 0.4)';
    ctx.beginPath();
    ctx.arc(0, 0, 8, 0, Math.PI * 2);
    ctx.fill();

    // Arrow pointer
    ctx.fillStyle = '#f43f5e';
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(0, -6);
    ctx.lineTo(5, 5);
    ctx.lineTo(-5, 5);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.restore();
  }
}

// ==========================================
// 9. MAIN GAME ORCHESTRATOR
// ==========================================
class ApexRallyGame {
  constructor() {
    this.totalLaps = 3;
    this.currentLap = 1;
    this.nextCheckpointIndex = 1;
    this.passedCheckpointsCount = 0;
    this.lapStartTime = 0;
    this.raceStartTime = 0;
    this.currentLapTime = 0;
    this.totalRaceTime = 0;
    this.bestLapTime = null;
    this.lastLapTime = null;
    this.topSpeedRecord = 0;
    this.cleanLaps = 3;

    this.gameState = 'READY'; // READY, COUNTDOWN, RACING, PAUSED, FINISHED

    this.initRenderer();
    this.initScene();
    this.initGameObjects();
    this.initUI();
    this.initEvents();

    this.animate = this.animate.bind(this);
    this.lastFrameTime = performance.now();
    requestAnimationFrame(this.animate);
  }

  initRenderer() {
    this.canvas = document.getElementById('webgl-canvas');
    this.renderer = new THREE.WebGLRenderer({
      canvas: this.canvas,
      antialias: true,
      powerPreference: 'high-performance'
    });
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
    this.renderer.toneMappingExposure = 1.1;

    this.camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 900);
  }

  initScene() {
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x7dd3fc); // Sky Blue
    this.scene.fog = new THREE.FogExp2(0xb0e0f8, 0.0032); // Atmospheric mountain haze

    // Lighting
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.65);
    this.scene.add(ambientLight);

    const sunLight = new THREE.DirectionalLight(0xfff7ed, 1.25);
    sunLight.position.set(120, 200, 80);
    sunLight.castShadow = true;
    sunLight.shadow.mapSize.width = 2048;
    sunLight.shadow.mapSize.height = 2048;
    sunLight.shadow.camera.near = 10;
    sunLight.shadow.camera.far = 500;
    sunLight.shadow.camera.left = -100;
    sunLight.shadow.camera.right = 100;
    sunLight.shadow.camera.top = 100;
    sunLight.shadow.camera.bottom = -100;
    sunLight.shadow.bias = -0.0005;
    this.scene.add(sunLight);
    this.sunLight = sunLight;
  }

  initGameObjects() {
    this.sound = new SoundController();
    this.track = new TrackSystem(this.scene);
    this.car = new RallyCar(this.scene);
    this.particles = new ParticleSystem(this.scene);
    this.chaseCam = new ChaseCamera(this.camera);
    this.input = new InputManager();

    // Minimap
    this.minimap = new MinimapRenderer(
      document.getElementById('minimap-canvas'),
      this.track.curve
    );

    // Initial Car placement at start line pointing down track
    const startPt = this.track.curve.getPointAt(0);
    const startTangent = this.track.curve.getTangentAt(0).normalize();
    const heading = Math.atan2(startTangent.x, startTangent.z);
    this.car.reset(startPt, heading);
    this.chaseCam.smoothPos.copy(this.car.position).add(new THREE.Vector3(0, 3, -8));
    this.chaseCam.smoothLook.copy(this.car.position);
  }

  initUI() {
    this.ui = {
      timeCurrent: document.getElementById('time-current'),
      timeBest: document.getElementById('time-best'),
      timeLast: document.getElementById('time-last'),
      lapCurrent: document.getElementById('lap-current'),
      surfaceTag: document.getElementById('surface-tag'),
      speedVal: document.getElementById('speed-val'),
      rpmBar: document.getElementById('rpm-bar'),
      gearVal: document.getElementById('gear-val'),
      centerBanner: document.getElementById('center-banner'),
      bannerMain: document.getElementById('banner-main'),
      bannerSub: document.getElementById('banner-sub'),
      countdownScreen: document.getElementById('countdown-screen'),
      countdownNum: document.getElementById('countdown-num'),
      pauseModal: document.getElementById('pause-modal'),
      finishModal: document.getElementById('finish-modal'),
      startModal: document.getElementById('start-modal'),
      statTotalTime: document.getElementById('stat-total-time'),
      statBestLap: document.getElementById('stat-best-lap'),
      statTopSpeed: document.getElementById('stat-top-speed'),
      statCleanLaps: document.getElementById('stat-clean-laps'),
      finishGrade: document.getElementById('finish-grade')
    };
  }

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

    document.getElementById('btn-start-game').addEventListener('click', () => {
      this.sound.init();
      this.ui.startModal.style.display = 'none';
      this.startCountdown();
    });

    document.getElementById('btn-cam').addEventListener('click', () => {
      const mode = this.chaseCam.cycleMode();
      const names = ['CHASE CAM', 'CLOSE CAM', 'BONNET CAM', 'HELI CAM'];
      this.showCenterBanner(names[mode], '');
    });

    document.getElementById('btn-audio').addEventListener('click', (e) => {
      const unmuted = this.sound.toggleMute();
      e.target.innerText = unmuted ? '🔊' : '🔇';
    });

    document.getElementById('btn-pause').addEventListener('click', () => this.togglePause());
    document.getElementById('btn-resume').addEventListener('click', () => this.togglePause());
    document.getElementById('btn-restart-pause').addEventListener('click', () => {
      this.ui.pauseModal.style.display = 'none';
      this.restartRace();
    });
    document.getElementById('btn-restart-finish').addEventListener('click', () => {
      this.ui.finishModal.style.display = 'none';
      this.restartRace();
    });

    window.addEventListener('keydown', (e) => {
      if (e.code === 'KeyP') {
        this.togglePause();
      } else if (e.code === 'KeyR') {
        this.resetCarToTrack();
      } else if (e.code === 'KeyC') {
        const mode = this.chaseCam.cycleMode();
        const names = ['CHASE CAM', 'CLOSE CAM', 'BONNET CAM', 'HELI CAM'];
        this.showCenterBanner(names[mode], '');
      } else if (e.code === 'KeyM') {
        const unmuted = this.sound.toggleMute();
        document.getElementById('btn-audio').innerText = unmuted ? '🔊' : '🔇';
      }
    });
  }

  startCountdown() {
    this.gameState = 'COUNTDOWN';
    let count = 3;
    this.ui.countdownScreen.style.display = 'flex';

    const tick = () => {
      if (count > 0) {
        this.ui.countdownNum.innerText = count;
        this.ui.countdownNum.classList.remove('go');
        this.ui.countdownNum.classList.add('show');
        this.sound.playBeep(false);

        setTimeout(() => {
          this.ui.countdownNum.classList.remove('show');
        }, 650);

        count--;
        setTimeout(tick, 1000);
      } else {
        this.ui.countdownNum.innerText = 'GO!';
        this.ui.countdownNum.classList.add('go', 'show');
        this.sound.playBeep(true);

        this.gameState = 'RACING';
        this.raceStartTime = performance.now();
        this.lapStartTime = performance.now();

        setTimeout(() => {
          this.ui.countdownNum.classList.remove('show');
          this.ui.countdownScreen.style.display = 'none';
        }, 900);
      }
    };
    tick();
  }

  togglePause() {
    if (this.gameState === 'RACING') {
      this.gameState = 'PAUSED';
      this.ui.pauseModal.style.display = 'flex';
    } else if (this.gameState === 'PAUSED') {
      this.gameState = 'RACING';
      this.ui.pauseModal.style.display = 'none';
    }
  }

  resetCarToTrack() {
    // Places car back safely onto track at current progress point
    const proj = this.track.getTrackProjection(this.car.position);
    const centerPt = proj.trackCenter;
    const tangent = proj.tangent;
    const heading = Math.atan2(tangent.x, tangent.z);
    this.car.reset(centerPt, heading);
    this.showCenterBanner('CAR RECOVERED', 'Penalty +2.0s');
    this.cleanLaps = Math.max(0, this.cleanLaps - 1);
  }

  restartRace() {
    this.currentLap = 1;
    this.nextCheckpointIndex = 1;
    this.passedCheckpointsCount = 0;
    this.currentLapTime = 0;
    this.totalRaceTime = 0;
    this.bestLapTime = null;
    this.lastLapTime = null;
    this.topSpeedRecord = 0;
    this.cleanLaps = 3;

    this.ui.lapCurrent.innerText = `1 / ${this.totalLaps}`;
    this.ui.timeCurrent.innerText = '00:00.000';
    this.ui.timeBest.innerText = '--:--.---';
    this.ui.timeLast.innerText = '--:--.---';

    const startPt = this.track.curve.getPointAt(0);
    const startTangent = this.track.curve.getTangentAt(0).normalize();
    const heading = Math.atan2(startTangent.x, startTangent.z);
    this.car.reset(startPt, heading);

    this.startCountdown();
  }

  formatTime(ms) {
    const totalSecs = Math.floor(ms / 1000);
    const mins = Math.floor(totalSecs / 60);
    const secs = totalSecs % 60;
    const millis = Math.floor(ms % 1000);
    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}.${String(millis).padStart(3, '0')}`;
  }

  showCenterBanner(mainText, subText = '') {
    this.ui.bannerMain.innerText = mainText;
    this.ui.bannerSub.innerText = subText;
    this.ui.centerBanner.classList.add('active');
    clearTimeout(this.bannerTimer);
    this.bannerTimer = setTimeout(() => {
      this.ui.centerBanner.classList.remove('active');
    }, 2200);
  }

  checkLapProgress(proj) {
    const nextCp = this.track.checkpoints[this.nextCheckpointIndex];
    const distToCp = this.car.position.distanceTo(nextCp.position);

    if (distToCp < nextCp.radius) {
      this.nextCheckpointIndex = (this.nextCheckpointIndex + 1) % this.track.checkpoints.length;
      this.passedCheckpointsCount++;

      // When crossing the finish line after hitting intermediate checkpoints
      if (this.nextCheckpointIndex === 1 && this.passedCheckpointsCount >= (this.track.checkpoints.length - 1)) {
        this.passedCheckpointsCount = 0;
        this.completeLap();
      }
    }
  }

  completeLap() {
    const now = performance.now();
    const lapDuration = now - this.lapStartTime;
    this.lastLapTime = lapDuration;
    this.ui.timeLast.innerText = this.formatTime(lapDuration);

    let isNewBest = false;
    if (!this.bestLapTime || lapDuration < this.bestLapTime) {
      this.bestLapTime = lapDuration;
      this.ui.timeBest.innerText = this.formatTime(lapDuration);
      isNewBest = true;
    }

    if (this.currentLap < this.totalLaps) {
      this.currentLap++;
      this.lapStartTime = now;
      this.ui.lapCurrent.innerText = `${this.currentLap} / ${this.totalLaps}`;
      this.showCenterBanner(`LAP ${this.currentLap}`, isNewBest ? '⭐ NEW BEST LAP!' : `Lap: ${this.formatTime(lapDuration)}`);
      this.sound.playBeep(true);
    } else {
      // Race Finish!
      this.gameState = 'FINISHED';
      this.totalRaceTime = now - this.raceStartTime;
      this.sound.playFanfare();
      this.showFinishModal();
    }
  }

  showFinishModal() {
    this.ui.statTotalTime.innerText = this.formatTime(this.totalRaceTime);
    this.ui.statBestLap.innerText = this.formatTime(this.bestLapTime);
    this.ui.statTopSpeed.innerText = `${Math.round(this.topSpeedRecord)} km/h`;
    this.ui.statCleanLaps.innerText = `${this.cleanLaps} / 3 Laps`;

    if (this.totalRaceTime < 130000) {
      this.ui.finishGrade.innerText = '🏆 GOLD TROPHY - LEGENDARY PACE!';
      this.ui.finishGrade.style.color = '#fbbf24';
    } else if (this.totalRaceTime < 160000) {
      this.ui.finishGrade.innerText = '🥈 SILVER TROPHY - PRO RALLY DRIVER!';
      this.ui.finishGrade.style.color = '#94a3b8';
    } else {
      this.ui.finishGrade.innerText = '🥉 BRONZE TROPHY - FINISHER!';
      this.ui.finishGrade.style.color = '#d97706';
    }

    this.ui.finishModal.style.display = 'flex';
  }

  animate(currentTime) {
    requestAnimationFrame(this.animate);

    const dt = Math.min(0.05, (currentTime - this.lastFrameTime) / 1000.0);
    this.lastFrameTime = currentTime;

    if (this.gameState === 'RACING' || this.gameState === 'COUNTDOWN' || this.gameState === 'FINISHED') {
      const activeInput = (this.gameState === 'RACING') ? this.input : { up: false, down: false, left: false, right: false, handbrake: false };

      // 1. Projection on Track
      const proj = this.track.getTrackProjection(this.car.position);

      // 2. Update Car Physics
      this.car.updatePhysics(dt, activeInput, proj, this.track);

      // 3. Obstacle Collision Resolution
      this.car.checkObstacleCollisions(this.track.obstacles, this.sound);

      // 4. Lap Timing & Checkpoints
      if (this.gameState === 'RACING') {
        this.currentLapTime = currentTime - this.lapStartTime;
        this.ui.timeCurrent.innerText = this.formatTime(this.currentLapTime);
        this.topSpeedRecord = Math.max(this.topSpeedRecord, this.car.speed);
        this.checkLapProgress(proj);
      }

      // 5. Particle Effects (Dust clouds on drift or offroad)
      if (this.car.speed > 15.0) {
        if (this.car.driftFactor > 0.2 || proj.isOffTrack || proj.surface.type === 'dirt') {
          const spawnDirt = proj.isOffTrack || proj.surface.type === 'dirt';
          this.particles.spawnDust(this.car.position, spawnDirt);
        }
      }
      this.particles.update(dt);

      // 6. Camera Follow
      this.chaseCam.update(this.car, dt);

      // 7. Dynamic Sun Light Follow Car for High Res Shadows
      this.sunLight.position.set(this.car.position.x + 80, 160, this.car.position.z + 60);
      this.sunLight.target.position.copy(this.car.position);
      this.sunLight.target.updateMatrixWorld();

      // 8. Sound Synthesizer Updates
      const speedRatio = Math.min(1.0, this.car.speed / 180.0);
      const rpmRatio = (this.car.rpm - 900) / 7600;
      this.sound.updateEngine(rpmRatio, activeInput.up ? 1 : 0, speedRatio);
      this.sound.updateSkid(this.car.driftFactor * (this.car.speed / 80), proj.surface.type === 'dirt' || proj.isOffTrack);

      // 9. Update HUD Elements
      this.updateHUD(proj);
    }

    // Render 3D Scene
    this.renderer.render(this.scene, this.camera);

    // Render Minimap
    this.minimap.render(this.car.position, this.car.heading);
  }

  updateHUD(proj) {
    this.ui.speedVal.innerText = Math.round(this.car.speed);
    const rpmPercent = Math.min(100, Math.max(0, ((this.car.rpm - 900) / 7600) * 100));
    this.ui.rpmBar.style.width = `${rpmPercent}%`;
    this.ui.gearVal.innerText = this.car.speed < 1.0 && !this.input.up && this.input.down ? 'R' : `GEAR ${this.car.gear}`;

    // Surface indicator
    if (proj.isOffTrack) {
      this.ui.surfaceTag.innerText = 'OFF-TRACK (SLOW)';
      this.ui.surfaceTag.className = 'surface-tag offroad';
    } else if (proj.surface.type === 'dirt') {
      this.ui.surfaceTag.innerText = 'GRAVEL / DIRT';
      this.ui.surfaceTag.className = 'surface-tag dirt';
    } else {
      this.ui.surfaceTag.innerText = 'TARMAC';
      this.ui.surfaceTag.className = 'surface-tag';
    }
  }
}

// Instantiate Game on Page Load
window.addEventListener('DOMContentLoaded', () => {
  window.game = new ApexRallyGame();
});
</script>
</body>
</html>