← Canyon Run results

Canyon Run

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 66,957 bytes · SHA-256 7bf216751213
<!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>Solar Velocity - High-Speed Canyon Racer</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }

    body, html {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background-color: #0d0614;
      font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
      color: #fff;
    }

    #canvas-container {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 1;
    }

    /* UI Overlay */
    #ui-layer {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 10;
      pointer-events: none;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding: 24px 32px;
      box-sizing: border-box;
    }

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

    .stat-box {
      background: rgba(18, 9, 32, 0.45);
      border: 1px solid rgba(255, 140, 50, 0.25);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      padding: 12px 20px;
      border-radius: 4px;
      box-shadow: 0 4px 20px rgba(0,0,0,0.5), inset 0 0 15px rgba(255,100,20,0.05);
      letter-spacing: 1px;
    }

    .stat-label {
      font-size: 10px;
      font-weight: 700;
      text-transform: uppercase;
      color: #ff9d5c;
      letter-spacing: 2px;
      margin-bottom: 2px;
    }

    .stat-value {
      font-size: 28px;
      font-weight: 900;
      font-family: 'Courier New', monospace;
      color: #ffffff;
      text-shadow: 0 0 12px rgba(255, 160, 60, 0.6);
    }

    .multiplier-wrap {
      display: flex;
      flex-direction: column;
      align-items: center;
      min-width: 130px;
      transition: transform 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275);
    }

    .multiplier-value {
      font-size: 38px;
      font-weight: 900;
      font-family: 'Courier New', monospace;
      color: #ffde59;
      text-shadow: 0 0 16px rgba(255, 220, 80, 0.8), 0 0 30px rgba(255, 100, 20, 0.5);
    }

    .multiplier-pulse {
      transform: scale(1.35);
      filter: brightness(1.5);
    }

    /* Energy & Speed Center Bottom HUD */
    .hud-bottom {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 14px;
      width: 100%;
      max-width: 640px;
      margin: 0 auto;
    }

    .near-miss-banner {
      font-size: 20px;
      font-weight: 900;
      letter-spacing: 4px;
      text-transform: uppercase;
      color: #fff;
      text-shadow: 0 0 15px #ff5e3a, 0 0 30px #ff2a85;
      opacity: 0;
      transform: translateY(10px) scale(0.9);
      transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
      pointer-events: none;
    }

    .near-miss-banner.active {
      opacity: 1;
      transform: translateY(0) scale(1.15);
    }

    .gauge-container {
      width: 100%;
      display: flex;
      flex-direction: column;
      gap: 8px;
      background: rgba(13, 6, 24, 0.6);
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      padding: 16px 24px;
      border-radius: 8px;
      border: 1px solid rgba(255, 120, 60, 0.3);
      box-shadow: 0 10px 30px rgba(0,0,0,0.6);
    }

    .bar-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 16px;
    }

    .bar-title {
      font-size: 11px;
      font-weight: 800;
      letter-spacing: 2px;
      text-transform: uppercase;
      width: 70px;
      color: #ff9d5c;
    }

    .progress-track {
      flex: 1;
      height: 8px;
      background: rgba(255, 255, 255, 0.08);
      border-radius: 4px;
      overflow: hidden;
      position: relative;
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    .progress-fill {
      height: 100%;
      width: 100%;
      border-radius: 4px;
      transition: width 0.08s linear;
    }

    #energy-fill {
      background: linear-gradient(90deg, #ff2a6d, #ff9500, #ffea00);
      box-shadow: 0 0 12px rgba(255, 149, 0, 0.8);
    }

    #turbo-fill {
      background: linear-gradient(90deg, #05d9e8, #0056ff, #d600ff);
      box-shadow: 0 0 12px rgba(5, 217, 232, 0.8);
    }

    .turbo-ready {
      animation: turboGlow 0.8s infinite alternate;
    }

    @keyframes turboGlow {
      from { box-shadow: 0 0 8px #05d9e8; }
      to { box-shadow: 0 0 20px #05d9e8, 0 0 35px #d600ff; }
    }

    .speedo-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-top: 4px;
    }

    .speedo-marks {
      display: flex;
      gap: 3px;
      flex: 1;
      height: 4px;
      margin: 0 16px;
    }

    .speed-tick {
      flex: 1;
      background: rgba(255,255,255,0.12);
      border-radius: 1px;
      transition: background 0.05s ease;
    }

    .speed-tick.lit {
      background: #ffaa40;
      box-shadow: 0 0 6px #ff8800;
    }

    .speed-tick.lit-boost {
      background: #00f0ff;
      box-shadow: 0 0 8px #00f0ff;
    }

    .speed-digits {
      font-family: 'Courier New', monospace;
      font-size: 14px;
      font-weight: 800;
      color: #ffc480;
      min-width: 65px;
      text-align: right;
    }

    /* Screen Tint / Boost Overlay */
    #speed-vignette {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 5;
      background: radial-gradient(circle, transparent 40%, rgba(255, 60, 0, 0) 80%);
      opacity: 0;
      transition: opacity 0.3s ease;
    }

    #speed-lines-canvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 6;
      opacity: 0;
      transition: opacity 0.2s ease;
    }

    /* Screen Modals (Menu / Game Over) */
    .modal-backdrop {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 100;
      background: rgba(8, 3, 15, 0.78);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      display: flex;
      justify-content: center;
      align-items: center;
      pointer-events: auto;
      transition: opacity 0.4s ease, visibility 0.4s;
    }

    .modal-backdrop.hidden {
      opacity: 0;
      visibility: hidden;
      pointer-events: none;
    }

    .modal-card {
      background: linear-gradient(135deg, rgba(30, 12, 45, 0.9) 0%, rgba(15, 6, 25, 0.95) 100%);
      border: 2px solid rgba(255, 120, 50, 0.4);
      box-shadow: 0 0 50px rgba(255, 60, 0, 0.3), inset 0 0 30px rgba(255, 100, 20, 0.1);
      border-radius: 12px;
      padding: 40px 48px;
      max-width: 480px;
      width: 90%;
      text-align: center;
      color: #fff;
    }

    .game-title {
      font-size: 38px;
      font-weight: 900;
      letter-spacing: 6px;
      text-transform: uppercase;
      margin-bottom: 6px;
      background: linear-gradient(90deg, #ffea00, #ff5e3a, #ff2a85);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      text-shadow: 0 0 25px rgba(255, 94, 58, 0.4);
    }

    .game-subtitle {
      font-size: 13px;
      letter-spacing: 4px;
      text-transform: uppercase;
      color: #ff9d5c;
      margin-bottom: 28px;
    }

    .instructions-list {
      text-align: left;
      font-size: 13px;
      line-height: 1.8;
      color: #d8c8e8;
      margin-bottom: 30px;
      background: rgba(255, 255, 255, 0.04);
      padding: 18px 22px;
      border-radius: 6px;
      border-left: 3px solid #ff7a28;
    }

    .key-badge {
      display: inline-block;
      background: rgba(255, 255, 255, 0.15);
      border: 1px solid rgba(255, 255, 255, 0.3);
      border-radius: 3px;
      padding: 1px 7px;
      font-size: 11px;
      font-weight: 700;
      color: #fff;
      margin: 0 2px;
      font-family: monospace;
    }

    .action-btn {
      background: linear-gradient(90deg, #ff5e3a, #ff2a85);
      color: #ffffff;
      border: none;
      padding: 16px 36px;
      font-size: 16px;
      font-weight: 800;
      letter-spacing: 3px;
      text-transform: uppercase;
      border-radius: 30px;
      cursor: pointer;
      box-shadow: 0 0 25px rgba(255, 94, 58, 0.6);
      transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
      outline: none;
    }

    .action-btn:hover {
      transform: scale(1.05);
      box-shadow: 0 0 35px rgba(255, 94, 58, 0.9), 0 0 60px rgba(255, 42, 133, 0.5);
    }

    .action-btn:active {
      transform: scale(0.97);
    }

    /* Score Table in Game Over */
    .score-summary {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 14px;
      margin: 24px 0 32px 0;
    }

    .summary-item {
      background: rgba(255,255,255,0.04);
      padding: 12px;
      border-radius: 6px;
      border: 1px solid rgba(255,255,255,0.07);
    }

    .summary-item .label {
      font-size: 10px;
      letter-spacing: 1.5px;
      text-transform: uppercase;
      color: #ff9d5c;
      margin-bottom: 4px;
    }

    .summary-item .val {
      font-size: 22px;
      font-weight: 900;
      font-family: 'Courier New', monospace;
      color: #fff;
    }

    .highlight-val {
      color: #ffea00 !important;
      text-shadow: 0 0 10px rgba(255,234,0,0.5);
    }

    /* Mobile Controls Overlay */
    #mobile-controls {
      position: absolute;
      bottom: 120px;
      left: 0;
      width: 100%;
      display: none;
      justify-content: space-between;
      padding: 0 30px;
      box-sizing: border-box;
      pointer-events: auto;
      z-index: 20;
    }

    .dpad-btn, .boost-touch-btn {
      width: 65px;
      height: 65px;
      background: rgba(255,255,255,0.15);
      border: 1px solid rgba(255,255,255,0.3);
      border-radius: 50%;
      color: white;
      font-size: 22px;
      display: flex;
      align-items: center;
      justify-content: center;
      backdrop-filter: blur(5px);
      -webkit-backdrop-filter: blur(5px);
      touch-action: manipulation;
    }

    .dpad-btn:active, .boost-touch-btn:active {
      background: rgba(255, 94, 58, 0.6);
      transform: scale(0.92);
    }

    @media (max-width: 768px) {
      #mobile-controls {
        display: flex;
      }
      .hud-top {
        padding: 0 4px;
      }
      .stat-box {
        padding: 8px 12px;
      }
      .stat-value {
        font-size: 20px;
      }
      .multiplier-value {
        font-size: 26px;
      }
    }
  </style>
  <!-- Three.js CDN -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
  <div id="canvas-container"></div>
  <div id="speed-vignette"></div>
  <canvas id="speed-lines-canvas"></canvas>

  <div id="ui-layer">
    <!-- Top HUD -->
    <div class="hud-top">
      <div class="stat-box">
        <div class="stat-label">Score</div>
        <div class="stat-value" id="score-display">000000</div>
      </div>

      <div class="stat-box multiplier-wrap" id="multiplier-box">
        <div class="stat-label">Multiplier</div>
        <div class="multiplier-value" id="multiplier-display">x1.0</div>
      </div>

      <div class="stat-box" style="text-align: right;">
        <div class="stat-label">Distance</div>
        <div class="stat-value" id="dist-display">0 m</div>
      </div>
    </div>

    <!-- Center Near Miss Announcement -->
    <div class="near-miss-banner" id="near-miss-banner">⚡ NEAR MISS +250 ⚡</div>

    <!-- Bottom HUD (Energy & Speedometer) -->
    <div class="hud-bottom">
      <div class="gauge-container">
        <!-- Energy Bar -->
        <div class="bar-row">
          <div class="bar-title">Solar Core</div>
          <div class="progress-track">
            <div class="progress-fill" id="energy-fill" style="width: 100%;"></div>
          </div>
        </div>

        <!-- Turbo Bar -->
        <div class="bar-row">
          <div class="bar-title">Turbo</div>
          <div class="progress-track">
            <div class="progress-fill" id="turbo-fill" style="width: 0%;"></div>
          </div>
        </div>

        <!-- Speedometer -->
        <div class="speedo-row">
          <div class="bar-title">Velocity</div>
          <div class="speedo-marks" id="speed-ticks">
            <!-- Dynamically populated ticks -->
          </div>
          <div class="speed-digits" id="speed-digits">180 KM/H</div>
        </div>
      </div>
    </div>
  </div>

  <!-- Mobile Touch Controls -->
  <div id="mobile-controls">
    <div style="display: flex; gap: 15px;">
      <button class="dpad-btn" id="btn-left">◀</button>
      <button class="dpad-btn" id="btn-right">▶</button>
    </div>
    <div style="display: flex; gap: 15px;">
      <button class="dpad-btn" id="btn-pitch">▲</button>
      <button class="boost-touch-btn" id="btn-boost">🚀</button>
    </div>
  </div>

  <!-- Start Menu Modal -->
  <div class="modal-backdrop" id="start-menu">
    <div class="modal-card">
      <div class="game-title">Solar Velocity</div>
      <div class="game-subtitle">High-Speed Canyon Flight</div>
      <div class="instructions-list">
        <div>• <b>Steer:</b> <span class="key-badge">A</span><span class="key-badge">D</span> or <span class="key-badge">←</span><span class="key-badge">→</span> (Banking flight physics)</div>
        <div>• <b>Pitch:</b> <span class="key-badge">W</span><span class="key-badge">S</span> or <span class="key-badge">↑</span><span class="key-badge">↓</span> (Glide altitude control)</div>
        <div>• <b>Turbo Boost:</b> <span class="key-badge">Space</span> or <span class="key-badge">Shift</span></div>
        <div>• <b>Solar Energy:</b> Decays constantly — collect <span style="color:#ffea00">Amber Orbs</span> to recharge!</div>
        <div>• <b>Proximity:</b> Skim close to canyon walls for high <b>Near-Miss Multipliers</b>!</div>
      </div>
      <button class="action-btn" id="start-btn">Engage Engines</button>
    </div>
  </div>

  <!-- Game Over Modal -->
  <div class="modal-backdrop hidden" id="game-over-menu">
    <div class="modal-card">
      <div class="game-title" id="game-over-title" style="font-size: 32px;">Solar Core Depleted</div>
      <div class="game-subtitle" id="game-over-reason">Canyon Flight Terminated</div>

      <div class="score-summary">
        <div class="summary-item">
          <div class="label">Total Score</div>
          <div class="val highlight-val" id="final-score">0</div>
        </div>
        <div class="summary-item">
          <div class="label">Distance Flown</div>
          <div class="val" id="final-dist">0 m</div>
        </div>
        <div class="summary-item">
          <div class="label">Max Multiplier</div>
          <div class="val" id="final-multi">x1.0</div>
        </div>
        <div class="summary-item">
          <div class="label">Near Misses</div>
          <div class="val" id="final-nearmiss">0</div>
        </div>
      </div>

      <button class="action-btn" id="restart-btn">Fly Again (R)</button>
    </div>
  </div>

  <script>
    /* =========================================================================
       1. WEB AUDIO SYNTHESIS SYSTEM
       ========================================================================= */
    class AudioSynthesizer {
      constructor() {
        this.ctx = null;
        this.isMuted = false;
        this.engineOsc1 = null;
        this.engineOsc2 = null;
        this.engineFilter = null;
        this.engineGain = null;
        this.windGain = null;
        this.windFilter = null;
        this.initialized = false;
      }

      init() {
        if (this.initialized) return;
        try {
          const AudioContext = window.AudioContext || window.webkitAudioContext;
          this.ctx = new AudioContext();

          // Master Gain
          this.masterGain = this.ctx.createGain();
          this.masterGain.gain.setValueAtTime(0.7, this.ctx.currentTime);
          this.masterGain.connect(this.ctx.destination);

          // 1. Engine Drone (Dual saw/triangle + Lowpass)
          this.engineGain = this.ctx.createGain();
          this.engineGain.gain.setValueAtTime(0.001, this.ctx.currentTime);

          this.engineFilter = this.ctx.createBiquadFilter();
          this.engineFilter.type = 'lowpass';
          this.engineFilter.frequency.setValueAtTime(180, this.ctx.currentTime);
          this.engineFilter.Q.setValueAtTime(3.5, this.ctx.currentTime);

          this.engineOsc1 = this.ctx.createOscillator();
          this.engineOsc1.type = 'sawtooth';
          this.engineOsc1.frequency.setValueAtTime(55, this.ctx.currentTime); // A1

          this.engineOsc2 = this.ctx.createOscillator();
          this.engineOsc2.type = 'triangle';
          this.engineOsc2.frequency.setValueAtTime(110, this.ctx.currentTime); // A2

          this.engineOsc1.connect(this.engineFilter);
          this.engineOsc2.connect(this.engineFilter);
          this.engineFilter.connect(this.engineGain);
          this.engineGain.connect(this.masterGain);

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

          // 2. High-speed Wind Noise Generator
          const bufferSize = this.ctx.sampleRate * 2;
          const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
          const output = noiseBuffer.getChannelData(0);
          for (let i = 0; i < bufferSize; i++) {
            output[i] = Math.random() * 2 - 1;
          }

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

          this.windFilter = this.ctx.createBiquadFilter();
          this.windFilter.type = 'bandpass';
          this.windFilter.frequency.setValueAtTime(400, this.ctx.currentTime);
          this.windFilter.Q.setValueAtTime(1.2, this.ctx.currentTime);

          this.windGain = this.ctx.createGain();
          this.windGain.gain.setValueAtTime(0.02, this.ctx.currentTime);

          whiteNoise.connect(this.windFilter);
          this.windFilter.connect(this.windGain);
          this.windGain.connect(this.masterGain);
          whiteNoise.start();

          this.initialized = true;
        } catch (e) {
          console.warn('Web Audio API not supported or blocked:', e);
        }
      }

      updateEngine(speedRatio, isBoosting, proximityDist) {
        if (!this.initialized || !this.ctx || this.ctx.state !== 'running') return;
        const t = this.ctx.currentTime;

        // Base frequency scales with velocity
        const baseFreq = 50 + speedRatio * 90 + (isBoosting ? 65 : 0);
        this.engineOsc1.frequency.setTargetAtTime(baseFreq, t, 0.08);
        this.engineOsc2.frequency.setTargetAtTime(baseFreq * 2.01, t, 0.08);

        const filterFreq = 160 + speedRatio * 900 + (isBoosting ? 1200 : 0);
        this.engineFilter.frequency.setTargetAtTime(filterFreq, t, 0.06);

        const targetVol = 0.12 + speedRatio * 0.18 + (isBoosting ? 0.15 : 0);
        this.engineGain.gain.setTargetAtTime(targetVol, t, 0.05);

        // Wind noise modulation
        const windFreq = 200 + speedRatio * 1800 + (proximityDist < 12 ? 800 : 0);
        const windVol = 0.03 + speedRatio * 0.22 + (isBoosting ? 0.15 : 0);
        this.windFilter.frequency.setTargetAtTime(windFreq, t, 0.1);
        this.windGain.gain.setTargetAtTime(windVol, t, 0.1);
      }

      playOrbCollect(multiplier) {
        if (!this.initialized || !this.ctx) return;
        const t = this.ctx.currentTime;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();

        const basePitch = 523.25; // C5
        const scale = [1, 1.25, 1.333, 1.5, 1.875, 2.0];
        const pitchIdx = Math.min(Math.floor(multiplier), scale.length - 1);
        const freq = basePitch * scale[pitchIdx];

        osc.type = 'sine';
        osc.frequency.setValueAtTime(freq, t);
        osc.frequency.exponentialRampToValueAtTime(freq * 1.5, t + 0.18);

        gain.gain.setValueAtTime(0.3, t);
        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.35);

        osc.connect(gain);
        gain.connect(this.masterGain);

        osc.start(t);
        osc.stop(t + 0.35);
      }

      playNearMiss() {
        if (!this.initialized || !this.ctx) return;
        const t = this.ctx.currentTime;
        const osc = this.ctx.createOscillator();
        const filter = this.ctx.createBiquadFilter();
        const gain = this.ctx.createGain();

        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(220, t);
        osc.frequency.exponentialRampToValueAtTime(660, t + 0.12);
        osc.frequency.exponentialRampToValueAtTime(110, t + 0.3);

        filter.type = 'bandpass';
        filter.frequency.setValueAtTime(400, t);
        filter.frequency.exponentialRampToValueAtTime(2400, t + 0.12);
        filter.frequency.exponentialRampToValueAtTime(300, t + 0.3);
        filter.Q.setValueAtTime(5.0, t);

        gain.gain.setValueAtTime(0.25, t);
        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);

        osc.connect(filter);
        filter.connect(gain);
        gain.connect(this.masterGain);

        osc.start(t);
        osc.stop(t + 0.3);
      }

      playBoostIgnite() {
        if (!this.initialized || !this.ctx) return;
        const t = this.ctx.currentTime;
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();

        osc.type = 'triangle';
        osc.frequency.setValueAtTime(120, t);
        osc.frequency.exponentialRampToValueAtTime(480, t + 0.25);

        gain.gain.setValueAtTime(0.35, t);
        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);

        osc.connect(gain);
        gain.connect(this.masterGain);

        osc.start(t);
        osc.stop(t + 0.4);
      }

      playCrash() {
        if (!this.initialized || !this.ctx) return;
        const t = this.ctx.currentTime;

        // Low boom + noise burst
        const osc = this.ctx.createOscillator();
        const gain = this.ctx.createGain();
        osc.type = 'triangle';
        osc.frequency.setValueAtTime(130, t);
        osc.frequency.exponentialRampToValueAtTime(25, t + 0.8);

        gain.gain.setValueAtTime(0.6, t);
        gain.gain.exponentialRampToValueAtTime(0.001, t + 0.85);

        osc.connect(gain);
        gain.connect(this.masterGain);

        osc.start(t);
        osc.stop(t + 0.85);

        // Mute engine
        if (this.engineGain) {
          this.engineGain.gain.setTargetAtTime(0, t, 0.1);
        }
      }
    }

    const audio = new AudioSynthesizer();

    /* =========================================================================
       2. THREE.JS SCENE SETUP & CYBER SUNSET AESTHETICS
       ========================================================================= */
    const container = document.getElementById('canvas-container');
    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x190826, 0.0065); // Cyber Sunset purple/burgundy fog

    const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 900);
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: "high-performance" });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.15;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    container.appendChild(renderer.domElement);

    // Speed Lines Canvas 2D
    const speedLinesCanvas = document.getElementById('speed-lines-canvas');
    const speedLinesCtx = speedLinesCanvas.getContext('2d');
    const speedVignette = document.getElementById('speed-vignette');

    function resizeSpeedLines() {
      speedLinesCanvas.width = window.innerWidth;
      speedLinesCanvas.height = window.innerHeight;
    }
    resizeSpeedLines();

    // Lighting setup for dramatic golden hour / cyber sunset long shadows
    const ambientLight = new THREE.AmbientLight(0x421c59, 1.4);
    scene.add(ambientLight);

    const hemiLight = new THREE.HemisphereLight(0xff7b29, 0x1f0b3b, 1.2);
    scene.add(hemiLight);

    const sunLight = new THREE.DirectionalLight(0xffa238, 2.8);
    sunLight.position.set(0, 45, -350);
    sunLight.castShadow = true;
    sunLight.shadow.mapSize.width = 2048;
    sunLight.shadow.mapSize.height = 2048;
    sunLight.shadow.camera.near = 10;
    sunLight.shadow.camera.far = 600;
    sunLight.shadow.camera.left = -120;
    sunLight.shadow.camera.right = 120;
    sunLight.shadow.camera.top = 100;
    sunLight.shadow.camera.bottom = -40;
    sunLight.shadow.bias = -0.0005;
    scene.add(sunLight);

    // Glowing Giant Sun Disk on Horizon
    const sunGeo = new THREE.CircleGeometry(160, 48);
    const sunMat = new THREE.MeshBasicMaterial({
      color: 0xff4b1f,
      side: THREE.DoubleSide
    });
    const sunMesh = new THREE.Mesh(sunGeo, sunMat);
    sunMesh.position.set(0, 45, -750);
    scene.add(sunMesh);

    // Sun Inner Core
    const sunInnerGeo = new THREE.CircleGeometry(130, 48);
    const sunInnerMat = new THREE.MeshBasicMaterial({
      color: 0xffd200,
      side: THREE.DoubleSide
    });
    const sunInner = new THREE.Mesh(sunInnerGeo, sunInnerMat);
    sunInner.position.set(0, 45, -748);
    scene.add(sunInner);

    // Horizon Grid / Mountain Backdrops
    const bgMountainsGroup = new THREE.Group();
    const mountainMat = new THREE.MeshLambertMaterial({
      color: 0x240e3b
    });

    for (let i = -12; i <= 12; i++) {
      const h = 40 + Math.random() * 70;
      const w = 60 + Math.random() * 50;
      const mGeo = new THREE.ConeGeometry(w, h, 4);
      const mMesh = new THREE.Mesh(mGeo, mountainMat);
      mMesh.position.set(i * 50 + (Math.random() - 0.5) * 20, h * 0.4 - 10, -680 + (Math.random() - 0.5) * 60);
      mMesh.rotation.y = Math.random() * Math.PI;
      bgMountainsGroup.add(mMesh);
    }
    scene.add(bgMountainsGroup);

    /* =========================================================================
       3. PLAYER CRAFT MODEL & WING DYNAMICS
       ========================================================================= */
    const playerGroup = new THREE.Group();
    scene.add(playerGroup);

    // Sleek geometric low-poly interceptor craft
    const shipBodyGroup = new THREE.Group();
    playerGroup.add(shipBodyGroup);

    // Main fuselage (sharp wedge)
    const fuselageGeo = new THREE.BufferGeometry();
    const vertices = new Float32Array([
      // Top central nose to wingroots
      0, 0.4, -2.4,   -1.4, 0.1, 1.2,    1.4, 0.1, 1.2,
      0, 0.4, -2.4,    1.4, 0.1, 1.2,    0, 0.6, 1.0,
      0, 0.4, -2.4,    0, 0.6, 1.0,     -1.4, 0.1, 1.2,

      // Cockpit / Canopy
      0, 0.7, -0.6,   -0.5, 0.35, 0.4,   0.5, 0.35, 0.4,
      0, 0.7, -0.6,    0, 0.3, -1.5,    -0.5, 0.35, 0.4,
      0, 0.7, -0.6,    0.5, 0.35, 0.4,   0, 0.3, -1.5,

      // Wings Left / Right (swept forward-angled poly)
      -1.4, 0.1, 1.2,  -3.2, 0.2, 0.4,  -2.2, -0.05, 1.6,
       1.4, 0.1, 1.2,   2.2, -0.05, 1.6, 3.2, 0.2, 0.4,
      -1.4, 0.1, 1.2,   0, 0.6, 1.0,    -2.2, -0.05, 1.6,
       1.4, 0.1, 1.2,   2.2, -0.05, 1.6, 0, 0.6, 1.0,

      // Bottom Belly
      0, -0.3, -2.2,    1.4, 0.1, 1.2,  -1.4, 0.1, 1.2,
      0, -0.3, -2.2,   -1.4, 0.1, 1.2,   0, -0.2, 1.4,
      0, -0.3, -2.2,    0, -0.2, 1.4,    1.4, 0.1, 1.2,
    ]);
    fuselageGeo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
    fuselageGeo.computeVertexNormals();

    const shipMaterial = new THREE.MeshStandardMaterial({
      color: 0xe8e8f2,
      roughness: 0.25,
      metalness: 0.85,
      flatShading: true
    });
    const shipMesh = new THREE.Mesh(fuselageGeo, shipMaterial);
    shipMesh.castShadow = true;
    shipMesh.receiveShadow = true;
    shipBodyGroup.add(shipMesh);

    // Dark trim / cockpit canopy
    const canopyGeo = new THREE.ConeGeometry(0.55, 1.6, 4);
    canopyGeo.rotateX(Math.PI / 2);
    const canopyMat = new THREE.MeshStandardMaterial({
      color: 0x05d9e8,
      roughness: 0.1,
      metalness: 0.9,
      emissive: 0x012b33,
      flatShading: true
    });
    const canopyMesh = new THREE.Mesh(canopyGeo, canopyMat);
    canopyMesh.position.set(0, 0.4, -0.1);
    canopyMesh.scale.set(0.8, 0.5, 1.1);
    shipBodyGroup.add(canopyMesh);

    // Glowing Thruster Engine Cores
    const thrusterGeo = new THREE.CylinderGeometry(0.2, 0.32, 0.5, 6);
    thrusterGeo.rotateX(Math.PI / 2);
    const thrusterMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });

    const thrusterL = new THREE.Mesh(thrusterGeo, thrusterMat);
    thrusterL.position.set(-0.6, 0.15, 1.4);
    shipBodyGroup.add(thrusterL);

    const thrusterR = new THREE.Mesh(thrusterGeo, thrusterMat);
    thrusterR.position.set(0.6, 0.15, 1.4);
    shipBodyGroup.add(thrusterR);

    // Thruster Jet Glow Sprites/Plumes
    const plumeGeo = new THREE.ConeGeometry(0.22, 1.8, 6);
    plumeGeo.rotateX(-Math.PI / 2);
    const plumeMat = new THREE.MeshBasicMaterial({
      color: 0xffb700,
      transparent: true,
      opacity: 0.85
    });
    const plumeL = new THREE.Mesh(plumeGeo, plumeMat);
    plumeL.position.set(-0.6, 0.15, 2.3);
    shipBodyGroup.add(plumeL);

    const plumeR = new THREE.Mesh(plumeGeo, plumeMat);
    plumeR.position.set(0.6, 0.15, 2.3);
    shipBodyGroup.add(plumeR);

    // Shadow blob under craft
    const shadowGeo = new THREE.CircleGeometry(1.6, 16);
    shadowGeo.rotateX(-Math.PI / 2);
    const shadowMat = new THREE.MeshBasicMaterial({
      color: 0x000000,
      transparent: true,
      opacity: 0.4
    });
    const playerShadow = new THREE.Mesh(shadowGeo, shadowMat);
    scene.add(playerShadow);

    /* =========================================================================
       4. PROCEDURAL CHUNK & CANYON GENERATION SYSTEM
       ========================================================================= */
    const CHUNK_LENGTH = 180;
    const TOTAL_CHUNKS = 9;
    const CANYON_WIDTH_BASE = 48;

    // Reusable Materials for Low-Poly Canyon
    const canyonMaterial = new THREE.MeshStandardMaterial({
      color: 0x5a2347, // Sunset canyon burgundy/terracotta
      roughness: 0.85,
      metalness: 0.15,
      flatShading: true
    });

    const canyonFloorMaterial = new THREE.MeshStandardMaterial({
      color: 0x2a1030,
      roughness: 0.9,
      metalness: 0.1,
      flatShading: true
    });

    const monolithMaterial = new THREE.MeshStandardMaterial({
      color: 0x8b3a4a,
      roughness: 0.5,
      metalness: 0.4,
      flatShading: true
    });

    const archMaterial = new THREE.MeshStandardMaterial({
      color: 0xd95a3f,
      roughness: 0.6,
      metalness: 0.3,
      flatShading: true
    });

    // Glowing Energy Orb Material & Geometry
    const orbCoreGeo = new THREE.OctahedronGeometry(1.2, 0);
    const orbCoreMat = new THREE.MeshBasicMaterial({ color: 0xffea00 });
    const orbRingGeo = new THREE.TorusGeometry(1.8, 0.15, 6, 16);
    const orbRingMat = new THREE.MeshBasicMaterial({ color: 0xff5e00, wireframe: true });

    class CanyonChunk {
      constructor(index) {
        this.group = new THREE.Group();
        this.obstacles = []; // Array of { collider: Box3/Sphere, mesh, type }
        this.orbs = [];      // Array of { mesh, collected, ring }
        this.index = index;
        this.startZ = 0;
        this.endZ = 0;
        scene.add(this.group);
      }

      generate(startZ, difficultyFactor, trackCurvature) {
        this.startZ = startZ;
        this.endZ = startZ - CHUNK_LENGTH;
        this.group.position.set(0, 0, 0);
        this.clear();

        const segmentsZ = 12;
        const segmentStep = CHUNK_LENGTH / segmentsZ;
        const width = Math.max(26, CANYON_WIDTH_BASE - difficultyFactor * 8);

        // 1. Canyon Floor & Wall Geometry
        const floorGeo = new THREE.PlaneGeometry(width * 2.2, CHUNK_LENGTH, 8, segmentsZ);
        floorGeo.rotateX(-Math.PI / 2);

        // Perturb floor vertices for terrain feel
        const pos = floorGeo.attributes.position;
        for (let i = 0; i < pos.count; i++) {
          const zRel = pos.getZ(i);
          const globalZ = startZ - (zRel + CHUNK_LENGTH / 2);
          const curveOffset = Math.sin(globalZ * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
          pos.setX(i, pos.getX(i) + curveOffset);
          pos.setY(i, (Math.random() - 0.5) * 1.5);
        }
        floorGeo.computeVertexNormals();

        const floorMesh = new THREE.Mesh(floorGeo, canyonFloorMaterial);
        floorMesh.position.set(0, 0, startZ - CHUNK_LENGTH / 2);
        floorMesh.receiveShadow = true;
        this.group.add(floorMesh);

        // 2. Left and Right Canyon Walls
        for (let side of [-1, 1]) {
          const wallGeo = new THREE.BufferGeometry();
          const wallVerts = [];
          const wallHeight = 45 + Math.random() * 15;
          const wallSteps = segmentsZ;

          for (let s = 0; s < wallSteps; s++) {
            const z1 = -s * segmentStep;
            const z2 = -(s + 1) * segmentStep;
            const globalZ1 = startZ + z1;
            const globalZ2 = startZ + z2;

            const curve1 = Math.sin(globalZ1 * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
            const curve2 = Math.sin(globalZ2 * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);

            const xBase1 = curve1 + side * (width * 0.5);
            const xBase2 = curve2 + side * (width * 0.5);
            const xTop1 = curve1 + side * (width * 0.5 + 18 + Math.sin(s) * 6);
            const xTop2 = curve2 + side * (width * 0.5 + 18 + Math.sin(s + 1) * 6);

            // 2 triangles per wall segment
            wallVerts.push(xBase1, 0, z1);
            wallVerts.push(xTop1, wallHeight, z1);
            wallVerts.push(xBase2, 0, z2);

            wallVerts.push(xBase2, 0, z2);
            wallVerts.push(xTop1, wallHeight, z1);
            wallVerts.push(xTop2, wallHeight, z2);
          }

          wallGeo.setAttribute('position', new THREE.Float32BufferAttribute(wallVerts, 3));
          wallGeo.computeVertexNormals();

          const wallMesh = new THREE.Mesh(wallGeo, canyonMaterial);
          wallMesh.position.set(0, 0, startZ);
          wallMesh.castShadow = true;
          wallMesh.receiveShadow = true;
          this.group.add(wallMesh);

          // Wall collision proxy boxes along the sides
          for (let s = 0; s < wallSteps; s += 2) {
            const zCenter = startZ - s * segmentStep - segmentStep;
            const curve = Math.sin(zCenter * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);
            const xWallCenter = curve + side * (width * 0.5 + 8);
            
            const wallCollider = new THREE.Box3(
              new THREE.Vector3(side === -1 ? xWallCenter - 25 : xWallCenter - 5, -2, zCenter - segmentStep),
              new THREE.Vector3(side === -1 ? xWallCenter + 5 : xWallCenter + 25, wallHeight, zCenter + segmentStep)
            );
            this.obstacles.push({
              box: wallCollider,
              isWall: true,
              center: new THREE.Vector3(xWallCenter, wallHeight * 0.5, zCenter)
            });
          }
        }

        // 3. Obstacle Types in Chunk:
        // A) Floating Monoliths, B) Tight Squeeze Arches, C) Needle Spikes
        const obstacleCount = 2 + Math.floor(difficultyFactor * 3.5);
        for (let o = 0; o < obstacleCount; o++) {
          const zLocal = -15 - Math.random() * (CHUNK_LENGTH - 30);
          const zGlobal = startZ + zLocal;
          const curve = Math.sin(zGlobal * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);

          const rType = Math.random();

          if (rType < 0.45) {
            // Monolith Pillar (requires banking left/right)
            const xOffset = (Math.random() - 0.5) * (width * 0.55);
            const pHeight = 25 + Math.random() * 25;
            const pRadius = 2.4 + Math.random() * 2.2;
            const pillarGeo = new THREE.CylinderGeometry(pRadius * 0.6, pRadius, pHeight, 5);
            const pillar = new THREE.Mesh(pillarGeo, monolithMaterial);
            pillar.position.set(curve + xOffset, pHeight * 0.5, zGlobal);
            pillar.rotation.y = Math.random() * Math.PI;
            pillar.rotation.z = (Math.random() - 0.5) * 0.25;
            pillar.castShadow = true;
            pillar.receiveShadow = true;
            this.group.add(pillar);

            const box = new THREE.Box3().setFromObject(pillar);
            this.obstacles.push({
              box: box,
              mesh: pillar,
              isWall: false,
              center: pillar.position.clone()
            });

          } else if (rType < 0.75) {
            // Tight Squeeze Overhang Arch (requires precision altitude / roll)
            const archGroup = new THREE.Group();
            const archHeight = 8 + Math.random() * 8;
            const archW = 12 + Math.random() * 8;

            const topBarGeo = new THREE.BoxGeometry(archW, 4, 5);
            const topBar = new THREE.Mesh(topBarGeo, archMaterial);
            topBar.position.set(0, archHeight + 3, 0);
            archGroup.add(topBar);

            const pillarGeoL = new THREE.BoxGeometry(3.5, archHeight + 6, 4);
            const pL = new THREE.Mesh(pillarGeoL, archMaterial);
            pL.position.set(-archW * 0.5, (archHeight + 6) * 0.5, 0);
            archGroup.add(pL);

            const pR = new THREE.Mesh(pillarGeoL, archMaterial);
            pR.position.set(archW * 0.5, (archHeight + 6) * 0.5, 0);
            archGroup.add(pR);

            archGroup.position.set(curve + (Math.random() - 0.5) * 6, 0, zGlobal);
            archGroup.rotation.y = (Math.random() - 0.5) * 0.4;
            archGroup.traverse(c => { if (c.isMesh) { c.castShadow = true; c.receiveShadow = true; } });
            this.group.add(archGroup);

            // Add colliders for top bar and side pillars
            const topBox = new THREE.Box3().setFromObject(topBar);
            const lBox = new THREE.Box3().setFromObject(pL);
            const rBox = new THREE.Box3().setFromObject(pR);

            this.obstacles.push({ box: topBox, isWall: false, center: topBar.position.clone().add(archGroup.position) });
            this.obstacles.push({ box: lBox, isWall: false, center: pL.position.clone().add(archGroup.position) });
            this.obstacles.push({ box: rBox, isWall: false, center: pR.position.clone().add(archGroup.position) });

          } else {
            // Slanted Needle Spires
            const spireH = 30 + Math.random() * 20;
            const spireGeo = new THREE.ConeGeometry(3, spireH, 4);
            const spire = new THREE.Mesh(spireGeo, monolithMaterial);
            const slantSide = Math.random() > 0.5 ? 1 : -1;
            const xPos = curve + slantSide * (width * 0.28);
            spire.position.set(xPos, spireH * 0.4, zGlobal);
            spire.rotation.z = -slantSide * (0.35 + Math.random() * 0.3);
            spire.castShadow = true;
            spire.receiveShadow = true;
            this.group.add(spire);

            const box = new THREE.Box3().setFromObject(spire);
            this.obstacles.push({ box: box, mesh: spire, isWall: false, center: spire.position.clone() });
          }
        }

        // 4. Energy Orbs Generation (Placed strategically in gaps & flight paths)
        const orbCount = 3 + Math.floor(Math.random() * 3);
        for (let i = 0; i < orbCount; i++) {
          const zLocal = -10 - (i / orbCount) * (CHUNK_LENGTH - 20) + (Math.random() - 0.5) * 8;
          const zGlobal = startZ + zLocal;
          const curve = Math.sin(zGlobal * 0.015 + trackCurvature) * (14 + difficultyFactor * 8);

          const orbGroup = new THREE.Group();
          const core = new THREE.Mesh(orbCoreGeo, orbCoreMat);
          const ring = new THREE.Mesh(orbRingGeo, orbRingMat);
          orbGroup.add(core);
          orbGroup.add(ring);

          const orbY = 2.5 + Math.random() * 8.5;
          const orbX = curve + (Math.random() - 0.5) * (width * 0.6);
          orbGroup.position.set(orbX, orbY, zGlobal);

          this.group.add(orbGroup);
          this.orbs.push({
            mesh: orbGroup,
            core: core,
            ring: ring,
            pos: orbGroup.position,
            collected: false,
            radius: 2.2
          });
        }
      }

      clear() {
        while (this.group.children.length > 0) {
          const obj = this.group.children[0];
          this.group.remove(obj);
          if (obj.geometry) obj.geometry.dispose();
        }
        this.obstacles = [];
        this.orbs = [];
      }
    }

    // Initialize Chunks
    const chunks = [];
    let farthestZ = 0;
    let trackCurvatureSeed = 0.5;

    function initChunks() {
      farthestZ = 20;
      for (let i = 0; i < TOTAL_CHUNKS; i++) {
        const chunk = new CanyonChunk(i);
        chunk.generate(farthestZ, 0.1, trackCurvatureSeed);
        chunks.push(chunk);
        farthestZ -= CHUNK_LENGTH;
      }
    }

    function updateChunks(playerZ, difficulty) {
      for (let i = 0; i < chunks.length; i++) {
        const chunk = chunks[i];
        // If chunk is behind player by more than 1 chunk length, recycle to front
        if (chunk.startZ > playerZ + CHUNK_LENGTH) {
          chunk.generate(farthestZ, difficulty, trackCurvatureSeed);
          farthestZ -= CHUNK_LENGTH;
        }
      }
    }

    /* =========================================================================
       5. PARTICLE SYSTEMS (Trail Particles & Speed Dust & Crash Debris)
       ========================================================================= */
    // Dust Particles Flying Past Craft
    const DUST_COUNT = 350;
    const dustGeo = new THREE.BufferGeometry();
    const dustPositions = new Float32Array(DUST_COUNT * 3);
    const dustColors = new Float32Array(DUST_COUNT * 3);

    for (let i = 0; i < DUST_COUNT; i++) {
      dustPositions[i * 3] = (Math.random() - 0.5) * 80;
      dustPositions[i * 3 + 1] = Math.random() * 30;
      dustPositions[i * 3 + 2] = (Math.random() - 0.5) * 200;

      dustColors[i * 3] = 1.0;
      dustColors[i * 3 + 1] = 0.6 + Math.random() * 0.4;
      dustColors[i * 3 + 2] = 0.2 + Math.random() * 0.6;
    }
    dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPositions, 3));
    dustGeo.setAttribute('color', new THREE.BufferAttribute(dustColors, 3));

    const dustMat = new THREE.PointsMaterial({
      size: 0.65,
      vertexColors: true,
      transparent: true,
      opacity: 0.75,
      blending: THREE.AdditiveBlending
    });
    const dustParticles = new THREE.Points(dustGeo, dustMat);
    scene.add(dustParticles);

    // Trail ribbon particle ring buffer
    const TRAIL_MAX = 60;
    const trailPositionsL = [];
    const trailPositionsR = [];
    for (let i = 0; i < TRAIL_MAX; i++) {
      trailPositionsL.push(new THREE.Vector3());
      trailPositionsR.push(new THREE.Vector3());
    }

    const trailGeoL = new THREE.BufferGeometry().setFromPoints(trailPositionsL);
    const trailGeoR = new THREE.BufferGeometry().setFromPoints(trailPositionsR);
    const trailMat = new THREE.LineBasicMaterial({
      color: 0x00f0ff,
      linewidth: 2,
      transparent: true,
      opacity: 0.8
    });
    const trailLineL = new THREE.Line(trailGeoL, trailMat);
    const trailLineR = new THREE.Line(trailGeoR, trailMat);
    scene.add(trailLineL);
    scene.add(trailLineR);

    // Crash Debris Fracture System
    const debrisGroup = new THREE.Group();
    scene.add(debrisGroup);
    const debrisList = [];

    function triggerCrashExplosion(position) {
      const fragCount = 35;
      const shardGeo = new THREE.TetrahedronGeometry(0.7, 0);
      const shardMat = new THREE.MeshBasicMaterial({ color: 0xff7700 });

      for (let i = 0; i < fragCount; i++) {
        const mesh = new THREE.Mesh(shardGeo, shardMat);
        mesh.position.copy(position);
        mesh.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);

        const velocity = new THREE.Vector3(
          (Math.random() - 0.5) * 35,
          Math.random() * 25 + 5,
          (Math.random() - 0.5) * 35 - 15
        );

        debrisGroup.add(mesh);
        debrisList.push({
          mesh: mesh,
          vel: velocity,
          rotVel: new THREE.Vector3(Math.random() * 10, Math.random() * 10, Math.random() * 10),
          life: 1.0
        });
      }
    }

    /* =========================================================================
       6. GAME STATE & FLIGHT PHYSICS ENGINE
       ========================================================================= */
    const STATE_MENU = 0;
    const STATE_PLAYING = 1;
    const STATE_GAMEOVER = 2;

    const game = {
      state: STATE_MENU,
      score: 0,
      distance: 0,
      multiplier: 1.0,
      multiplierTimer: 0,
      maxMultiplier: 1.0,
      nearMissCount: 0,

      // Energy & Turbo
      energy: 100,
      maxEnergy: 100,
      energyDecayRate: 6.8, // % per second
      turboCharge: 0,
      isBoosting: false,

      // Physics State
      pos: new THREE.Vector3(0, 3.5, 0),
      vel: new THREE.Vector3(0, 0, 0),
      baseSpeed: 75,
      currentSpeed: 75,
      maxSpeed: 75,
      boostSpeedBonus: 55,

      // Rotation & Flight Dynamics
      pitch: 0,
      roll: 0,
      yaw: 0,
      targetRoll: 0,
      targetPitch: 0,

      // Camera Controls
      camOffset: new THREE.Vector3(0, 3.8, 8.5),
      camLookOffset: new THREE.Vector3(0, 1.2, -18),
      camShake: 0,

      // Difficulty
      difficulty: 0.1
    };

    // Keyboard & Input Listener
    const keys = {
      left: false,
      right: false,
      up: false,
      down: false,
      boost: false
    };

    window.addEventListener('keydown', (e) => {
      if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
      if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
      if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = true;
      if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = true;
      if (e.code === 'Space' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') {
        keys.boost = true;
        if (game.state === STATE_PLAYING && game.turboCharge >= 20 && !game.isBoosting) {
          audio.playBoostIgnite();
        }
      }
      if (e.code === 'KeyR' && game.state === STATE_GAMEOVER) {
        restartGame();
      }
    });

    window.addEventListener('keyup', (e) => {
      if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
      if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
      if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = false;
      if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = false;
      if (e.code === 'Space' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') keys.boost = false;
    });

    // Touch Controls setup
    function setupTouchControls() {
      const btnL = document.getElementById('btn-left');
      const btnR = document.getElementById('btn-right');
      const btnP = document.getElementById('btn-pitch');
      const btnB = document.getElementById('btn-boost');

      const bindTouch = (elem, pressFn, releaseFn) => {
        elem.addEventListener('touchstart', (e) => { e.preventDefault(); pressFn(); }, { passive: false });
        elem.addEventListener('touchend', (e) => { e.preventDefault(); releaseFn(); }, { passive: false });
      };

      bindTouch(btnL, () => keys.left = true, () => keys.left = false);
      bindTouch(btnR, () => keys.right = true, () => keys.right = false);
      bindTouch(btnP, () => keys.up = true, () => keys.up = false);
      bindTouch(btnB, () => keys.boost = true, () => keys.boost = false);
    }
    setupTouchControls();

    // Speedometer UI Ticks Generation
    const speedTicksContainer = document.getElementById('speed-ticks');
    const TOTAL_TICKS = 24;
    for (let i = 0; i < TOTAL_TICKS; i++) {
      const tick = document.createElement('div');
      tick.className = 'speed-tick';
      speedTicksContainer.appendChild(tick);
    }
    const tickElements = document.querySelectorAll('.speed-tick');

    /* =========================================================================
       7. CORE SIMULATION LOOP & FLIGHT MECHANICS
       ========================================================================= */
    let lastTime = performance.now();

    function updateFlightPhysics(dt) {
      // 1. Difficulty Scaling based on score & distance
      game.difficulty = Math.min(1.0, 0.1 + (game.distance / 4500) * 0.9);
      game.baseSpeed = 75 + game.difficulty * 45;

      // 2. Turbo Boost Triggering & Consumption
      if (keys.boost && game.turboCharge > 2) {
        game.isBoosting = true;
        game.turboCharge = Math.max(0, game.turboCharge - dt * 28);
      } else {
        game.isBoosting = false;
        // Passive slow turbo build on straight flying
        if (Math.abs(game.roll) < 0.15 && game.state === STATE_PLAYING) {
          game.turboCharge = Math.min(100, game.turboCharge + dt * 4);
        }
      }

      // 3. Target Speed & Acceleration
      const targetSpeed = game.baseSpeed + (game.isBoosting ? game.boostSpeedBonus : 0);
      game.currentSpeed += (targetSpeed - game.currentSpeed) * Math.min(1.0, dt * 4.5);
      if (game.currentSpeed > game.maxSpeed) game.maxSpeed = game.currentSpeed;

      // 4. Banking (Roll) & Turning Mechanics
      const rollSpeed = 3.2;
      const returnSpeed = 4.5;
      const maxRoll = 1.15; // ~66 degrees banking

      if (keys.left) {
        game.targetRoll = Math.min(maxRoll, game.targetRoll + dt * rollSpeed);
      } else if (keys.right) {
        game.targetRoll = Math.max(-maxRoll, game.targetRoll - dt * rollSpeed);
      } else {
        game.targetRoll += (0 - game.targetRoll) * Math.min(1.0, dt * returnSpeed);
      }

      // Smooth Roll Interpolation
      game.roll += (game.targetRoll - game.roll) * Math.min(1.0, dt * 10);

      // Pitch (Dive / Climb) Controls
      const pitchSpeed = 2.2;
      if (keys.up) {
        game.targetPitch = Math.min(0.6, game.targetPitch + dt * pitchSpeed); // Climb
      } else if (keys.down) {
        game.targetPitch = Math.max(-0.6, game.targetPitch - dt * pitchSpeed); // Dive
      } else {
        game.targetPitch += (0 - game.targetPitch) * Math.min(1.0, dt * 6.0);
      }
      game.pitch += (game.targetPitch - game.pitch) * Math.min(1.0, dt * 8);

      // 5. Calculate Flight Momentum Velocities
      // Lateral velocity is driven directly by the bank angle (Flight physics feel)
      const lateralForce = -Math.sin(game.roll) * 38;
      game.vel.x += (lateralForce - game.vel.x) * Math.min(1.0, dt * 6);

      // Vertical climb / dive
      const verticalForce = Math.sin(game.pitch) * 22;
      game.vel.y += (verticalForce - game.vel.y) * Math.min(1.0, dt * 6);

      // Forward velocity along -Z
      game.vel.z = -game.currentSpeed;

      // Apply displacement
      game.pos.x += game.vel.x * dt;
      game.pos.y += game.vel.y * dt;
      game.pos.z += game.vel.z * dt;

      // Flight boundary clamps
      game.pos.y = Math.max(1.2, Math.min(26.0, game.pos.y));

      // Update Craft 3D Group Transform
      playerGroup.position.copy(game.pos);
      shipBodyGroup.rotation.z = game.roll;
      shipBodyGroup.rotation.x = game.pitch * 0.7;
      shipBodyGroup.rotation.y = -game.roll * 0.25;

      // Dynamic wingtip plume stretch & colors
      const plumeScale = 0.8 + (game.currentSpeed / game.baseSpeed) * 0.6 + (game.isBoosting ? 0.8 : 0);
      plumeL.scale.set(1, plumeScale, 1);
      plumeR.scale.set(1, plumeScale, 1);
      plumeMat.color.setHex(game.isBoosting ? 0x00f0ff : 0xffaa00);

      // Shadow projection directly on ground
      playerShadow.position.set(game.pos.x, 0.08, game.pos.z);
      const heightRatio = Math.max(0.1, 1 - (game.pos.y / 25));
      playerShadow.scale.setScalar(heightRatio * 1.3);
      shadowMat.opacity = heightRatio * 0.45;

      // 6. Camera Follow Physics with Smooth Damping & Dynamic FOV
      const targetCamX = game.pos.x + (-game.roll * 1.8);
      const targetCamY = game.pos.y + game.camOffset.y + (game.pitch * 1.5);
      const targetCamZ = game.pos.z + game.camOffset.z + (game.isBoosting ? 1.5 : 0);

      camera.position.x += (targetCamX - camera.position.x) * Math.min(1.0, dt * 10);
      camera.position.y += (targetCamY - camera.position.y) * Math.min(1.0, dt * 10);
      camera.position.z += (targetCamZ - camera.position.z) * Math.min(1.0, dt * 12);

      // Camera Shake
      if (game.camShake > 0) {
        camera.position.x += (Math.random() - 0.5) * game.camShake;
        camera.position.y += (Math.random() - 0.5) * game.camShake;
        game.camShake = Math.max(0, game.camShake - dt * 3.5);
      }

      const lookTarget = new THREE.Vector3(
        game.pos.x + game.camLookOffset.x - (game.roll * 4.0),
        game.pos.y + game.camLookOffset.y,
        game.pos.z + game.camLookOffset.z
      );
      camera.lookAt(lookTarget);

      // Camera FOV Expansion during high speed / boost
      const targetFov = game.isBoosting ? 82 : (65 + (game.currentSpeed - 75) * 0.15);
      camera.fov += (targetFov - camera.fov) * Math.min(1.0, dt * 5);
      camera.updateProjectionMatrix();

      // Dynamic Sun follows player along Z for infinite sunset
      sunMesh.position.z = game.pos.z - 750;
      sunInner.position.z = game.pos.z - 748;
      sunLight.position.z = game.pos.z - 350;
      sunLight.target.position.set(game.pos.x, 0, game.pos.z);
      sunLight.target.updateMatrixWorld();
      bgMountainsGroup.position.z = game.pos.z * 0.85; // Parallax
    }

    /* =========================================================================
       8. PROXIMITY SCORING (NEAR-MISS) & COLLISION CHECKS
       ========================================================================= */
    const playerSphere = new THREE.Sphere(new THREE.Vector3(), 1.3);
    const nearMissSphere = new THREE.Sphere(new THREE.Vector3(), 4.8);
    const nearMissBanner = document.getElementById('near-miss-banner');
    let bannerTimeout = null;

    function triggerNearMissEffect(bonusScore) {
      game.nearMissCount++;
      game.multiplier = Math.min(10.0, game.multiplier + 0.5);
      if (game.multiplier > game.maxMultiplier) game.maxMultiplier = game.multiplier;
      game.multiplierTimer = 4.0; // Multiplier decay reset

      game.score += Math.round(bonusScore * game.multiplier);
      game.camShake = Math.min(0.6, game.camShake + 0.25);
      game.turboCharge = Math.min(100, game.turboCharge + 12);

      audio.playNearMiss();

      // UI Punch
      const multiBox = document.getElementById('multiplier-box');
      multiBox.classList.add('multiplier-pulse');
      setTimeout(() => multiBox.classList.remove('multiplier-pulse'), 200);

      nearMissBanner.textContent = `⚡ NEAR MISS +${Math.round(bonusScore * game.multiplier)} ⚡`;
      nearMissBanner.classList.add('active');
      clearTimeout(bannerTimeout);
      bannerTimeout = setTimeout(() => nearMissBanner.classList.remove('active'), 800);
    }

    function checkCollisionsAndNearMisses(dt) {
      playerSphere.center.copy(game.pos);
      nearMissSphere.center.copy(game.pos);

      let closestObstacleDist = 999;

      for (let c = 0; c < chunks.length; c++) {
        const chunk = chunks[c];
        // Only test chunks near the player
        if (chunk.startZ < game.pos.z - CHUNK_LENGTH || chunk.endZ > game.pos.z + 40) continue;

        // 1. Check Obstacle Collisions & Near-Misses
        for (let i = 0; i < chunk.obstacles.length; i++) {
          const obs = chunk.obstacles[i];
          const distToCenter = obs.center.distanceTo(game.pos);

          if (distToCenter < closestObstacleDist) {
            closestObstacleDist = distToCenter;
          }

          // Exact Box vs Player Sphere Collision Check
          if (obs.box.intersectsSphere(playerSphere)) {
            // CRASH!
            gameOver("COLLISION DETECTED");
            return;
          }

          // Near Miss Check
          if (!obs.nearMissed && obs.box.intersectsSphere(nearMissSphere)) {
            obs.nearMissed = true;
            triggerNearMissEffect(250);
          }
        }

        // 2. Check Energy Orb Collectibles
        for (let i = 0; i < chunk.orbs.length; i++) {
          const orb = chunk.orbs[i];
          if (orb.collected) continue;

          // Rotate orb meshes
          orb.core.rotation.y += dt * 3;
          orb.core.rotation.x += dt * 2;
          orb.ring.rotation.z += dt * 4;

          const dist = orb.pos.distanceTo(game.pos);
          if (dist < orb.radius + 1.4) {
            // Collected!
            orb.collected = true;
            orb.mesh.visible = false;
            game.energy = Math.min(game.maxEnergy, game.energy + 28);
            game.turboCharge = Math.min(100, game.turboCharge + 18);
            game.score += Math.round(150 * game.multiplier);

            audio.playOrbCollect(game.multiplier);

            // Floating feedback
            game.camShake = Math.min(0.35, game.camShake + 0.15);
          }
        }
      }

      // Audio update
      const speedRatio = (game.currentSpeed - 70) / (game.baseSpeed + game.boostSpeedBonus - 70);
      audio.updateEngine(Math.max(0, Math.min(1.5, speedRatio)), game.isBoosting, closestObstacleDist);
    }

    /* =========================================================================
       9. GAME LOOP, SCORE TRACKING & UI UPDATES
       ========================================================================= */
    const scoreDisplay = document.getElementById('score-display');
    const distDisplay = document.getElementById('dist-display');
    const multiplierDisplay = document.getElementById('multiplier-display');
    const energyFill = document.getElementById('energy-fill');
    const turboFill = document.getElementById('turbo-fill');
    const speedDigits = document.getElementById('speed-digits');

    function updateParticlesAndTrails(dt) {
      // 1. Dust stream
      const posAttr = dustGeo.attributes.position;
      for (let i = 0; i < DUST_COUNT; i++) {
        let pz = posAttr.getZ(i);
        pz += (game.currentSpeed * 1.5) * dt;
        if (pz > 20) {
          pz = -180 - Math.random() * 50;
          posAttr.setX(i, game.pos.x + (Math.random() - 0.5) * 80);
          posAttr.setY(i, Math.random() * 30);
        }
        posAttr.setZ(i, pz);
      }
      dustGeo.attributes.position.needsUpdate = true;
      dustParticles.position.z = game.pos.z;

      // 2. Wingtip Trails
      const leftWingPos = new THREE.Vector3(-2.8, 0.1, 0.8).applyMatrix4(playerGroup.matrixWorld);
      const rightWingPos = new THREE.Vector3(2.8, 0.1, 0.8).applyMatrix4(playerGroup.matrixWorld);

      trailPositionsL.unshift(leftWingPos);
      trailPositionsL.pop();
      trailPositionsR.unshift(rightWingPos);
      trailPositionsR.pop();

      trailGeoL.setFromPoints(trailPositionsL);
      trailGeoR.setFromPoints(trailPositionsR);

      // 3. Crash Debris Update
      for (let i = debrisList.length - 1; i >= 0; i--) {
        const d = debrisList[i];
        d.mesh.position.addScaledVector(d.vel, dt);
        d.vel.y -= 35 * dt; // gravity
        d.mesh.rotation.x += d.rotVel.x * dt;
        d.mesh.rotation.y += d.rotVel.y * dt;
        d.life -= dt * 0.8;
        if (d.life <= 0) {
          debrisGroup.remove(d.mesh);
          debrisList.splice(i, 1);
        }
      }
    }

    function drawSpeedLines(dt) {
      if (game.isBoosting || game.currentSpeed > 105) {
        speedLinesCanvas.style.opacity = '1';
        speedVignette.style.opacity = game.isBoosting ? '0.7' : '0.35';
        speedLinesCtx.clearRect(0, 0, speedLinesCanvas.width, speedLinesCanvas.height);

        const cx = speedLinesCanvas.width / 2;
        const cy = speedLinesCanvas.height / 2;
        const lineCount = game.isBoosting ? 28 : 14;

        speedLinesCtx.strokeStyle = game.isBoosting ? 'rgba(5, 217, 232, 0.65)' : 'rgba(255, 170, 60, 0.4)';
        speedLinesCtx.lineWidth = 1.5;

        for (let i = 0; i < lineCount; i++) {
          const angle = Math.random() * Math.PI * 2;
          const r1 = Math.random() * 180 + 120;
          const r2 = r1 + Math.random() * 120 + 80;

          const x1 = cx + Math.cos(angle) * r1;
          const y1 = cy + Math.sin(angle) * r1;
          const x2 = cx + Math.cos(angle) * r2;
          const y2 = cy + Math.sin(angle) * r2;

          speedLinesCtx.beginPath();
          speedLinesCtx.moveTo(x1, y1);
          speedLinesCtx.lineTo(x2, y2);
          speedLinesCtx.stroke();
        }
      } else {
        speedLinesCanvas.style.opacity = '0';
        speedVignette.style.opacity = '0';
      }
    }

    function updateHUD(dt) {
      // Score & Distance
      scoreDisplay.textContent = String(Math.floor(game.score)).padStart(6, '0');
      distDisplay.textContent = `${Math.floor(game.distance)} m`;
      multiplierDisplay.textContent = `x${game.multiplier.toFixed(1)}`;

      // Energy Bar
      energyFill.style.width = `${Math.max(0, game.energy)}%`;

      // Turbo Bar
      turboFill.style.width = `${Math.max(0, game.turboCharge)}%`;
      if (game.turboCharge >= 20) {
        turboFill.classList.add('turbo-ready');
      } else {
        turboFill.classList.remove('turbo-ready');
      }

      // Speedometer
      const kmh = Math.floor(game.currentSpeed * 2.2);
      speedDigits.textContent = `${kmh} KM/H`;

      const maxKmh = 290;
      const minKmh = 140;
      const litCount = Math.floor(((kmh - minKmh) / (maxKmh - minKmh)) * TOTAL_TICKS);
      tickElements.forEach((t, idx) => {
        t.className = 'speed-tick';
        if (idx < litCount) {
          t.classList.add(game.isBoosting ? 'lit-boost' : 'lit');
        }
      });
    }

    function animate(currentTime) {
      requestAnimationFrame(animate);

      const dt = Math.min(0.1, (currentTime - lastTime) / 1000);
      lastTime = currentTime;

      if (game.state === STATE_PLAYING) {
        // Flight physics & world displacement
        updateFlightPhysics(dt);
        updateChunks(game.pos.z, game.difficulty);
        checkCollisionsAndNearMisses(dt);
        updateParticlesAndTrails(dt);
        drawSpeedLines(dt);

        // Continuous scoring & distance progression
        game.distance += game.currentSpeed * dt;
        game.score += game.currentSpeed * dt * 0.6 * game.multiplier;

        // Energy decay mechanic (forces collecting glowing amber orbs)
        game.energy -= game.energyDecayRate * dt;
        if (game.energy <= 0) {
          game.energy = 0;
          gameOver("SOLAR CORE DEPLETED");
        }

        // Multiplier gradual timer decay back to 1.0
        if (game.multiplierTimer > 0) {
          game.multiplierTimer -= dt;
        } else if (game.multiplier > 1.0) {
          game.multiplier = Math.max(1.0, game.multiplier - dt * 0.45);
        }

        updateHUD(dt);

      } else if (game.state === STATE_MENU) {
        // Idle camera sweep in canyon
        const time = currentTime * 0.0006;
        playerGroup.position.set(Math.sin(time) * 4, 3.5 + Math.sin(time * 2) * 0.4, -20);
        playerGroup.rotation.z = Math.sin(time) * 0.2;
        camera.position.set(Math.sin(time) * 3, 5, -12);
        camera.lookAt(playerGroup.position);
        updateParticlesAndTrails(dt);

      } else if (game.state === STATE_GAMEOVER) {
        updateParticlesAndTrails(dt);
      }

      renderer.render(scene, camera);
    }

    /* =========================================================================
       10. GAME STATE TRANSITIONS (START, GAME OVER, RESTART)
       ========================================================================= */
    const startMenu = document.getElementById('start-menu');
    const gameOverMenu = document.getElementById('game-over-menu');
    const startBtn = document.getElementById('start-btn');
    const restartBtn = document.getElementById('restart-btn');

    function startGame() {
      audio.init();
      startMenu.classList.add('hidden');
      gameOverMenu.classList.add('hidden');

      game.state = STATE_PLAYING;
      game.score = 0;
      game.distance = 0;
      game.multiplier = 1.0;
      game.multiplierTimer = 0;
      game.maxMultiplier = 1.0;
      game.nearMissCount = 0;
      game.energy = 100;
      game.turboCharge = 0;
      game.currentSpeed = 75;
      game.maxSpeed = 75;
      game.pos.set(0, 3.5, 0);
      game.vel.set(0, 0, 0);
      game.roll = 0;
      game.pitch = 0;
      game.targetRoll = 0;
      game.targetPitch = 0;
      game.camShake = 0;

      playerGroup.visible = true;

      // Re-init chunks
      chunks.forEach(c => c.clear());
      chunks.length = 0;
      initChunks();
    }

    function gameOver(reason) {
      if (game.state === STATE_GAMEOVER) return;
      game.state = STATE_GAMEOVER;

      audio.playCrash();
      triggerCrashExplosion(game.pos.clone());
      playerGroup.visible = false;

      // Populate game over stats
      document.getElementById('game-over-title').textContent = reason;
      document.getElementById('final-score').textContent = Math.floor(game.score).toLocaleString();
      document.getElementById('final-dist').textContent = `${Math.floor(game.distance)} m`;
      document.getElementById('final-multi').textContent = `x${game.maxMultiplier.toFixed(1)}`;
      document.getElementById('final-nearmiss').textContent = game.nearMissCount;

      setTimeout(() => {
        gameOverMenu.classList.remove('hidden');
      }, 700);
    }

    function restartGame() {
      startGame();
    }

    startBtn.addEventListener('click', startGame);
    restartBtn.addEventListener('click', restartGame);

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

    // Initialize world
    initChunks();
    requestAnimationFrame(animate);
  </script>
</body>
</html>
<!-- agent-meta {"model":"google/gemini-3.7-flash","provider":"openrouter","persona":"full","sessionId":"f7e5b2c7-393d-4420-9289-e82d58289a28","tokensIn":309400,"tokensOut":26051,"tokensTotal":335451,"cost":0.1426462875,"turns":10,"toolCalls":9,"failedToolCalls":0,"timestamp":"2026-08-15T22:59:40.498Z"} -->