← Canyon Run results

Canyon Run

Qwen 3.6 27b MTP · composer

Initial view of Canyon Run

Reviews & generation details

Reviews

Human review · Jul 17, 2026 · rubric 1

Historical path-based review. Association with these exact bytes is unverified.

Visual design
4 / 5
UX / feel
4 / 5
Works
3 / 5
Prompt spirit
5 / 5
Delight
4 / 5
Gorgeous, feels great; but terrain runs out after a few seconds

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 75,345 bytes · SHA-256 59cc201a5db4
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Canyon Racer — High-Speed Procedural Arcade</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0015; font-family: 'Courier New', monospace; }
#game-container { position: relative; width: 100vw; height: 100vh; overflow: hidden; }
#game-canvas { display: block; width: 100%; height: 100%; }
.hidden { display: none !important; }

/* Vignette */
#vignette {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 5;
  background: radial-gradient(ellipse at center, transparent 50%, rgba(10,0,21,0.7) 100%);
}

/* HUD */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }

/* Speed Bar (left side) */
#speed-bar-container {
  position: absolute; left: 30px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; gap: 12px;
}
#speed-label { font-size: 10px; color: rgba(255,140,50,0.6); letter-spacing: 3px; writing-mode: vertical-rl; text-orientation: mixed; }
#speed-track { width: 8px; height: 200px; background: rgba(255,255,255,0.05); border-radius: 4px; overflow: hidden; position: relative; }
#speed-fill { position: absolute; bottom: 0; width: 100%; height: 30%; background: linear-gradient(to top, #ff6a00, #ee0979); border-radius: 4px; transition: height 0.1s ease-out; }
#speed-value { font-size: 28px; color: #ff8c32; font-weight: bold; min-width: 50px; text-shadow: 0 0 10px rgba(255,140,50,0.5); }

/* Multiplier */
#multiplier-container { position: absolute; top: 60px; left: 50%; transform: translateX(-50%); text-align: center; }
#multiplier-label { font-size: 10px; color: rgba(255,255,255,0.3); letter-spacing: 4px; margin-bottom: 4px; }
#multiplier-value { font-size: 48px; color: #ff6a00; font-weight: bold; text-shadow: 0 0 20px rgba(255,106,0,0.6); transition: transform 0.1s; }
#multiplier-value.pulse { animation: multPulse 0.3s ease-out; }

/* Score */
#score-container { position: absolute; top: 40px; right: 40px; text-align: right; }
#score-value { font-size: 36px; color: rgba(255,255,255,0.8); font-weight: bold; letter-spacing: 2px; text-shadow: 0 0 15px rgba(255,255,255,0.3); }

/* Energy Bar */
#energy-bar-container { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); width: 300px; text-align: center; }
#energy-label { font-size: 10px; color: rgba(255,255,255,0.3); letter-spacing: 4px; margin-bottom: 6px; }
#energy-track { width: 100%; height: 6px; background: rgba(255,255,255,0.08); border-radius: 3px; overflow: hidden; }
#energy-fill { width: 100%; height: 100%; background: linear-gradient(to right, #ee0979, #ff6a00); border-radius: 3px; transition: width 0.15s ease-out; box-shadow: 0 0 8px rgba(255,106,0,0.4); }
#energy-fill.low { background: linear-gradient(to right, #ff0000, #ee0979); animation: energyPulse 0.5s infinite; }

/* Turbo Meter */
#turbo-container { position: absolute; bottom: 30px; right: 40px; text-align: right; width: 120px; }
#turbo-label { font-size: 10px; color: rgba(0,255,200,0.4); letter-spacing: 3px; margin-bottom: 6px; }
#turbo-track { width: 100%; height: 4px; background: rgba(255,255,255,0.08); border-radius: 2px; overflow: hidden; }
#turbo-fill { width: 0%; height: 100%; background: linear-gradient(to right, #00ffc8, #00e5ff); border-radius: 2px; transition: width 0.1s ease-out; box-shadow: 0 0 6px rgba(0,255,200,0.4); }

/* Near Miss Flash */
#near-miss-flash {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  pointer-events: none; opacity: 0;
  background: radial-gradient(ellipse at center, rgba(255,140,50,0.3), transparent 70%);
  transition: opacity 0.1s;
}

/* Start Screen */
#start-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 20; background: rgba(10,0,21,0.9); }
#start-content { text-align: center; color: white; }
#start-content h1 { font-size: 64px; letter-spacing: 12px; margin-bottom: 8px; background: linear-gradient(to bottom, #ff8c32, #ee0979); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.subtitle { font-size: 14px; color: rgba(255,255,255,0.4); letter-spacing: 6px; margin-bottom: 40px; }
#controls-info { margin-bottom: 40px; }
#controls-info p { font-size: 13px; color: rgba(255,255,255,0.5); margin: 8px 0; letter-spacing: 1px; }
.key { display: inline-block; padding: 2px 8px; background: rgba(255,255,255,0.1); border-radius: 3px; color: #ff8c32; font-size: 11px; margin: 0 2px; }

/* Buttons */
button { pointer-events: all; background: linear-gradient(135deg, #ff6a00, #ee0979); border: none; color: white; padding: 14px 48px; font-size: 16px; letter-spacing: 4px; cursor: pointer; font-family: 'Courier New', monospace; text-transform: uppercase; transition: transform 0.2s, box-shadow 0.2s; }
button:hover { transform: scale(1.05); box-shadow: 0 0 30px rgba(255,106,0,0.5); }

/* Game Over */
#game-over { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 20; background: rgba(10,0,21,0.85); }
#game-over-content { text-align: center; color: white; }
#go-title { font-size: 56px; letter-spacing: 10px; margin-bottom: 30px; color: #ee0979; text-shadow: 0 0 30px rgba(238,9,121,0.5); }
#go-stats { margin-bottom: 40px; }
.stat-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,0.08); min-width: 300px; margin: 0 auto; }
.stat-label { font-size: 12px; color: rgba(255,255,255,0.4); letter-spacing: 3px; }
.stat-value { font-size: 28px; color: #ff8c32; font-weight: bold; text-shadow: 0 0 10px rgba(255,140,50,0.4); }

/* Animations */
@keyframes multPulse { 0% { transform: scale(1); } 50% { transform: scale(1.3); } 100% { transform: scale(1); } }
@keyframes energyPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
</style>
</head>
<body>
<div id="game-container">
  <canvas id="game-canvas"></canvas>
  
  <!-- HUD Overlay -->
  <div id="hud" class="hidden">
    <!-- Speed Bar (left side) -->
    <div id="speed-bar-container">
      <div id="speed-label">SPEED</div>
      <div id="speed-track">
        <div id="speed-fill"></div>
      </div>
      <div id="speed-value">0</div>
    </div>
    
    <!-- Multiplier (center top) -->
    <div id="multiplier-container">
      <div id="multiplier-label">MULTIPLIER</div>
      <div id="multiplier-value">x1.0</div>
    </div>
    
    <!-- Score (top center-right) -->
    <div id="score-container">
      <div id="score-value">0</div>
    </div>
    
    <!-- Energy Bar (bottom center) -->
    <div id="energy-bar-container">
      <div id="energy-label">ENERGY</div>
      <div id="energy-track">
        <div id="energy-fill"></div>
      </div>
    </div>
    
    <!-- Turbo Meter (bottom right) -->
    <div id="turbo-container">
      <div id="turbo-label">TURBO</div>
      <div id="turbo-track">
        <div id="turbo-fill"></div>
      </div>
    </div>
    
    <!-- Near Miss Flash -->
    <div id="near-miss-flash"></div>
  </div>
  
  <!-- Game Over Screen -->
  <div id="game-over" class="hidden">
    <div id="game-over-content">
      <h1 id="go-title">GAME OVER</h1>
      <div id="go-stats">
        <div class="stat-row"><span class="stat-label">FINAL SCORE</span><span class="stat-value" id="go-score">0</span></div>
        <div class="stat-row"><span class="stat-label">MAX SPEED</span><span class="stat-value" id="go-maxspeed">0</span></div>
        <div class="stat-row"><span class="stat-label">DISTANCE</span><span class="stat-value" id="go-distance">0</span></div>
      </div>
      <button id="restart-btn">RESTART</button>
    </div>
  </div>
  
  <!-- Start Screen -->
  <div id="start-screen">
    <div id="start-content">
      <h1>CANYON RACER</h1>
      <p class="subtitle">HIGH-SPEED PROCEDURAL ARCADE</p>
      <div id="controls-info">
        <p><span class="key">W/S</span> or <span class="key">↑/↓</span> — Pitch Up / Down</p>
        <p><span class="key">A/D</span> or <span class="key">←/→</span> — Roll Left / Right</p>
        <p><span class="key">SPACE</span> — Boost (when Turbo is charged)</p>
      </div>
      <button id="start-btn">LAUNCH</button>
    </div>
  </div>
  
  <!-- Vignette overlay -->
  <div id="vignette"></div>
</div>
<script>
'use strict';

// ── const: CONFIG ──
const CONFIG = {
  // Player physics
  BASE_SPEED: 0.8,
  MAX_SPEED: 2.5,
  BOOST_SPEED: 4.0,
  ACCELERATION: 0.015,
  DECELERATION: 0.008,
  LATERAL_SPEED: 0.35,
  VERTICAL_SPEED: 0.25,
  BANKING_RATE: 0.06,
  MOMENTUM_DAMPING: 0.94,
  
  // Energy system
  ENERGY_MAX: 100,
  ENERGY_DECAY: 0.035,
  ENERGY_RECHARGE: 25,
  
  // Turbo system
  TURBO_MAX: 100,
  TURBO_BUILD_RATE: 0.15,
  TURBO_DRAIN_RATE: 0.6,
  TURBO_THRESHOLD: 80,
  
  // Scoring
  NEAR_MISS_DISTANCE: 4.0,
  NEAR_MISS_MULTIPLIER_BOOST: 0.5,
  MULTIPLIER_DECAY: 0.01,
  BASE_SCORE_RATE: 10,
  
  // Chunk generation
  CHUNK_LENGTH: 80,
  CHUNKS_AHEAD: 6,
  CHUNKS_BEHIND: 2,
  CANYON_WIDTH_BASE: 30,
  CANYON_HEIGHT_BASE: 40,
  
  // Difficulty scaling
  DIFFICULTY_SCALE_RATE: 0.0001,
  MAX_DIFFICULTY: 3.0,
  
  // Visual
  FOV_NORMAL: 75,
  FOV_BOOST: 95,
  CAMERA_DISTANCE: 12,
  CAMERA_HEIGHT: 4,
};

// ── const: COLORS ──
const COLORS = {
  skyTop: new THREE.Color(0x1a0033),
  skyBottom: new THREE.Color(0xff6b35),
  canyonWall: new THREE.Color(0x8b4513),
  canyonWallDark: new THREE.Color(0x5c2d0e),
  monolith: new THREE.Color(0x9b59b6),
  monolithEdge: new THREE.Color(0xff6a00),
  orb: new THREE.Color(0x00ffc8),
  playerBody: new THREE.Color(0xc0c0c0),
  playerAccent: new THREE.Color(0xff6a00),
  engineGlow: new THREE.Color(0xff4500),
  sunColor: new THREE.Color(0xffaa33),
};

// ── class: AudioSystem ──
class AudioSystem {
  constructor() {
    this.ctx = null;
    this.engineOsc = null;
    this.engineGain = null;
    this.masterGain = null;
    this.initialized = false;
  }

  init() {
    if (this.initialized) return;
    try {
      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
      this.masterGain = this.ctx.createGain();
      this.masterGain.gain.value = 0.3;
      this.masterGain.connect(this.ctx.destination);

      // Engine hum - low frequency oscillator with modulation
      this.engineOsc = this.ctx.createOscillator();
      this.engineOsc.type = 'sawtooth';
      this.engineOsc.frequency.value = 60;
      
      const engineFilter = this.ctx.createBiquadFilter();
      engineFilter.type = 'lowpass';
      engineFilter.frequency.value = 200;
      engineFilter.Q.value = 5;

      this.engineGain = this.ctx.createGain();
      this.engineGain.gain.value = 0.15;

      // LFO for engine modulation
      const lfo = this.ctx.createOscillator();
      lfo.type = 'sine';
      lfo.frequency.value = 8;
      const lfoGain = this.ctx.createGain();
      lfoGain.gain.value = 15;
      lfo.connect(lfoGain);
      lfoGain.connect(this.engineOsc.frequency);
      lfo.start();

      this.engineOsc.connect(engineFilter);
      engineFilter.connect(this.engineGain);
      this.engineGain.connect(this.masterGain);
      this.engineOsc.start();

      // Background drone
      const drone = this.ctx.createOscillator();
      drone.type = 'sine';
      drone.frequency.value = 40;
      const droneGain = this.ctx.createGain();
      droneGain.gain.value = 0.06;
      drone.connect(droneGain);
      droneGain.connect(this.masterGain);
      drone.start();

      this.initialized = true;
    } catch(e) { console.warn('Audio init failed:', e); }
  }

  updateEngine(speedRatio) {
    if (!this.initialized || !this.engineOsc) return;
    const freq = 50 + speedRatio * 120;
    this.engineOsc.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.1);
    this.engineGain.gain.setTargetAtTime(0.1 + speedRatio * 0.15, this.ctx.currentTime, 0.1);
  }

  playCollect() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(800, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(1600, this.ctx.currentTime + 0.15);
    
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.2, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
    
    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.3);
  }

  playNearMiss() {
    if (!this.initialized) return;
    // Whoosh sound - filtered noise burst
    const bufferSize = this.ctx.sampleRate * 0.2;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3));
    }
    
    const source = this.ctx.createBufferSource();
    source.buffer = buffer;
    
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'bandpass';
    filter.frequency.setValueAtTime(2000, this.ctx.currentTime);
    filter.frequency.exponentialRampToValueAtTime(500, this.ctx.currentTime + 0.15);
    filter.Q.value = 3;
    
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.25, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
    
    source.connect(filter);
    filter.connect(gain);
    gain.connect(this.masterGain);
    source.start();
  }

  playBoost() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(200, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(800, this.ctx.currentTime + 0.3);
    
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.15, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.4);
    
    osc.connect(gain);
    gain.connect(this.masterGain);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.4);
  }

  playCrash() {
    if (!this.initialized) return;
    const bufferSize = this.ctx.sampleRate * 0.5;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.15));
    }
    
    const source = this.ctx.createBufferSource();
    source.buffer = buffer;
    
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.setValueAtTime(3000, this.ctx.currentTime);
    filter.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.5);
    
    const gain = this.ctx.createGain();
    gain.gain.setValueAtTime(0.4, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.6);
    
    source.connect(filter);
    filter.connect(gain);
    gain.connect(this.masterGain);
    source.start();

    // Low boom
    const osc = this.ctx.createOscillator();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(100, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(20, this.ctx.currentTime + 0.4);
    const boomGain = this.ctx.createGain();
    boomGain.gain.setValueAtTime(0.3, this.ctx.currentTime);
    boomGain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
    osc.connect(boomGain);
    boomGain.connect(this.masterGain);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.5);
  }

  stop() {
    if (this.engineOsc) { try { this.engineOsc.stop(); } catch(e){} }
    if (this.ctx) { try { this.ctx.close(); } catch(e){} }
    this.initialized = false;
  }
}

// ── class: Player ──
class Player {
  constructor() {
    this.mesh = null;
    this.position = new THREE.Vector3(0, 0, 0);
    this.velocity = new THREE.Vector3(0, 0, 0);
    this.targetBankAngle = 0;
    this.currentBankAngle = 0;
    this.targetPitchAngle = 0;
    this.currentPitchAngle = 0;
    this.speed = CONFIG.BASE_SPEED;
    this.boundaries = { minX: -12, maxX: 12, minY: -8, maxY: 15 };
    this.engineGlowMesh = null;
    this.trailPositions = [];
    this.createCraft();
  }

  createCraft() {
    const group = new THREE.Group();

    // Main body - sleek arrowhead shape
    const bodyGeo = new THREE.ConeGeometry(0.4, 2.5, 6);
    bodyGeo.rotateX(Math.PI / 2);
    const bodyMat = new THREE.MeshLambertMaterial({ color: COLORS.playerBody, flatShading: true });
    const body = new THREE.Mesh(bodyGeo, bodyMat);
    group.add(body);

    // Wings - swept back triangles
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(-2.5, -1.5);
    wingShape.lineTo(-2.0, -0.3);
    wingShape.lineTo(0, 0.3);

    const wingExtrudeSettings = { depth: 0.08, bevelEnabled: false };
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, wingExtrudeSettings);
    
    const leftWingMat = new THREE.MeshLambertMaterial({ color: COLORS.playerAccent, flatShading: true });
    const rightWingMat = new THREE.MeshLambertMaterial({ color: COLORS.playerAccent, flatShading: true });
    
    const leftWing = new THREE.Mesh(wingGeo, leftWingMat);
    leftWing.position.set(0.15, -0.04, 0.3);
    group.add(leftWing);

    const rightWingShape = new THREE.Shape();
    rightWingShape.moveTo(0, 0);
    rightWingShape.lineTo(2.5, -1.5);
    rightWingShape.lineTo(2.0, -0.3);
    rightWingShape.lineTo(0, 0.3);
    
    const rightWingGeo = new THREE.ExtrudeGeometry(rightWingShape, wingExtrudeSettings);
    const rightWing = new THREE.Mesh(rightWingGeo, rightWingMat);
    rightWing.position.set(-0.15, -0.04, 0.3);
    group.add(rightWing);

    // Engine glow (back of craft)
    const engineGeo = new THREE.SphereGeometry(0.2, 6, 4);
    const engineMat = new THREE.MeshBasicMaterial({ color: COLORS.engineGlow });
    this.engineGlowMesh = new THREE.Mesh(engineGeo, engineMat);
    this.engineGlowMesh.position.set(0, 0, -1.3);
    group.add(this.engineGlowMesh);

    // Engine light
    const engineLight = new THREE.PointLight(COLORS.engineGlow, 2, 8);
    engineLight.position.set(0, 0, -1.5);
    group.add(engineLight);

    this.mesh = group;
  }

  update(input, dt) {
    // Apply input with momentum
    const lateralAccel = CONFIG.LATERAL_SPEED * dt * 60;
    const verticalAccel = CONFIG.VERTICAL_SPEED * dt * 60;

    if (input.left) this.velocity.x -= lateralAccel;
    if (input.right) this.velocity.x += lateralAccel;
    if (input.up) this.velocity.y += verticalAccel;
    if (input.down) this.velocity.y -= verticalAccel;

    // Apply damping
    this.velocity.multiplyScalar(Math.pow(CONFIG.MOMENTUM_DAMPING, dt * 60));

    // Clamp velocity
    const maxLatVel = CONFIG.LATERAL_SPEED * 2;
    const maxVertVel = CONFIG.VERTICAL_SPEED * 2;
    this.velocity.x = Math.max(-maxLatVel, Math.min(maxLatVel, this.velocity.x));
    this.velocity.y = Math.max(-maxVertVel, Math.min(maxVertVel, this.velocity.y));

    // Update position
    this.position.x += this.velocity.x * dt * 60;
    this.position.y += this.velocity.y * dt * 60;

    // Clamp to boundaries (with some forgiveness)
    const b = this.boundaries;
    this.position.x = Math.max(b.minX - 2, Math.min(b.maxX + 2, this.position.x));
    this.position.y = Math.max(b.minY - 1, Math.min(b.maxY + 1, this.position.y));

    // Banking animation based on lateral velocity
    this.targetBankAngle = -this.velocity.x * 0.8;
    this.currentBankAngle += (this.targetBankAngle - this.currentBankAngle) * CONFIG.BANKING_RATE * dt * 60;

    // Pitch animation based on vertical velocity
    this.targetPitchAngle = -this.velocity.y * 0.5;
    this.currentPitchAngle += (this.targetPitchAngle - this.currentPitchAngle) * CONFIG.BANKING_RATE * dt * 60;

    // Apply rotation to mesh
    if (this.mesh) {
      this.mesh.position.copy(this.position);
      this.mesh.rotation.z = this.currentBankAngle;
      this.mesh.rotation.x = this.currentPitchAngle;

      // Engine glow pulse
      const speedRatio = (this.speed - CONFIG.BASE_SPEED) / (CONFIG.BOOST_SPEED - CONFIG.BASE_SPEED);
      if (this.engineGlowMesh) {
        const scale = 1 + speedRatio * 0.8 + Math.sin(Date.now() * 0.01) * 0.1;
        this.engineGlowMesh.scale.set(scale, scale, scale * 2);
      }
    }

    // Record trail positions
    if (this.trailPositions.length === 0 || 
        this.position.distanceTo(this.trailPositions[this.trailPositions.length - 1]) > 0.5) {
      this.trailPositions.push(this.position.clone());
      if (this.trailPositions.length > 40) this.trailPositions.shift();
    }

    return this;
  }

  getSpeedRatio() {
    return Math.max(0, Math.min(1, (this.speed - CONFIG.BASE_SPEED) / (CONFIG.BOOST_SPEED - CONFIG.BASE_SPEED)));
  }
}

// ── class: ParticleSystem ──
class ParticleSystem {
  constructor(scene) {
    this.scene = scene;
    this.trailParticles = [];
    this.debrisParticles = [];
    this.explosionParticles = [];
    
    // Trail particle pool
    this.createTrailPool();
    this.createDebrisPool();
  }

  createTrailPool() {
    const geometry = new THREE.BufferGeometry();
    const count = 200;
    const positions = new Float32Array(count * 3);
    const colors = new Float32Array(count * 4); // rgba
    const sizes = new Float32Array(count);
    
    for (let i = 0; i < count; i++) {
      positions[i * 3] = 0;
      positions[i * 3 + 1] = 0;
      positions[i * 3 + 2] = 0;
      colors[i * 4] = 1; // r
      colors[i * 4 + 1] = 0.4; // g
      colors[i * 4 + 2] = 0; // b
      colors[i * 4 + 3] = 0; // a (hidden initially)
      sizes[i] = 0.3;
    }
    
    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
    geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
    
    const material = new THREE.PointsMaterial({
      size: 0.5,
      vertexColors: true,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      sizeAttenuation: true,
    });
    
    this.trailPoints = new THREE.Points(geometry, material);
    this.scene.add(this.trailPoints);
    
    // Initialize particle data
    for (let i = 0; i < count; i++) {
      this.trailParticles.push({ 
        active: false, life: 0, maxLife: 1, velocity: new THREE.Vector3() 
      });
    }
  }

  createDebrisPool() {
    const geometry = new THREE.BufferGeometry();
    const count = 150;
    const positions = new Float32Array(count * 3);
    const colors = new Float32Array(count * 4);
    
    for (let i = 0; i < count; i++) {
      positions[i * 3] = 0;
      positions[i * 3 + 1] = 0;
      positions[i * 3 + 2] = 0;
      colors[i * 4] = 0.5;
      colors[i * 4 + 1] = 0.3;
      colors[i * 4 + 2] = 0.1;
      colors[i * 4 + 3] = 0;
    }
    
    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
    
    const material = new THREE.PointsMaterial({
      size: 0.2,
      vertexColors: true,
      transparent: true,
      depthWrite: false,
      sizeAttenuation: true,
    });
    
    this.debrisPoints = new THREE.Points(geometry, material);
    this.scene.add(this.debrisPoints);
    
    for (let i = 0; i < count; i++) {
      this.debrisParticles.push({ 
        active: false, life: 0, maxLife: 1, velocity: new THREE.Vector3() 
      });
    }
  }

  emitTrail(position, speedRatio) {
    // Find inactive particle
    for (let p of this.trailParticles) {
      if (!p.active) {
        p.active = true;
        p.life = 0;
        p.maxLife = 0.5 + Math.random() * 0.3;
        p.velocity.set(
          (Math.random() - 0.5) * 0.1,
          (Math.random() - 0.5) * 0.1,
          -(speedRatio * 2 + 0.5)
        );
        return position.clone();
      }
    }
    return null;
  }

  emitDebris(position, speed) {
    for (let p of this.debrisParticles) {
      if (!p.active) {
        p.active = true;
        p.life = 0;
        p.maxLife = 0.3 + Math.random() * 0.5;
        p.velocity.set(
          (Math.random() - 0.5) * speed * 2,
          (Math.random() - 0.5) * speed * 2,
          -(speed * 3 + Math.random())
        );
        return position.clone();
      }
    }
    return null;
  }

  emitExplosion(position) {
    for (let i = 0; i < 30; i++) {
      for (let p of this.trailParticles) {
        if (!p.active) {
          p.active = true;
          p.life = 0;
          p.maxLife = 0.5 + Math.random() * 1.0;
          const angle = Math.random() * Math.PI * 2;
          const upAngle = (Math.random() - 0.5) * Math.PI;
          const speed = 2 + Math.random() * 4;
          p.velocity.set(
            Math.cos(angle) * Math.cos(upAngle) * speed,
            Math.sin(upAngle) * speed,
            Math.sin(angle) * Math.cos(upAngle) * speed
          );
          break;
        }
      }
    }
  }

  update(dt, playerPos, speedRatio) {
    // Update trail particles
    const tPositions = this.trailPoints.geometry.attributes.position.array;
    const tColors = this.trailPoints.geometry.attributes.color.array;
    
    for (let i = 0; i < this.trailParticles.length; i++) {
      const p = this.trailParticles[i];
      
      if (!p.active) {
        tColors[i * 4 + 3] = 0;
        continue;
      }
      
      p.life += dt;
      if (p.life >= p.maxLife) {
        p.active = false;
        tColors[i * 4 + 3] = 0;
        continue;
      }
      
      const lifeRatio = 1 - p.life / p.maxLife;
      
      // Move particle relative to player movement
      tPositions[i * 3] += p.velocity.x * dt * 60;
      tPositions[i * 3 + 1] += p.velocity.y * dt * 60;
      tPositions[i * 3 + 2] += p.velocity.z * dt * 60;
      
      // Color based on speed ratio (orange to red)
      const r = 1.0;
      const g = 0.4 - lifeRatio * 0.3;
      const b = lifeRatio * 0.2;
      tColors[i * 4] = r;
      tColors[i * 4 + 1] = Math.max(0, g);
      tColors[i * 4 + 2] = b;
      tColors[i * 4 + 3] = lifeRatio * (0.6 + speedRatio * 0.4);
    }
    
    this.trailPoints.geometry.attributes.position.needsUpdate = true;
    this.trailPoints.geometry.attributes.color.needsUpdate = true;

    // Update debris particles
    const dPositions = this.debrisPoints.geometry.attributes.position.array;
    const dColors = this.debrisPoints.geometry.attributes.color.array;
    
    for (let i = 0; i < this.debrisParticles.length; i++) {
      const p = this.debrisParticles[i];
      
      if (!p.active) {
        dColors[i * 4 + 3] = 0;
        continue;
      }
      
      p.life += dt;
      if (p.life >= p.maxLife) {
        p.active = false;
        dColors[i * 4 + 3] = 0;
        continue;
      }
      
      const lifeRatio = 1 - p.life / p.maxLife;
      
      dPositions[i * 3] += p.velocity.x * dt * 60;
      dPositions[i * 3 + 1] += p.velocity.y * dt * 60;
      dPositions[i * 3 + 2] += p.velocity.z * dt * 60;
      
      // Dust color - warm brownish
      const brightness = 0.4 + lifeRatio * 0.3;
      dColors[i * 4] = brightness;
      dColors[i * 4 + 1] = brightness * 0.5;
      dColors[i * 4 + 2] = brightness * 0.2;
      dColors[i * 4 + 3] = lifeRatio * 0.6;
    }
    
    this.debrisPoints.geometry.attributes.position.needsUpdate = true;
    this.debrisPoints.geometry.attributes.color.needsUpdate = true;

    // Emit new trail particles based on speed
    if (Math.random() < 0.3 + speedRatio * 0.7) {
      const offset = new THREE.Vector3(
        (Math.random() - 0.5) * 0.5,
        (Math.random() - 0.5) * 0.5,
        -1.5
      );
      this.emitTrail(playerPos.clone().add(offset), speedRatio);
    }

    // Emit debris particles based on speed
    if (Math.random() < 0.1 + speedRatio * 0.4) {
      const offset = new THREE.Vector3(
        (Math.random() - 0.5) * 20,
        (Math.random() - 0.5) * 15,
        -CONFIG.CHUNK_LENGTH * 0.5 + Math.random() * CONFIG.CHUNK_LENGTH
      );
      this.emitDebris(playerPos.clone().add(offset), speedRatio);
    }
  }

  dispose() {
    if (this.trailPoints) {
      this.scene.remove(this.trailPoints);
      this.trailPoints.geometry.dispose();
      this.trailPoints.material.dispose();
    }
    if (this.debrisPoints) {
      this.scene.remove(this.debrisPoints);
      this.debrisPoints.geometry.dispose();
      this.debrisPoints.material.dispose();
    }
  }
}

// ── class: CollisionDetector ──
class CollisionDetector {
  constructor() {
    this.playerRadius = 0.8;
    this.nearMissThreshold = CONFIG.NEAR_MISS_DISTANCE;
  }

  checkCollision(playerPos, collisionBoxes) {
    const px = playerPos.x;
    const py = playerPos.y;
    const pz = playerPos.z;
    
    let collided = false;
    let closestDist = Infinity;
    let nearMissSide = null;

    for (const box of collisionBoxes) {
      // Only check boxes that are close in Z
      if (Math.abs(box.minZ - pz) > 10 || Math.abs(box.maxZ - pz) > 10) continue;
      
      // Find closest point on AABB to player position
      const closestX = Math.max(box.minX, Math.min(px, box.maxX));
      const closestY = Math.max(box.minY, Math.min(py, box.maxY));
      const closestZ = Math.max(box.minZ, Math.min(pz, box.maxZ));
      
      // Distance from player to closest point on box
      const dx = px - closestX;
      const dy = py - closestY;
      const dz = pz - closestZ;
      const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
      
      if (dist < this.playerRadius) {
        collided = true;
      }
      
      if (dist < closestDist) {
        closestDist = dist;
        
        // Determine which side is closest for near-miss feedback
        const absDx = Math.abs(dx);
        const absDy = Math.abs(dy);
        if (absDx > absDy && absDx > Math.abs(dz)) {
          nearMissSide = dx > 0 ? 'right' : 'left';
        } else if (absDy > Math.abs(dz)) {
          nearMissSide = dy > 0 ? 'above' : 'below';
        } else {
          nearMissSide = dz > 0 ? 'behind' : 'front';
        }
      }
    }

    return { collided, closestDist, nearMissSide };
  }

  checkOrbCollection(playerPos, orbs) {
    const collected = [];
    for (const orb of orbs) {
      const dist = playerPos.distanceTo(orb.worldPos);
      if (dist < 2.0) {
        collected.push(orb);
      }
    }
    return collected;
  }
}

// ── class: CameraController ──
class CameraController {
  constructor(camera) {
    this.camera = camera;
    this.shakeIntensity = 0;
    this.targetShake = 0;
    this.shakeDecay = 0.92;
    this.baseFOV = CONFIG.FOV_NORMAL;
    this.currentFOV = CONFIG.FOV_NORMAL;
    this.targetFOV = CONFIG.FOV_NORMAL;
    
    // Camera offset from player
    this.offset = new THREE.Vector3(0, CONFIG.CAMERA_HEIGHT, CONFIG.CAMERA_DISTANCE);
  }

  update(playerPos, speedRatio, dt) {
    // Smooth camera follow with slight lag for dynamic feel
    const targetX = playerPos.x * 0.3; // Slight lateral follow
    const targetY = playerPos.y * 0.5 + CONFIG.CAMERA_HEIGHT;
    
    this.camera.position.x += (targetX - this.camera.position.x) * 0.08;
    this.camera.position.y += (targetY - this.camera.position.y) * 0.06;
    this.camera.position.z = playerPos.z + CONFIG.CAMERA_DISTANCE;
    
    // Look at point ahead of player
    const lookTarget = new THREE.Vector3(
      playerPos.x * 0.5,
      playerPos.y * 0.7,
      playerPos.z - 20
    );
    this.camera.lookAt(lookTarget);

    // FOV changes based on speed
    this.targetFOV = CONFIG.FOV_NORMAL + speedRatio * (CONFIG.FOV_BOOST - CONFIG.FOV_NORMAL);
    this.currentFOV += (this.targetFOV - this.currentFOV) * 0.1;
    this.camera.fov = this.currentFOV;
    this.camera.updateProjectionMatrix();

    // Apply camera shake
    if (this.shakeIntensity > 0.01) {
      const shakeX = (Math.random() - 0.5) * this.shakeIntensity;
      const shakeY = (Math.random() - 0.5) * this.shakeIntensity;
      const shakeZ = (Math.random() - 0.5) * this.shakeIntensity * 0.5;
      
      this.camera.position.x += shakeX;
      this.camera.position.y += shakeY;
      this.camera.position.z += shakeZ;
      
      this.shakeIntensity *= Math.pow(this.shakeDecay, dt * 60);
    } else {
      this.shakeIntensity = 0;
    }
  }

  addShake(intensity) {
    this.targetShake = Math.min(2.0, this.targetShake + intensity);
    this.shakeIntensity = Math.max(this.shakeIntensity, this.targetShake);
    this.targetShake *= 0.9; // Decay target shake
  }

  setFOV(fov) {
    this.targetFOV = fov;
  }
}

// ── class: GameState ──
class GameState {
  constructor() {
    this.reset();
  }

  reset() {
    this.score = 0;
    this.energy = CONFIG.ENERGY_MAX;
    this.multiplier = 1.0;
    this.turbo = 0;
    this.maxSpeed = CONFIG.BASE_SPEED;
    this.distance = 0;
    this.isRunning = false;
    this.isGameOver = false;
    this.nearMissTimer = 0;
    this.lastNearMissTime = 0;
  }

  update(dt, speed, collisionResult) {
    if (!this.isRunning || this.isGameOver) return;

    // Distance tracking
    this.distance += speed * dt * 60;

    // Max speed tracking
    if (speed > this.maxSpeed) this.maxSpeed = speed;

    // Energy decay
    this.energy -= CONFIG.ENERGY_DECAY * dt * 60;
    this.energy = Math.max(0, this.energy);

    // Turbo building from high speed straightaways
    const speedRatio = (speed - CONFIG.BASE_SPEED) / (CONFIG.BOOST_SPEED - CONFIG.BASE_SPEED);
    if (speedRatio > 0.5 && !this.isBoosting()) {
      this.turbo += CONFIG.TURBO_BUILD_RATE * dt * 60;
      this.turbo = Math.min(CONFIG.TURBO_MAX, this.turbo);
    }

    // Near-miss scoring
    if (collisionResult.closestDist < CONFIG.NEAR_MISS_DISTANCE && 
        collisionResult.closestDist > 0.5) {
      const now = performance.now();
      if (now - this.lastNearMissTime > 200) { // Debounce near misses
        this.multiplier += CONFIG.NEAR_MISS_MULTIPLIER_BOOST;
        this.score += Math.floor(50 * this.multiplier);
        this.nearMissTimer = 0.3;
        this.lastNearMissTime = now;
        
        // Turbo boost from near miss
        this.turbo = Math.min(CONFIG.TURBO_MAX, this.turbo + 10);
      }
    }

    // Multiplier decay
    if (this.nearMissTimer > 0) {
      this.nearMissTimer -= dt;
    } else {
      this.multiplier = Math.max(1.0, this.multiplier - CONFIG.MULTIPLIER_DECAY * dt * 60);
    }

    // Base score from distance and speed
    this.score += Math.floor(CONFIG.BASE_SCORE_RATE * this.multiplier * speedRatio * dt * 60);

    // Check game over conditions
    if (this.energy <= 0) {
      this.triggerGameOver();
    }
  }

  isBoosting() {
    return this.turbo >= CONFIG.TURBO_THRESHOLD;
  }

  activateBoost() {
    if (!this.isBoosting()) return false;
    this._boostActive = true;
    return true;
  }

  updateBoost(dt) {
    if (this._boostActive) {
      this.turbo -= CONFIG.TURBO_DRAIN_RATE * dt * 60;
      if (this.turbo < CONFIG.TURBO_THRESHOLD) {
        this._boostActive = false;
      }
    }
    return this._boostActive || false;
  }

  rechargeEnergy(amount) {
    this.energy = Math.min(CONFIG.ENERGY_MAX, this.energy + amount);
    this.score += Math.floor(25 * this.multiplier);
  }

  triggerGameOver() {
    this.isGameOver = true;
    this.isRunning = false;
  }

  getSpeedRatio(speed) {
    return Math.max(0, Math.min(1, (speed - CONFIG.BASE_SPEED) / (CONFIG.BOOST_SPEED - CONFIG.BASE_SPEED)));
  }
}

// ── class: HUDManager ──
class HUDManager {
  constructor() {
    this.speedFill = document.getElementById('speed-fill');
    this.speedValue = document.getElementById('speed-value');
    this.multiplierValue = document.getElementById('multiplier-value');
    this.scoreValue = document.getElementById('score-value');
    this.energyFill = document.getElementById('energy-fill');
    this.turboFill = document.getElementById('turbo-fill');
    this.nearMissFlash = document.getElementById('near-miss-flash');
    this.hud = document.getElementById('hud');
    this.gameOverScreen = document.getElementById('game-over');
    this.startScreen = document.getElementById('start-screen');
    
    this.goScore = document.getElementById('go-score');
    this.goMaxSpeed = document.getElementById('go-maxspeed');
    this.goDistance = document.getElementById('go-distance');
  }

  showHUD() {
    this.hud.classList.remove('hidden');
    this.startScreen.classList.add('hidden');
    this.gameOverScreen.classList.add('hidden');
  }

  hideHUD() {
    this.hud.classList.add('hidden');
  }

  showStartScreen() {
    this.startScreen.classList.remove('hidden');
    this.hud.classList.add('hidden');
    this.gameOverScreen.classList.add('hidden');
  }

  showGameOver(gameState) {
    this.goScore.textContent = Math.floor(gameState.score).toLocaleString();
    this.goMaxSpeed.textContent = Math.floor(gameState.maxSpeed * 100);
    this.goDistance.textContent = Math.floor(gameState.distance).toLocaleString() + 'm';
    
    this.gameOverScreen.classList.remove('hidden');
    this.hud.classList.add('hidden');
  }

  update(gameState, speedRatio) {
    // Speed bar
    const speedPercent = Math.min(100, speedRatio * 100);
    this.speedFill.style.height = speedPercent + '%';
    this.speedValue.textContent = Math.floor(speedRatio * 300);

    // Multiplier
    this.multiplierValue.textContent = 'x' + gameState.multiplier.toFixed(1);
    
    if (gameState.nearMissTimer > 0) {
      this.multiplierValue.classList.add('pulse');
      setTimeout(() => this.multiplierValue.classList.remove('pulse'), 300);
      
      // Near miss flash
      this.nearMissFlash.style.opacity = gameState.nearMissTimer * 2;
    } else {
      this.nearMissFlash.style.opacity = '0';
    }

    // Score
    this.scoreValue.textContent = Math.floor(gameState.score).toLocaleString();

    // Energy bar
    const energyPercent = (gameState.energy / CONFIG.ENERGY_MAX) * 100;
    this.energyFill.style.width = energyPercent + '%';
    
    if (energyPercent < 25) {
      this.energyFill.classList.add('low');
    } else {
      this.energyFill.classList.remove('low');
    }

    // Turbo meter
    const turboPercent = (gameState.turbo / CONFIG.TURBO_MAX) * 100;
    this.turboFill.style.width = turboPercent + '%';
    
    if (gameState.isBoosting()) {
      this.turboFill.style.background = 'linear-gradient(to right, #ff6a00, #ee0979)';
    } else {
      this.turboFill.style.background = '';
    }
  }
}

// ── class: InputHandler ──
class InputHandler {
  constructor() {
    this.keys = {};
    this.left = false;
    this.right = false;
    this.up = false;
    this.down = false;
    this.boost = false;

    window.addEventListener('keydown', (e) => {
      this.keys[e.code] = true;
      this.updateState();
      
      // Prevent scrolling with game keys
      if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Space'].includes(e.code)) {
        e.preventDefault();
      }
    });

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

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

// ── class: BloomManager ──
class BloomManager {
  constructor(scene) {
    this.scene = scene;
    this.glowSprites = [];
  }

  createGlowSprite(color, size, position) {
    const canvas = document.createElement('canvas');
    canvas.width = 64;
    canvas.height = 64;
    const ctx = canvas.getContext('2d');
    
    const r = (color >> 16) & 0xff;
    const g = (color >> 8) & 0xff;
    const b = color & 0xff;
    
    const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
    gradient.addColorStop(0, `rgba(${r},${g},${b},0.8)`);
    gradient.addColorStop(0.3, `rgba(${r},${g},${b},0.4)`);
    gradient.addColorStop(0.7, `rgba(${r},${g},${b},0.1)`);
    gradient.addColorStop(1, `rgba(${r},${g},${b},0)`);
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 64, 64);

    const texture = new THREE.CanvasTexture(canvas);
    const material = new THREE.SpriteMaterial({ 
      map: texture, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false 
    });
    const sprite = new THREE.Sprite(material);
    sprite.scale.set(size, size, 1);
    sprite.position.copy(position);
    
    this.scene.add(sprite);
    this.glowSprites.push({ sprite, baseSize: size, pulsePhase: Math.random() * Math.PI * 2 });
    return sprite;
  }

  update(time) {
    for (const gs of this.glowSprites) {
      const pulse = 1 + Math.sin(time * 3 + gs.pulsePhase) * 0.15;
      gs.sprite.scale.set(gs.baseSize * pulse, gs.baseSize * pulse, 1);
    }
  }

  dispose() {
    for (const gs of this.glowSprites) {
      this.scene.remove(gs.sprite);
      if (gs.sprite.material.map) gs.sprite.material.map.dispose();
      gs.sprite.material.dispose();
    }
    this.glowSprites = [];
  }
}

// ── class: SpeedLines ──
class SpeedLines {
  constructor(scene) {
    this.scene = scene;
    this.active = false;
    
    const count = 100;
    const geometry = new THREE.BufferGeometry();
    const positions = new Float32Array(count * 6); // 2 vertices per line, 3 coords each
    const colors = new Float32Array(count * 8); // rgba per vertex
    
    for (let i = 0; i < count; i++) {
      positions[i * 12] = 0;     // start x
      positions[i * 12 + 1] = 0; // start y
      positions[i * 12 + 2] = 0; // start z
      positions[i * 12 + 3] = 0; // end x
      positions[i * 12 + 4] = 0; // end y
      positions[i * 12 + 5] = 0; // end z
      
      colors[i * 8] = 1;     // start r
      colors[i * 8 + 1] = 0.6; // start g
      colors[i * 8 + 2] = 0.3; // start b
      colors[i * 8 + 3] = 0;   // start a (hidden)
      colors[i * 8 + 4] = 1;   // end r
      colors[i * 8 + 5] = 0.6; // end g
      colors[i * 8 + 6] = 0.3; // end b
      colors[i * 8 + 7] = 0;   // end a (hidden)
    }
    
    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
    
    const material = new THREE.LineBasicMaterial({ 
      vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false 
    });
    
    this.lines = new THREE.LineSegments(geometry, material);
    this.scene.add(this.lines);
    
    this.lineData = [];
    for (let i = 0; i < count; i++) {
      this.lineData.push({ active: false, life: 0, maxLife: 0.5 });
    }
  }

  emit(playerPos, speedRatio) {
    if (!this.active && speedRatio < 0.3) return;
    
    const positions = this.lines.geometry.attributes.position.array;
    const colors = this.lines.geometry.attributes.color.array;
    
    for (let i = 0; i < this.lineData.length; i++) {
      const ld = this.lineData[i];
      
      if (!ld.active && Math.random() < speedRatio * 0.3) {
        ld.active = true;
        ld.life = 0;
        ld.maxLife = 0.2 + Math.random() * 0.4;
        
        // Random position around player, ahead in the distance
        const angle = Math.random() * Math.PI * 2;
        const dist = 5 + Math.random() * 30;
        const x = playerPos.x + Math.cos(angle) * dist;
        const y = playerPos.y + (Math.random() - 0.5) * 20;
        const z = playerPos.z - CONFIG.CHUNK_LENGTH * 0.5 - Math.random() * CONFIG.CHUNK_LENGTH;
        
        positions[i * 12] = x;
        positions[i * 12 + 1] = y;
        positions[i * 12 + 2] = z;
        positions[i * 12 + 3] = x + (Math.random() - 0.5) * 0.5;
        positions[i * 12 + 4] = y - 1 - Math.random() * 2;
        positions[i * 12 + 5] = z;
        
        colors[i * 8 + 3] = 0.6;
        colors[i * 8 + 7] = 0.2;
      } else if (ld.active) {
        ld.life += 0.016; // approximate dt
        
        if (ld.life >= ld.maxLife) {
          ld.active = false;
          colors[i * 8 + 3] = 0;
          colors[i * 8 + 7] = 0;
        } else {
          const lifeRatio = 1 - ld.life / ld.maxLife;
          
          // Move lines toward player (simulating speed)
          positions[i * 12 + 2] += speedRatio * 5;
          positions[i * 12 + 5] += speedRatio * 5;
          
          colors[i * 8 + 3] = lifeRatio * 0.6;
          colors[i * 8 + 7] = lifeRatio * 0.2;
        }
      }
    }
    
    this.lines.geometry.attributes.position.needsUpdate = true;
    this.lines.geometry.attributes.color.needsUpdate = true;
  }

  dispose() {
    this.scene.remove(this.lines);
    this.lines.geometry.dispose();
    this.lines.material.dispose();
  }
}

// ── class: ChunkManager ──
class ChunkManager {
  constructor(scene) {
    this.scene = scene;
    this.chunks = new Map();
    this.chunkIndex = 0;
    this.difficulty = 1.0;
    this.rng = this.createRNG(42);
    
    // Shared materials for performance (reuse across chunks)
    this.wallMaterials = [
      new THREE.MeshLambertMaterial({ color: 0x8b5e3c, flatShading: true }),
      new THREE.MeshLambertMaterial({ color: 0x7a4f2e, flatShading: true }),
      new THREE.MeshLambertMaterial({ color: 0x6d4528, flatShading: true }),
    ];
    this.floorMat = new THREE.MeshLambertMaterial({ color: 0x3d1f0a, flatShading: true });
    this.monolithMats = [
      new THREE.MeshLambertMaterial({ color: 0x9b59b6, flatShading: true }),
      new THREE.MeshLambertMaterial({ color: 0x8e44ad, flatShading: true }),
      new THREE.MeshLambertMaterial({ color: 0xa569bd, flatShading: true }),
    ];
    this.edgeMat = new THREE.LineBasicMaterial({ color: 0xff6a00, transparent: true, opacity: 0.7 });
    
    // Set of all shared material references for quick lookup during disposal
    this.sharedMaterials = new Set([
      ...this.wallMaterials, this.floorMat, 
      ...this.monolithMats, this.edgeMat
    ]);
  }

  createRNG(seed) {
    let s = seed;
    return () => {
      s = (s * 16807 + 0) % 2147483647;
      return (s - 1) / 2147483646;
    };
  }

  getChunkZ(index) { return index * CONFIG.CHUNK_LENGTH; }

  generateChunk(index) {
    const z = this.getChunkZ(index);
    const chunkGroup = new THREE.Group();
    chunkGroup.position.z = -z;

    let canyonWidth = (CONFIG.CANYON_WIDTH_BASE - index * 0.1) / this.difficulty;
    canyonWidth += Math.sin(index * 0.5) * 8;
    canyonWidth = Math.max(14, Math.min(35, canyonWidth));

    const canyonHeight = CONFIG.CANYON_HEIGHT_BASE + Math.cos(index * 0.4) * 10;
    const centerOffsetX = Math.sin(index * 0.25) * 6;
    const centerOffsetY = Math.cos(index * 0.35) * 4;

    const terrainType = this.getTerrainType(index);
    this.generateCanyonBoundaries(chunkGroup, canyonWidth, canyonHeight, centerOffsetX, centerOffsetY, index);

    switch (terrainType) {
      case 'walls': this.generateWallObstacles(chunkGroup, canyonWidth, canyonHeight, centerOffsetX, centerOffsetY, index); break;
      case 'monoliths': this.generateMonolithField(chunkGroup, canyonWidth, canyonHeight, centerOffsetX, centerOffsetY, index); break;
      case 'squeeze': this.generateSqueezeZone(chunkGroup, canyonWidth * 0.55, canyonHeight, centerOffsetX, centerOffsetY, index); break;
    }

    this.generateOrbs(chunkGroup, canyonWidth, canyonHeight, centerOffsetX, centerOffsetY, terrainType, index);
    this.scene.add(chunkGroup);
    this.chunks.set(index, { group: chunkGroup, z: -z, type: terrainType });
  }

  getTerrainType(index) {
    const r = this.rng();
    if (index % 6 === 0 && this.difficulty > 1.3) return 'squeeze';
    if (r < 0.45) return 'walls';
    if (r < 0.75) return 'monoliths';
    return 'walls';
  }

  generateCanyonBoundaries(group, width, height, offsetX, offsetY, index) {
    const wallHeight = height * 1.8;
    const halfWidth = width / 2 + Math.abs(offsetX);
    
    this.createCanyonWall(group, -halfWidth - 3 + offsetX, wallHeight, offsetY, index, 'left');
    this.createCanyonWall(group, halfWidth + 3 + offsetX, wallHeight, offsetY, index, 'right');

    // Floor
    const floorGeo = new THREE.PlaneGeometry(width * 2, CONFIG.CHUNK_LENGTH, 6, 4);
    const fPos = floorGeo.attributes.position;
    for (let i = 0; i < fPos.count; i++) fPos.setZ(i, (this.rng() - 0.5) * 1.2);
    floorGeo.computeVertexNormals();
    
    const floor = new THREE.Mesh(floorGeo, this.floorMat);
    floor.rotation.x = -Math.PI / 2;
    floor.position.set(offsetX, offsetY - height * 0.35, 0);
    group.add(floor);

    // Ceiling (occasionally)
    if (this.rng() > 0.5 && this.difficulty > 1.2) {
      const ceilGeo = new THREE.PlaneGeometry(width * 1.8, CONFIG.CHUNK_LENGTH, 4, 3);
      const cPos = ceilGeo.attributes.position;
      for (let i = 0; i < cPos.count; i++) cPos.setZ(i, (this.rng() - 0.5) * 2);
      ceilGeo.computeVertexNormals();
      
      const ceiling = new THREE.Mesh(ceilGeo, this.wallMaterials[index % 3]);
      ceiling.rotation.x = Math.PI / 2;
      ceiling.position.set(offsetX, height + offsetY - 2, 0);
      group.add(ceiling);
    }

    group.userData.wallBounds = { 
      minX: -halfWidth + offsetX, maxX: halfWidth + offsetX,
      minY: offsetY - height * 0.35, maxY: wallHeight + offsetY 
    };
  }

  createCanyonWall(group, xPos, wallHeight, yOffset, index, side) {
    const segments = 8;
    const segLength = CONFIG.CHUNK_LENGTH / segments;
    
    for (let i = 0; i < segments; i++) {
      const zPos = -CONFIG.CHUNK_LENGTH * 0.5 + i * segLength;
      const segHeight = wallHeight * (0.6 + this.rng() * 0.8);
      const segWidth = 3 + this.rng() * 4;
      const lean = (this.rng() - 0.5) * 2;
      
      const geo = new THREE.BoxGeometry(segWidth, segHeight, segLength + 1);
      const posArr = geo.attributes.position;
      for (let j = 0; j < posArr.count; j++) {
        const y = posArr.getY(j);
        const topFactor = Math.max(0, y / (segHeight * 0.5));
        posArr.setX(j, posArr.getX(j) + lean * topFactor + (this.rng() - 0.5) * segWidth * 0.15);
        posArr.setZ(j, posArr.getZ(j) + (this.rng() - 0.5) * segLength * 0.2);
      }
      geo.computeVertexNormals();

      const mat = this.wallMaterials[(index + i) % 3];
      const mesh = new THREE.Mesh(geo, mat);
      
      // FIX: use wallHeight not height
      const yPos = yOffset + segHeight / 2 - wallHeight * 0.1;
      mesh.position.set(xPos, yPos, zPos);
      
      if (this.rng() > 0.4) {
        const edgeGeo = new THREE.EdgesGeometry(geo, 30);
        const edges = new THREE.LineSegments(edgeGeo, this.edgeMat.clone());
        edges.material.opacity = 0.2 + this.rng() * 0.3;
        mesh.add(edges);
      }

      mesh.userData.collisionBox = {
        minX: xPos - segWidth/2 - Math.abs(lean), maxX: xPos + segWidth/2,
        minY: yPos - segHeight/2, maxY: yPos + segHeight/2,
        minZ: zPos - segLength/2, maxZ: zPos + segLength/2
      };

      group.add(mesh);
    }
  }

  generateWallObstacles(group, width, height, offsetX, offsetY, index) {
    const numObs = Math.floor(2 + this.difficulty * 1.5);
    
    for (let i = 0; i < numObs; i++) {
      const zPos = -CONFIG.CHUNK_LENGTH * 0.7 + (i / numObs) * CONFIG.CHUNK_LENGTH * 1.4;
      const side = this.rng() > 0.5 ? 1 : -1;
      const halfWidth = width / 2;
      const protrudeAmount = (this.rng() * 0.35 + 0.08) * halfWidth;
      
      const wallW = 2 + this.rng() * 4;
      const wallH = height * (0.3 + this.rng() * 0.5);
      const wallD = CONFIG.CHUNK_LENGTH * (0.15 + this.rng() * 0.15);
      
      const geo = new THREE.BoxGeometry(wallW, wallH, wallD, 2, 2, 2);
      const posArr = geo.attributes.position;
      for (let j = 0; j < posArr.count; j++) {
        posArr.setX(j, posArr.getX(j) + (this.rng() - 0.5) * wallW * 0.15);
        posArr.setY(j, posArr.getY(j) + (this.rng() - 0.5) * wallH * 0.12);
      }
      geo.computeVertexNormals();

      const mat = this.wallMaterials[index % 3];
      const mesh = new THREE.Mesh(geo, mat);
      
      const xPos = side * (halfWidth - protrudeAmount + offsetX);
      const yPos = offsetY + wallH / 2;
      mesh.position.set(xPos, yPos, zPos);

      if (this.rng() > 0.3) {
        const edgeGeo = new THREE.EdgesGeometry(geo, 25);
        const edges = new THREE.LineSegments(edgeGeo, this.edgeMat.clone());
        mesh.add(edges);
      }

      mesh.userData.collisionBox = {
        minX: xPos - wallW/2 - 0.3, maxX: xPos + wallW/2 + 0.3,
        minY: yPos - wallH/2, maxY: yPos + wallH/2,
        minZ: zPos - wallD/2, maxZ: zPos + wallD/2
      };

      group.add(mesh);
    }
  }

  generateMonolithField(group, width, height, offsetX, offsetY, index) {
    const numM = Math.floor(3 + this.difficulty * 2);
    
    for (let i = 0; i < numM; i++) {
      const zPos = -CONFIG.CHUNK_LENGTH * 0.6 + (i / numM) * CONFIG.CHUNK_LENGTH * 1.2;
      const mType = Math.floor(this.rng() * 3);
      let geo;
      
      switch(mType) {
        case 0: geo = new THREE.BoxGeometry(2+this.rng()*2, height*(0.4+this.rng()*0.5), 2+this.rng()*2); break;
        case 1: { const s=1.5+this.rng()*2; geo=new THREE.OctahedronGeometry(s,0); geo.scale(1,height/(s*2)*(0.4+this.rng()*0.3),1); } break;
        case 2: { const ds=1+this.rng()*1.5; geo=new THREE.DodecahedronGeometry(ds,0); geo.scale(1,height/(ds*2)*(0.3+this.rng()*0.4),1); } break;
      }

      const mat = this.monolithMats[index % 3];
      const mesh = new THREE.Mesh(geo, mat);
      
      const xPos = (this.rng() - 0.5) * width * 0.6 + offsetX;
      const yPos = offsetY + height * 0.15 + this.rng() * height * 0.35;
      mesh.position.set(xPos, yPos, zPos);
      
      mesh.rotation.y = this.rng() * Math.PI;
      mesh.rotation.z = (this.rng() - 0.5) * 0.15;

      const edgeGeo = new THREE.EdgesGeometry(geo, 20);
      const edges = new THREE.LineSegments(edgeGeo, this.edgeMat.clone());
      mesh.add(edges);

      if (this.rng() > 0.5) {
        const pLight = new THREE.PointLight(0xff6a00, 0.3, 8);
        mesh.add(pLight);
      }

      let halfW = 2;
      try { geo.computeBoundingSphere(); halfW = Math.max(geo.boundingSphere.radius, 1.5) + 0.5; } catch(e) {}
      
      const halfH = (height * 0.4) / 2 + 0.5;
      mesh.userData.collisionBox = {
        minX: xPos - halfW, maxX: xPos + halfW,
        minY: yPos - halfH, maxY: yPos + halfH,
        minZ: zPos - halfW * 0.7, maxZ: zPos + halfW * 0.7
      };

      group.add(mesh);
    }
  }

  generateSqueezeZone(group, width, height, offsetX, offsetY, index) {
    const squeezeCenter = -CONFIG.CHUNK_LENGTH * 0.3;
    
    for (let side = -1; side <= 1; side += 2) {
      const segments = 10;
      for (let i = 0; i < segments; i++) {
        const t = i / segments;
        const zPos = squeezeCenter + (t - 0.5) * CONFIG.CHUNK_LENGTH * 0.7;
        const convergence = Math.sin(t * Math.PI);
        const wallX = side * (width / 2 - convergence * width * 0.35 + offsetX);
        
        const blockW = 1.5 + this.rng();
        const blockH = height * (0.6 + this.rng() * 0.4);
        const blockD = CONFIG.CHUNK_LENGTH * 0.7 / segments + 1;
        
        const geo = new THREE.BoxGeometry(blockW, blockH, blockD, 2, 3, 2);
        const posArr = geo.attributes.position;
        for (let j = 0; j < posArr.count; j++) {
          posArr.setX(j, posArr.getX(j) + (this.rng() - 0.5) * blockW * 0.1);
          posArr.setY(j, posArr.getY(j) + (this.rng() - 0.5) * blockH * 0.08);
        }
        geo.computeVertexNormals();

        const mat = this.wallMaterials[(index + i) % 3];
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.set(wallX, offsetY + blockH / 2 - height * 0.1, zPos);

        const edgeGeo = new THREE.EdgesGeometry(geo, 25);
        const edges = new THREE.LineSegments(edgeGeo, this.edgeMat.clone());
        edges.material.opacity = 0.4 + convergence * 0.4;
        mesh.add(edges);

        mesh.userData.collisionBox = {
          minX: wallX - blockW/2, maxX: wallX + blockW/2,
          minY: offsetY - height * 0.1, maxY: offsetY + blockH - height * 0.1,
          minZ: zPos - blockD/2, maxZ: zPos + blockD/2
        };

        group.add(mesh);
      }
    }

    // Tight ceiling at squeeze point
    const ceilGeo = new THREE.BoxGeometry(width * 0.5, 1.5, CONFIG.CHUNK_LENGTH * 0.35);
    const ceilMat = this.wallMaterials[index % 3];
    const ceiling = new THREE.Mesh(ceilGeo, ceilMat);
    ceiling.position.set(offsetX, offsetY + height * 0.65, squeezeCenter);
    
    const cEdgeGeo = new THREE.EdgesGeometry(ceilGeo, 20);
    const cEdges = new THREE.LineSegments(cEdgeGeo, this.edgeMat.clone());
    cEdges.material.opacity = 0.5;
    ceiling.add(cEdges);

    ceiling.userData.collisionBox = {
      minX: offsetX - width * 0.25, maxX: offsetX + width * 0.25,
      minY: offsetY + height * 0.6, maxY: offsetY + height * 0.75,
      minZ: squeezeCenter - CONFIG.CHUNK_LENGTH * 0.18, maxZ: squeezeCenter + CONFIG.CHUNK_LENGTH * 0.18
    };

    group.add(ceiling);
    
    // Warning glow at entrance
    const warnGeo = new THREE.RingGeometry(2, 3, 6);
    const warnMat = new THREE.MeshBasicMaterial({ color: 0xff4500, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
    const warning = new THREE.Mesh(warnGeo, warnMat);
    warning.position.set(offsetX, offsetY + height * 0.3, squeezeCenter + CONFIG.CHUNK_LENGTH * 0.4);
    group.add(warning);
  }

  generateOrbs(group, width, height, offsetX, offsetY, terrainType, index) {
    const numOrbs = terrainType === 'squeeze' ? 4 : Math.floor(2 + this.rng() * 3);
    
    for (let i = 0; i < numOrbs; i++) {
      const zPos = -CONFIG.CHUNK_LENGTH * 0.5 + (i / numOrbs) * CONFIG.CHUNK_LENGTH;
      
      let xPos, yPos;
      if (this.rng() > 0.4 && terrainType === 'squeeze') {
        xPos = offsetX + (this.rng() - 0.5) * width * 0.85;
      } else {
        xPos = (this.rng() - 0.5) * width * 0.6 + offsetX;
      }
      yPos = offsetY + height * 0.2 + this.rng() * height * 0.4;
      
      const orbGeo = new THREE.IcosahedronGeometry(0.6, 0);
      const orbMat = new THREE.MeshBasicMaterial({ color: COLORS.orb, transparent: true, opacity: 0.9 });
      const orb = new THREE.Mesh(orbGeo, orbMat);
      orb.position.set(xPos, yPos, zPos);
      
      // Glow sprite
      const gc = document.createElement('canvas');
      gc.width = 64; gc.height = 64;
      const gctx = gc.getContext('2d');
      const grd = gctx.createRadialGradient(32, 32, 0, 32, 32, 32);
      grd.addColorStop(0, 'rgba(0,255,200,0.8)');
      grd.addColorStop(0.4, 'rgba(0,255,200,0.3)');
      grd.addColorStop(1, 'rgba(0,255,200,0)');
      gctx.fillStyle = grd;
      gctx.fillRect(0, 0, 64, 64);
      
      const glowTex = new THREE.CanvasTexture(gc);
      const glowMat = new THREE.SpriteMaterial({ map: glowTex, transparent: true, blending: THREE.AdditiveBlending });
      const glowSprite = new THREE.Sprite(glowMat);
      glowSprite.scale.set(3.5, 3.5, 1);
      orb.add(glowSprite);
      
      const orbLight = new THREE.PointLight(COLORS.orb, 0.6, 8);
      orb.add(orbLight);
      
      orb.userData.isOrb = true;
      orb.userData.collected = false;
      orb.userData.baseY = yPos;
      
      group.add(orb);
    }
  }

  update(playerZ) {
    const currentChunkIndex = Math.floor((playerZ + CONFIG.CHUNK_LENGTH * 2) / CONFIG.CHUNK_LENGTH);
    
    for (let i = this.chunkIndex; i <= currentChunkIndex + CONFIG.CHUNKS_AHEAD; i++) {
      if (!this.chunks.has(i)) this.generateChunk(i);
    }
    
    const removeThreshold = currentChunkIndex - CONFIG.CHUNKS_BEHIND;
    for (const [index, chunk] of this.chunks) {
      if (index < removeThreshold) {
        this.scene.remove(chunk.group);
        
        // Dispose geometries and non-shared materials
        chunk.group.traverse((child) => {
          if (child.geometry) child.geometry.dispose();
          if (child.material && !this.sharedMaterials.has(child.material)) {
            if (Array.isArray(child.material)) {
              child.material.forEach(m => m.dispose());
            } else {
              child.material.dispose();
            }
          }
        });
        
        this.chunks.delete(index);
      }
    }
    
    this.chunkIndex = Math.max(this.chunkIndex, currentChunkIndex + CONFIG.CHUNKS_AHEAD);
  }

  getCollisionBoxes(playerZ) {
    const boxes = [];
    for (const [index, chunk] of this.chunks) {
      if (Math.abs(chunk.z - playerZ) > CONFIG.CHUNK_LENGTH * 3) continue;
      
      chunk.group.traverse((child) => {
        if (child.userData && child.userData.collisionBox) {
          boxes.push({
            minX: child.userData.collisionBox.minX,
            maxX: child.userData.collisionBox.maxX,
            minY: child.userData.collisionBox.minY,
            maxY: child.userData.collisionBox.maxY,
            minZ: child.userData.collisionBox.minZ + chunk.z,
            maxZ: child.userData.collisionBox.maxZ + chunk.z
          });
        }
      });
    }
    return boxes;
  }

  getOrbs(playerZ) {
    const orbs = [];
    for (const [index, chunk] of this.chunks) {
      if (Math.abs(chunk.z - playerZ) > CONFIG.CHUNK_LENGTH * 3) continue;
      
      chunk.group.traverse((child) => {
        if (child.userData && child.userData.isOrb && !child.userData.collected) {
          orbs.push({
            mesh: child,
            worldPos: new THREE.Vector3(child.position.x, child.position.y, child.position.z + chunk.z)
          });
        }
      });
    }
    return orbs;
  }

  collectOrb(orbData) {
    if (orbData.mesh.userData.collected) return false;
    orbData.mesh.userData.collected = true;
    
    const mesh = orbData.mesh;
    let scale = 1;
    const animateCollection = () => {
      scale -= 0.15;
      if (scale <= 0) { mesh.visible = false; return; }
      mesh.scale.set(scale, scale, scale);
      requestAnimationFrame(animateCollection);
    };
    animateCollection();
    
    return true;
  }

  updateDifficulty(score) {
    this.difficulty = Math.min(CONFIG.MAX_DIFFICULTY, 1.0 + score * CONFIG.DIFFICULTY_SCALE_RATE);
  }
}

// ── class: Game ──
class Game {
  constructor() {
    this.canvas = document.getElementById('game-canvas');
    this.renderer = null;
    this.scene = null;
    this.camera = null;
    this.player = null;
    this.chunkManager = null;
    this.particleSystem = null;
    this.bloomManager = null;
    this.speedLines = null;
    this.audioSystem = new AudioSystem();
    this.gameState = new GameState();
    this.hudManager = new HUDManager();
    this.inputHandler = new InputHandler();
    this.collisionDetector = new CollisionDetector();
    this.cameraController = null;
    
    this.clock = new THREE.Clock();
    this.isRunning = false;
    this.sunLight = null;

    this.initThreeJS();
    this.setupScene();
    this.hudManager.showStartScreen();
  }

  initThreeJS() {
    this.renderer = new THREE.WebGLRenderer({ 
      canvas: this.canvas, antialias: true, alpha: false, 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.3;

    this.camera = new THREE.PerspectiveCamera(
      CONFIG.FOV_NORMAL, window.innerWidth / window.innerHeight, 0.1, 500
    );

    this.scene = new THREE.Scene();
    this.scene.fog = new THREE.FogExp2(0x1a0033, 0.006);

    window.addEventListener('resize', () => this.onResize());
  }

  setupScene() {
    // Sky gradient sphere
    const skyGeo = new THREE.SphereGeometry(250, 16, 12);
    const skyMat = new THREE.ShaderMaterial({
      uniforms: {
        topColor: { value: COLORS.skyTop },
        bottomColor: { value: COLORS.skyBottom },
        offset: { value: 20 },
        exponent: { value: 0.4 }
      },
      vertexShader: `
        varying vec3 vWorldPosition;
        void main() {
          vec4 worldPos = modelMatrix * vec4(position, 1.0);
          vWorldPosition = worldPos.xyz;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform vec3 topColor;
        uniform vec3 bottomColor;
        uniform float offset;
        uniform float exponent;
        varying vec3 vWorldPosition;
        void main() {
          float h = normalize(vWorldPosition + offset).y;
          gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
        }
      `,
      side: THREE.BackSide, depthWrite: false
    });
    this.scene.add(new THREE.Mesh(skyGeo, skyMat));

    // Sun visual
    const sunGeo = new THREE.CircleGeometry(15, 16);
    const sunMat = new THREE.MeshBasicMaterial({ color: COLORS.sunColor, transparent: true, opacity: 0.8 });
    this.sunMesh = new THREE.Mesh(sunGeo, sunMat);
    this.sunMesh.position.set(0, 30, -200);
    this.scene.add(this.sunMesh);

    // Sun glow
    const gc = document.createElement('canvas');
    gc.width = 128; gc.height = 128;
    const gctx = gc.getContext('2d');
    const grd = gctx.createRadialGradient(64, 64, 0, 64, 64, 64);
    grd.addColorStop(0, 'rgba(255,170,50,0.6)');
    grd.addColorStop(0.3, 'rgba(255,100,30,0.2)');
    grd.addColorStop(1, 'rgba(255,50,0,0)');
    gctx.fillStyle = grd;
    gctx.fillRect(0, 0, 128, 128);
    
    const glowTex = new THREE.CanvasTexture(gc);
    this.sunGlow = new THREE.Sprite(new THREE.SpriteMaterial({ 
      map: glowTex, transparent: true, blending: THREE.AdditiveBlending 
    }));
    this.sunGlow.scale.set(60, 60, 1);
    this.sunMesh.add(this.sunGlow);

    // Lighting setup
    const ambient = new THREE.AmbientLight(0x4a2080, 0.5);
    this.scene.add(ambient);

    this.sunLight = new THREE.DirectionalLight(0xffaa33, 1.5);
    this.sunLight.position.set(10, 40, -50);
    this.sunLight.castShadow = true;
    this.sunLight.shadow.mapSize.width = 2048;
    this.sunLight.shadow.mapSize.height = 2048;
    this.sunLight.shadow.camera.near = 0.5;
    this.sunLight.shadow.camera.far = 300;
    this.sunLight.shadow.camera.left = -60;
    this.sunLight.shadow.camera.right = 60;
    this.sunLight.shadow.camera.top = 40;
    this.sunLight.shadow.camera.bottom = -20;
    this.scene.add(this.sunLight);

    const rimLight = new THREE.DirectionalLight(0x9b59b6, 0.3);
    rimLight.position.set(-10, 20, 30);
    this.scene.add(rimLight);

    const hemiLight = new THREE.HemisphereLight(0xff8c32, 0x4a2080, 0.3);
    this.scene.add(hemiLight);

    // Player craft
    this.player = new Player();
    this.scene.add(this.player.mesh);

    // Chunk manager for procedural generation
    this.chunkManager = new ChunkManager(this.scene);

    // Particle systems
    this.particleSystem = new ParticleSystem(this.scene);
    
    // Bloom/glow effects
    this.bloomManager = new BloomManager(this.scene);
    
    // Speed lines effect
    this.speedLines = new SpeedLines(this.scene);

    // Camera controller
    this.cameraController = new CameraController(this.camera);
  }

  start() {
    this.audioSystem.init();
    this.gameState.reset();
    this.gameState.isRunning = true;
    
    // Start player well behind the first obstacle chunks
    const startZ = -CONFIG.CHUNK_LENGTH * 3;
    this.player.position.set(0, 5, startZ);
    this.player.velocity.set(0, 0, 0);
    this.player.speed = CONFIG.BASE_SPEED;
    this.player.currentBankAngle = 0;
    this.player.currentPitchAngle = 0;

    // Clear old chunks
    for (const [index, chunk] of this.chunkManager.chunks) {
      this.scene.remove(chunk.group);
      chunk.group.traverse((child) => {
        if (child.geometry) child.geometry.dispose();
        if (child.material && !this.chunkManager.sharedMaterials.has(child.material)) {
          if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
          else child.material.dispose();
        }
      });
    }
    this.chunkManager.chunks.clear();
    this.chunkManager.chunkIndex = 0;

    // Generate initial chunks - start from chunk that's ahead of player
    const firstChunkIdx = Math.floor((startZ + CONFIG.CHUNK_LENGTH * 2) / CONFIG.CHUNK_LENGTH);
    for (let i = firstChunkIdx - 1; i <= firstChunkIdx + CONFIG.CHUNKS_AHEAD + 2; i++) {
      this.chunkManager.generateChunk(i);
    }

    // Position camera behind player
    this.camera.position.set(0, CONFIG.CAMERA_HEIGHT + 5, startZ + CONFIG.CAMERA_DISTANCE);

    this.hudManager.showHUD();
    this.isRunning = true;
    
    if (!this._loopStarted) {
      this._loopStarted = true;
      this.gameLoop();
    }
  }

  gameLoop() {
    requestAnimationFrame(() => this.gameLoop());
    
    const dt = Math.min(this.clock.getDelta(), 0.05);
    
    if (this.gameState.isRunning && !this.gameState.isGameOver) {
      this.update(dt);
    }
    
    this.render();
  }

  update(dt) {
    const gs = this.gameState;
    const input = this.inputHandler;
    const time = performance.now() * 0.001;

    // Handle boost
    let boosting = false;
    if (input.boost && gs.isBoosting()) {
      if (!gs._boostActive) {
        this.audioSystem.playBoost();
        this.cameraController.addShake(0.5);
      }
      gs.activateBoost();
      boosting = true;
    }
    boosting = gs.updateBoost(dt) || boosting;

    // Update player speed with difficulty scaling
    const diffSpeedBonus = (this.chunkManager.difficulty - 1) * 0.3;
    const targetSpeed = boosting ? CONFIG.BOOST_SPEED : 
                        Math.min(CONFIG.MAX_SPEED, CONFIG.BASE_SPEED + gs.distance * 0.0001 + diffSpeedBonus);
    
    this.player.speed += (targetSpeed - this.player.speed) * dt * (boosting ? 4 : 2);

    // Move player forward
    const moveZ = this.player.speed * dt * 60;
    this.player.position.z -= moveZ;

    // Update player physics
    this.player.update(input, dt);

    // Procedural generation
    this.chunkManager.update(this.player.position.z);
    this.chunkManager.updateDifficulty(gs.score);

    // Collision detection
    const collisionBoxes = this.chunkManager.getCollisionBoxes(this.player.position.z);
    const collisionResult = this.collisionDetector.checkCollision(
      this.player.position, collisionBoxes
    );

    // Actual collision → game over
    if (collisionResult.collided) {
      gs.triggerGameOver();
      this.audioSystem.playCrash();
      this.cameraController.addShake(2.0);
      this.particleSystem.emitExplosion(this.player.position.clone());
      
      setTimeout(() => this.hudManager.showGameOver(gs), 500);
    }

    // Near-miss effects
    if (collisionResult.closestDist < CONFIG.NEAR_MISS_DISTANCE && collisionResult.closestDist > 1.0) {
      const now = performance.now();
      if (now - (this._lastNearMissAudio || 0) > 300) {
        this.audioSystem.playNearMiss();
        this.cameraController.addShake(0.3);
        this._lastNearMissAudio = now;
      }
    }

    // Orb collection
    const orbs = this.chunkManager.getOrbs(this.player.position.z);
    const collectedOrbs = this.collisionDetector.checkOrbCollection(this.player.position, orbs);
    
    for (const orb of collectedOrbs) {
      if (this.chunkManager.collectOrb(orb)) {
        gs.rechargeEnergy(CONFIG.ENERGY_RECHARGE);
        this.audioSystem.playCollect();
        this.cameraController.addShake(0.15);
        
        // Add bloom flash at collection point
        this.bloomManager.createGlowSprite(0x00ffc8, 4, orb.worldPos.clone());
      }
    }

    // Update game state
    gs.update(dt, this.player.speed, collisionResult);

    // Audio engine sound
    const speedRatio = this.player.getSpeedRatio();
    this.audioSystem.updateEngine(speedRatio + (boosting ? 0.3 : 0));

    // Visual effects updates
    this.particleSystem.update(dt, this.player.position, speedRatio);
    this.speedLines.emit(this.player.position, speedRatio);
    this.bloomManager.update(time);

    // Animate orbs
    for (const [index, chunk] of this.chunkManager.chunks) {
      chunk.group.traverse((child) => {
        if (child.userData && child.userData.isOrb && !child.userData.collected) {
          child.rotation.y = time * 2;
          child.position.y = child.userData.baseY + Math.sin(time * 3 + index) * 0.5;
        }
      });
    }

    // Camera follow
    this.cameraController.update(this.player.position, speedRatio, dt);

    // Move sun and shadow light with player
    this.sunMesh.position.z = this.player.position.z - 200;
    this.sunLight.position.z = this.player.position.z - 50;
    this.sunLight.target.position.copy(this.player.position);
    this.sunLight.target.updateMatrixWorld();

    // Update HUD
    this.hudManager.update(gs, speedRatio);
  }

  render() {
    this.renderer.render(this.scene, this.camera);
  }

  onResize() {
    const w = window.innerWidth;
    const h = window.innerHeight;
    this.camera.aspect = w / h;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(w, h);
  }

  dispose() {
    this.isRunning = false;
    this.audioSystem.stop();
    this.particleSystem.dispose();
    if (this.bloomManager) this.bloomManager.dispose();
    if (this.speedLines) this.speedLines.dispose();
    
    for (const [index, chunk] of this.chunkManager.chunks) {
      this.scene.remove(chunk.group);
      chunk.group.traverse((child) => {
        if (child.geometry) child.geometry.dispose();
        if (child.material && !this.chunkManager.sharedMaterials.has(child.material)) {
          if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
          else child.material.dispose();
        }
      });
    }
    
    this.renderer.dispose();
  }
}

// ── main ──
(function() {
  let game = null;

  function init() {
    game = new Game();

    // Start button handler
    document.getElementById('start-btn').addEventListener('click', () => {
      game.start();
    });

    // Restart button handler
    document.getElementById('restart-btn').addEventListener('click', () => {
      if (game) {
        game.dispose();
      }
      init();
    });

    // Keyboard shortcut to start/restart
    window.addEventListener('keydown', (e) => {
      if (e.code === 'Enter') {
        const startScreen = document.getElementById('start-screen');
        const gameOverScreen = document.getElementById('game-over');
        
        if (!startScreen.classList.contains('hidden')) {
          game.start();
        } else if (!gameOverScreen.classList.contains('hidden')) {
          if (game) game.dispose();
          init();
        }
      }
    });
  }

  // Wait for Three.js to load then initialize
  function waitForThree() {
    if (typeof THREE !== 'undefined') {
      init();
    } else {
      setTimeout(waitForThree, 100);
    }
  }

  waitForThree();
})();
</script>
</body>
</html>