← Canyon Run results

Canyon Run

Qwen 3.6 35b a3b · composer

◈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 54,887 bytes · SHA-256 9866147def26
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CANYON RUNNER</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/AfterImageShader.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/postprocessing/AfterImagePass.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://unpkg.com/three@0.128.0/examples/js/shaders/ConvolutionShader.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0010; font-family: 'Segoe UI', system-ui, sans-serif; }
#gameCanvas { display: block; width: 100vw; height: 100vh; }
/* ── css: hud ── */
#hud { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
#score-display { position: absolute; top: 20px; right: 30px; text-align: right; }
#multiplier { font-size: 56px; font-weight: 900; color: #ff9900; text-shadow: 0 0 30px rgba(255,153,0,0.9), 0 0 60px rgba(255,100,0,0.4); letter-spacing: 3px; transition: transform 0.1s; }
#multiplier.pulse { transform: scale(1.5); color: #ffcc00; }
#score { font-size: 24px; color: #ddd; letter-spacing: 5px; margin-top: -8px; text-shadow: 0 0 10px rgba(255,255,255,0.3); }
#distance-display { position: absolute; top: 20px; left: 30px; text-align: left; }
#distance-label { font-size: 12px; color: #666; letter-spacing: 3px; text-transform: uppercase; }
#distance-value { font-size: 28px; color: #aaa; letter-spacing: 3px; text-shadow: 0 0 10px rgba(170,170,170,0.3); }
#speed-bar-container { position: absolute; bottom: 80px; left: 50%; transform: translateX(-50%); width: 300px; height: 3px; background: rgba(255,255,255,0.08); border-radius: 2px; }
#speed-bar { height: 100%; width: 0%; background: linear-gradient(90deg, #ff6600, #ffcc00); border-radius: 2px; transition: width 0.1s; box-shadow: 0 0 10px rgba(255,102,0,0.5); }
#energy-container { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); width: 200px; height: 6px; background: rgba(255,255,255,0.06); border-radius: 3px; overflow: hidden; }
#energy-bar { height: 100%; width: 100%; background: linear-gradient(90deg, #00ccff, #00ffcc); border-radius: 3px; transition: width 0.15s; box-shadow: 0 0 12px rgba(0,204,255,0.6); }
#energy-container.low #energy-bar { background: linear-gradient(90deg, #ff3300, #ff6600); box-shadow: 0 0 12px rgba(255,51,0,0.6); }
#boost-container { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); width: 150px; height: 4px; background: rgba(255,255,255,0.04); border-radius: 2px; overflow: hidden; }
#boost-bar { height: 100%; width: 0%; background: linear-gradient(90deg, #ff3300, #ff6600, #ffcc00); border-radius: 2px; transition: width 0.1s; box-shadow: 0 0 8px rgba(255,51,0,0.5); }
#boost-label { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); font-size: 10px; color: #555; letter-spacing: 4px; text-transform: uppercase; }
#near-miss-flash { position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; background: radial-gradient(ellipse at center, rgba(255,200,0,0.2) 0%, transparent 70%); transition: opacity 0.05s; pointer-events: none; }
#near-miss-flash.active { opacity: 1; }
/* ── css: overlays ── */
.hidden { display: none !important; }
#game-over { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 100; background: rgba(5,0,10,0.85); backdrop-filter: blur(8px); }
#game-over-content { text-align: center; }
#game-over-title { font-size: 72px; font-weight: 900; color: #ff3300; text-shadow: 0 0 40px rgba(255,51,0,0.6); letter-spacing: 8px; margin-bottom: 30px; }
#final-score { font-size: 28px; color: #ffcc00; letter-spacing: 4px; margin-bottom: 10px; text-shadow: 0 0 15px rgba(255,204,0,0.4); }
#max-speed { font-size: 18px; color: #888; letter-spacing: 3px; margin-bottom: 40px; }
#restart-btn { padding: 14px 50px; font-size: 20px; font-weight: 700; letter-spacing: 6px; color: #fff; background: transparent; border: 2px solid #ff6600; cursor: pointer; pointer-events: all; transition: all 0.2s; text-transform: uppercase; }
#restart-btn:hover { background: #ff6600; color: #000; box-shadow: 0 0 30px rgba(255,102,0,0.5); }
#start-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; z-index: 100; background: rgba(5,0,10,0.9); backdrop-filter: blur(8px); }
#start-content { text-align: center; }
#game-title { font-size: 64px; font-weight: 900; color: #ff9900; text-shadow: 0 0 40px rgba(255,153,0,0.6); letter-spacing: 10px; margin-bottom: 10px; }
#game-subtitle { font-size: 18px; color: #888; letter-spacing: 4px; margin-bottom: 40px; }
#controls-info { margin-bottom: 40px; color: #666; font-size: 14px; line-height: 2.2; }
.key { display: inline-block; padding: 2px 10px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; font-size: 12px; color: #aaa; margin: 0 2px; vertical-align: middle; }
#start-btn { padding: 14px 50px; font-size: 20px; font-weight: 700; letter-spacing: 6px; color: #fff; background: transparent; border: 2px solid #ff6600; cursor: pointer; pointer-events: all; transition: all 0.2s; text-transform: uppercase; }
#start-btn:hover { background: #ff6600; color: #000; box-shadow: 0 0 30px rgba(255,102,0,0.5); }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="hud">
  <div id="score-display">
    <div id="multiplier">x1</div>
    <div id="score">0</div>
  </div>
  <div id="speed-bar-container">
    <div id="speed-bar"></div>
  </div>
  <div id="energy-container">
    <div id="energy-bar"></div>
  </div>
  <div id="boost-container">
    <div id="boost-bar"></div>
  </div>
  <div id="near-miss-flash"></div>
<div id="distance-display">
  <div id="distance-label">DISTANCE</div>
  <div id="distance-value">0m</div>
</div>
<div id="boost-label">BOOST</div></div>
<div id="game-over" class="hidden">
  <div id="game-over-content">
    <h1 id="game-over-title">WRECKED</h1>
    <div id="final-score">SCORE: 0</div>
    <div id="max-speed">MAX SPEED: 0</div>
    <button id="restart-btn">RESTART</button>
  </div>
</div>
<div id="start-screen">
  <div id="start-content">
    <h1 id="game-title">CANYON RUN</h1>
    <p id="game-subtitle">Navigate the infinite canyon</p>
    <div id="controls-info">
      <div><span class="key">A</span><span class="key">D</span> or <span class="key">←</span><span class="key">→</span> — Steer</div>
      <div><span class="key">W</span><span class="key">S</span> or <span class="key">↑</span><span class="key">↓</span> — Pitch</div>
      <div><span class="key">SPACE</span> — Boost (when charged)</div>
    </div>
    <button id="start-btn">START</button>
  </div>
</div>
<script>
'use strict';

// ── const: CFG ──
const CFG = {
  CHUNK_SIZE: 200,
  CHUNK_COUNT: 8,
  CANYON_WIDTH: 120,
  CANYON_HEIGHT: 80,
  BASE_SPEED: 80,
  MAX_SPEED: 250,
  BOOST_SPEED: 400,
  PLAYER_SIZE: 2.5,
  PLAYER_START_Y: 10,
  ENERGY_MAX: 100,
  ENERGY_DECAY: 3.5,
  ENERGY_RECHARGE: 25,
  BOOST_MAX: 100,
  BOOST_DRAIN: 40,
  BOOST_REGEN: 8,
  BOOST_DURATION: 3,
  NEAR_MISS_THRESHOLD: 8,
  NEAR_MISS_MAX: 35,
  NEAR_MISS_DECAY: 0.3,
  SCROLL_SPEED: 600,
  FOV_BASE: 65,
  FOV_BOOST: 90,
  PARTICLE_COUNT: 600,
  TRAIL_COUNT: 200,
};

// ── let: audioCtx ──
let audioCtx;

// ── let: masterGain ──
let masterGain;

// ── let: engineOsc ──
let engineOsc;

// ── let: engineGain ──
let engineGain;

// ── function: initAudio ──
function initAudio() {
  audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  masterGain = audioCtx.createGain();
  masterGain.gain.value = 0.3;
  masterGain.connect(audioCtx.destination);
  engineOsc = audioCtx.createOscillator();
  engineGain = audioCtx.createGain();
  engineOsc.type = 'sawtooth';
  engineOsc.frequency.value = 55;
  engineGain.gain.value = 0.08;
  const engineFilter = audioCtx.createBiquadFilter();
  engineFilter.type = 'lowpass';
  engineFilter.frequency.value = 300;
  engineOsc.connect(engineFilter);
  engineFilter.connect(engineGain);
  engineGain.connect(masterGain);
  engineOsc.start();
}

// ── function: updateEngineSound ──
function updateEngineSound(speed, boostActive) {
  if (!engineOsc) return;
  const freq = 55 + (speed / CFG.MAX_SPEED) * 120;
  engineOsc.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.1);
  const vol = 0.05 + (speed / CFG.MAX_SPEED) * 0.08 + (boostActive ? 0.06 : 0);
  engineGain.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.1);
}

// ── function: playCollectSound ──
function playCollectSound() {
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = 'sine';
  osc.frequency.setValueAtTime(880, audioCtx.currentTime);
  osc.frequency.exponentialRampToValueAtTime(1760, audioCtx.currentTime + 0.15);
  gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
  osc.connect(gain);
  gain.connect(masterGain);
  osc.start();
  osc.stop(audioCtx.currentTime + 0.3);
}

// ── function: playNearMissSound ──
function playNearMissSound(multiplier) {
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = 'triangle';
  const baseFreq = 400 + multiplier * 80;
  osc.frequency.setValueAtTime(baseFreq, audioCtx.currentTime);
  osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.5, audioCtx.currentTime + 0.1);
  gain.gain.setValueAtTime(0.12, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2);
  osc.connect(gain);
  gain.connect(masterGain);
  osc.start();
  osc.stop(audioCtx.currentTime + 0.2);
}

// ── function: playBoostSound ──
function playBoostSound() {
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();
  osc.type = 'sawtooth';
  osc.frequency.setValueAtTime(100, audioCtx.currentTime);
  osc.frequency.exponentialRampToValueAtTime(800, audioCtx.currentTime + 0.3);
  gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
  osc.connect(gain);
  gain.connect(masterGain);
  osc.start();
  osc.stop(audioCtx.currentTime + 0.4);
}

// ── function: playCrashSound ──
function playCrashSound() {
  const bufferSize = audioCtx.sampleRate * 0.5;
  const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < bufferSize; i++) {
    data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
  }
  const source = audioCtx.createBufferSource();
  source.buffer = buffer;
  const gain = audioCtx.createGain();
  gain.gain.value = 0.3;
  source.connect(gain);
  gain.connect(masterGain);
  source.start();
}

// ── const: canyonChunks ──
const canyonChunks = [];

// ── const: activeObstacles ──
const activeObstacles = [];

// ── const: activeCollectibles ──
const activeCollectibles = [];

// ── let: chunkSeed ──
let chunkSeed = 0;

// ── function: seededRandom ──
function seededRandom(seed) {
  const x = Math.sin(seed) * 43758.5453123;
  return x - Math.floor(x);
}

// ── function: initChunks ──
function initChunks(playerZ) {
  for (let i = 0; i < CFG.CHUNK_COUNT; i++) {
    const z = playerZ - CFG.CHUNK_SIZE * CFG.CHUNK_COUNT / 2 + i * CFG.CHUNK_SIZE;
    canyonChunks.push(generateCanyonChunk(z));
  }
}

// ── const: player ──
const player = {
  x: 0, y: CFG.PLAYER_START_Y, z: 0,
  vx: 0, vy: 0, vz: 0,
  roll: 0, pitch: 0,
  speed: CFG.BASE_SPEED,
  energy: CFG.ENERGY_MAX,
  boost: 0,
  boostActive: false,
  boostTimer: 0,
  multiplier: 1,
  nearMissTimer: 0,
  score: 0,
  maxSpeed: 0,
  alive: true,
};

// ── const: playerMesh ──
const playerMesh = new THREE.Group();

// ── function: updatePlayerPhysics ──
function updatePlayerPhysics(dt) {
  if (!player.alive) return;
  
  const targetSpeed = player.boostActive ? CFG.BOOST_SPEED : CFG.BASE_SPEED + (player.speed - CFG.BASE_SPEED) * 0.95;
  player.speed += (targetSpeed - player.speed) * dt * 2;
  player.speed = Math.max(CFG.BASE_SPEED, Math.min(CFG.MAX_SPEED, player.speed));
  
  if (player.speed > player.maxSpeed) player.maxSpeed = player.speed;
  
  // Energy decay
  player.energy -= CFG.ENERGY_DECAY * dt;
  if (player.energy <= 0) {
    player.energy = 0;
    player.alive = false;
    return;
  }
  
  // Boost logic
  if (keys['Space'] && player.boost >= 50 && !player.boostActive) {
    player.boostActive = true;
    player.boostTimer = CFG.BOOST_DURATION;
    playBoostSound();
  }
  
  if (player.boostActive) {
    player.boost -= CFG.BOOST_DRAIN * dt;
    player.boostTimer -= dt;
    if (player.boostTimer <= 0 || player.boost <= 0) {
      player.boostActive = false;
      player.boost = Math.max(0, player.boost);
    }
  } else {
    player.boost = Math.min(CFG.BOOST_MAX, player.boost + CFG.BOOST_REGEN * dt);
  }
  
  // Steering input with flight-based feel
  const steerForce = 200;
  const pitchForce = 100;
  let steerX = 0;
  let steerY = 0;
  
  if (keys['KeyA'] || keys['ArrowLeft']) steerX = -1;
  if (keys['KeyD'] || keys['ArrowRight']) steerX = 1;
  if (keys['KeyW'] || keys['ArrowUp']) steerY = 1;
  if (keys['KeyS'] || keys['ArrowDown']) steerY = -1;
  
  // Acceleration (weighty feel)
  player.vx += steerX * steerForce * dt;
  player.vy += steerY * pitchForce * dt;
  
  // Stronger damping for weighty feel
  const damping = 1 - 2.5 * dt;
  player.vx *= damping;
  player.vy *= damping;
  
  // Clamp max velocity
  const maxVX = 60;
  const maxVY = 40;
  player.vx = Math.max(-maxVX, Math.min(maxVX, player.vx));
  player.vy = Math.max(-maxVY, Math.min(maxVY, player.vy));
  
  // Apply velocity
  player.x += player.vx * dt;
  player.y += player.vy * dt;
  
  // Bank (roll) based on steering - more dramatic
  const targetRoll = steerX * 0.7;
  player.roll += (targetRoll - player.roll) * dt * 6;
  
  // Pitch based on vertical input
  const targetPitch = steerY * 0.35;
  player.pitch += (targetPitch - player.pitch) * dt * 6;
  
  // Clamp position to canyon bounds
  const canyonWidth = CFG.CANYON_WIDTH * (1 - Math.min(player.z / 8000, 1) * 0.5);
  const canyonHeight = CFG.CANYON_HEIGHT * (1 - Math.min(player.z / 8000, 1) * 0.35);
  const margin = 6;
  if (player.x < -canyonWidth / 2 + margin) {
    player.x = -canyonWidth / 2 + margin;
    player.vx *= -0.3;
  }
  if (player.x > canyonWidth / 2 - margin) {
    player.x = canyonWidth / 2 - margin;
    player.vx *= -0.3;
  }
  if (player.y < -canyonHeight / 2 + margin) {
    player.y = -canyonHeight / 2 + margin;
    player.vy *= -0.3;
  }
  if (player.y > canyonHeight / 2 - margin) {
    player.y = canyonHeight / 2 - margin;
    player.vy *= -0.3;
  }
  
  // Move forward
  player.z += player.speed * dt;
  
  // Update mesh
  playerMesh.position.set(player.x, player.y, player.z);
  playerMesh.rotation.z = player.roll;
  playerMesh.rotation.x = player.pitch;
  
  // Engine glow pulse
  const engines = playerMesh.children.filter(c => c.name === 'engineGlow');
  for (const glow of engines) {
    const pulse = 0.6 + Math.sin(Date.now() * 0.01) * 0.2 + (player.boostActive ? 0.4 : 0);
    glow.scale.set(pulse, pulse, 1 + player.boostActive * 3);
    glow.material.color.setHex(player.boostActive ? 0xff3300 : 0xff6600);
  }
  
  const eLight = playerMesh.getObjectByName('engineLight');
  if (eLight) {
    eLight.intensity = 1.5 + (player.speed / CFG.MAX_SPEED) * 2 + (player.boostActive ? 4 : 0);
  }
  
  // Update engine sound
  updateEngineSound(player.speed, player.boostActive);
}

// ── function: checkCollectibles ──
function checkCollectibles() {
  if (!player.alive) return;
  
  for (const chunk of canyonChunks) {
    for (const col of chunk.collectibles) {
      if (col.collected) continue;
      
      const mesh = col.mesh;
      const worldPos = new THREE.Vector3();
      mesh.getWorldPosition(worldPos);
      
      const dx = player.x - worldPos.x;
      const dy = player.y - worldPos.y;
      const dz = player.z - worldPos.z;
      const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
      
      if (dist < col.radius + 3) {
        col.collected = true;
        player.energy = Math.min(CFG.ENERGY_MAX, player.energy + CFG.ENERGY_RECHARGE);
        player.score += 50 * Math.max(1, Math.floor(player.multiplier));
        player.boost = Math.min(CFG.BOOST_MAX, player.boost + 20);
        player.multiplier = Math.min(10, player.multiplier + 0.3);
        cameraShake.intensity = 1.5;
        playCollectSound();
        
        // Remove from scene
        chunk.meshes = chunk.meshes.filter(m => m !== mesh);
        scene.remove(mesh);
      }
    }
  }
}

// ── function: checkCollisions ──
function checkCollisions() {
  if (!player.alive) return;
  
  const playerRadius = CFG.PLAYER_SIZE * 0.5;
  
  for (const chunk of canyonChunks) {
    for (const obs of chunk.obstacles) {
      const mesh = obs.mesh;
      const worldPos = new THREE.Vector3();
      mesh.getWorldPosition(worldPos);
      
      const dx = Math.abs(player.x - worldPos.x);
      const dy = Math.abs(player.y - worldPos.y);
      const dz = Math.abs(player.z - worldPos.z);
      
      // Only check obstacles nearby
      if (dz > obs.radius + 10) continue;
      
      const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
      
      if (dist < obs.radius + playerRadius) {
        player.alive = false;
        playCrashSound();
        cameraShake.intensity = 8;
        return;
      }
    }
  }
}

// ── const: particleSystem ──
const particleSystem = {
  positions: null,
  colors: null,
  velocities: null,
  sizes: null,
  ages: null,
  maxAge: null,
  count: 0,
  geometry: null,
  material: null,
  points: null,
};

// ── function: initParticles ──
function initParticles() {
  const count = CFG.PARTICLE_COUNT;
  particleSystem.count = count;
  particleSystem.positions = new Float32Array(count * 3);
  particleSystem.colors = new Float32Array(count * 3);
  particleSystem.velocities = new Float32Array(count * 3);
  particleSystem.sizes = new Float32Array(count);
  particleSystem.ages = new Float32Array(count);
  particleSystem.maxAge = new Float32Array(count);
  
  particleSystem.geometry = new THREE.BufferGeometry();
  particleSystem.geometry.setAttribute('position', new THREE.BufferAttribute(particleSystem.positions, 3));
  particleSystem.geometry.setAttribute('color', new THREE.BufferAttribute(particleSystem.colors, 3));
  particleSystem.geometry.setAttribute('size', new THREE.BufferAttribute(particleSystem.sizes, 1));
  
  particleSystem.material = new THREE.ShaderMaterial({
    uniforms: {},
    vertexShader: `
      attribute float size;
      attribute vec3 color;
      varying vec3 vColor;
      void main() {
        vColor = color;
        vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
        gl_PointSize = size * (200.0 / -mvPosition.z);
        gl_Position = projectionMatrix * mvPosition;
      }
    `,
    fragmentShader: `
      varying vec3 vColor;
      void main() {
        float d = length(gl_PointCoord - vec2(0.5));
        if (d > 0.5) discard;
        float alpha = 1.0 - smoothstep(0.0, 0.5, d);
        gl_FragColor = vec4(vColor, alpha * 0.7);
      }
    `,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  
  particleSystem.points = new THREE.Points(particleSystem.geometry, particleSystem.material);
}

// ── function: spawnParticle ──
function spawnParticle(x, y, z, vx, vy, vz, r, g, b, size, age) {
  for (let i = 0; i < particleSystem.count; i++) {
    if (particleSystem.ages[i] < particleSystem.maxAge[i]) {
      particleSystem.positions[i * 3] = x;
      particleSystem.positions[i * 3 + 1] = y;
      particleSystem.positions[i * 3 + 2] = z;
      particleSystem.velocities[i * 3] = vx;
      particleSystem.velocities[i * 3 + 1] = vy;
      particleSystem.velocities[i * 3 + 2] = vz;
      particleSystem.colors[i * 3] = r;
      particleSystem.colors[i * 3 + 1] = g;
      particleSystem.colors[i * 3 + 2] = b;
      particleSystem.sizes[i] = size;
      particleSystem.ages[i] = 0;
      particleSystem.maxAge[i] = age;
      return;
    }
  }
}

// ── function: updateParticles ──
function updateParticles(dt) {
  for (let i = 0; i < particleSystem.count; i++) {
    particleSystem.ages[i] += dt;
    if (particleSystem.ages[i] >= particleSystem.maxAge[i]) {
      particleSystem.ages[i] = particleSystem.maxAge[i] + 1;
      continue;
    }
    particleSystem.positions[i * 3] += particleSystem.velocities[i * 3] * dt;
    particleSystem.positions[i * 3 + 1] += particleSystem.velocities[i * 3 + 1] * dt;
    particleSystem.positions[i * 3 + 2] += particleSystem.velocities[i * 3 + 2] * dt;
    const lifeRatio = particleSystem.ages[i] / particleSystem.maxAge[i];
    particleSystem.sizes[i] *= (1 - lifeRatio * 0.3);
  }
  particleSystem.geometry.attributes.position.needsUpdate = true;
  particleSystem.geometry.attributes.color.needsUpdate = true;
  particleSystem.geometry.attributes.size.needsUpdate = true;
}

// ── function: emitTrail ──
function emitTrail(dt) {
  if (!player.alive) return;
  
  const trailCount = player.boostActive ? 8 : 4;
  for (let i = 0; i < trailCount; i++) {
    const offset = (Math.random() - 0.5) * 1.5;
    const p = spawnParticle(
      player.x + offset,
      player.y + (Math.random() - 0.5) * 0.5,
      player.z + 3,
      -player.speed * 0.3 + (Math.random() - 0.5) * 20,
      (Math.random() - 0.5) * 10,
      (Math.random() - 0.5) * 10,
      player.boostActive ? 1 : 0.8,
      player.boostActive ? 0.4 : 0.6,
      0,
      player.boostActive ? 3 : 2,
      player.boostActive ? 1.5 : 0.8
    );
    if (p) {
      p.color.setHex(player.boostActive ? 0xff4400 : 0xff6600);
    }
  }
}

// ── function: emitDebris ──
function emitDebris(dt) {
  if (!player.alive) return;
  
  const debrisCount = Math.floor(2 + player.speed / CFG.BASE_SPEED * 3);
  for (let i = 0; i < debrisCount; i++) {
    const angle = Math.random() * Math.PI * 2;
    const radius = 30 + Math.random() * 40;
    spawnParticle(
      player.x + Math.cos(angle) * radius,
      player.y + (Math.random() - 0.5) * 20,
      player.z + 50 + Math.random() * 30,
      -player.speed * 0.9 + (Math.random() - 0.5) * 30,
      (Math.random() - 0.5) * 20,
      (Math.random() - 0.5) * 20,
      0.3 + Math.random() * 0.2,
      0.3 + Math.random() * 0.1,
      0.35 + Math.random() * 0.1,
      0.5 + Math.random() * 1.5,
      0.5 + Math.random() * 1.0
    );
  }
}

// ── let: scene ──
let scene;

// ── let: camera ──
let camera;

// ── let: renderer ──
let renderer;

// ── let: composer ──
let composer;

// ── let: sunLight ──
let sunLight;

// ── let: ambientLight ──
let ambientLight;

// ── let: fillLight ──
let fillLight;

// ── let: clock ──
let clock;

// ── let: keys ──
let keys = {};

// ── let: gameState ──
let gameState = 'start';

// ── let: lastTime ──
let lastTime = 0;

// ── let: scoreDisplay ──
let scoreDisplay;

// ── let: multiplierDisplay ──
let multiplierDisplay;

// ── let: energyBar ──
let energyBar;

// ── let: boostBar ──
let boostBar;

// ── let: speedBar ──
let speedBar;

// ── let: gameOverScreen ──
let gameOverScreen;

// ── let: startScreen ──
let startScreen;

// ── let: nearMissFlash ──
let nearMissFlash;

// ── let: cameraShake ──
let cameraShake = { intensity: 0 };

// ── let: totalScore ──
let totalScore = 0;

// ── let: distanceScore ──
let distanceScore = 0;

// ── function: createMotionBlurPass ──
function createMotionBlurPass(intensity) {
  const copyShader = new THREE.ShaderPass(THREE.CopyShader);
  copyShader.uniforms['tDiffuse'].value = null;
  copyShader.uniforms['opacity'].value = 1.0;
  
  const motionBlurShader = {
    uniforms: {
      tDiffuse: { value: null },
      tPrevious: { value: null },
      intensity: { value: intensity },
    },
    vertexShader: `
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform sampler2D tDiffuse;
      uniform sampler2D tPrevious;
      uniform float intensity;
      varying vec2 vUv;
      void main() {
        vec4 current = texture2D(tDiffuse, vUv);
        vec4 previous = texture2D(tPrevious, vUv);
        gl_FragColor = mix(current, previous, intensity);
      }
    `,
  };
  
  const motionBlurPass = new THREE.ShaderPass(motionBlurShader);
  motionBlurPass.renderToScreen = true;
  return { pass: motionBlurPass, uniforms: motionBlurPass.uniforms };
}

// ── function: generateCanyonChunk ──
function generateCanyonChunk(zBase) {
  const chunk = {
    zBase: zBase,
    zEnd: zBase + CFG.CHUNK_SIZE,
    meshes: [],
    obstacles: [],
    collectibles: [],
  };
  
  const difficulty = Math.min(zBase / 8000, 1);
  const canyonWidth = CFG.CANYON_WIDTH * (1 - difficulty * 0.5);
  const canyonHeight = CFG.CANYON_HEIGHT * (1 - difficulty * 0.35);
  
  const wallSegments = 20;
  const wallVerts = {};
  
  // Generate wall vertex strips
  for (let side = -1; side <= 1; side += 2) {
    wallVerts[side === -1 ? 'left' : 'right'] = [];
    for (let i = 0; i <= wallSegments; i++) {
      const t = i / wallSegments;
      const z = zBase + t * CFG.CHUNK_SIZE;
      const noise1 = seededRandom(zBase * 0.01 + i * (side === -1 ? 1.3 : 2.7));
      const noise2 = seededRandom(zBase * 0.01 + i * (side === -1 ? 3.1 : 4.3));
      const offset = (noise1 - 0.5) * 40 * (1 - difficulty * 0.6);
      const topNoise = seededRandom(zBase * 0.02 + i * 5.1);
      const bottomNoise = seededRandom(zBase * 0.02 + i * 6.3);
      
      const baseX = side * canyonWidth / 2;
      const wallX = baseX + offset;
      const topY = canyonHeight / 2 + (topNoise - 0.5) * 25;
      const bottomY = -canyonHeight / 2 + (bottomNoise - 0.5) * 20;
      
      wallVerts[side === -1 ? 'left' : 'right'].push({ x: wallX, yTop: topY, yBot: bottomY, z });
    }
  }
  
  // Create wall faces with triangles
  for (let side = -1; side <= 1; side += 2) {
    const name = side === -1 ? 'left' : 'right';
    const verts = wallVerts[name];
    
    for (let i = 0; i < wallSegments; i++) {
      const a = verts[i], b = verts[i + 1];
      
      // Wall face (vertical)
      const wallGeo = new THREE.BufferGeometry();
      const wVerts = new Float32Array([
        a.x, a.yBot, a.z,   b.x, b.yBot, b.z,   b.x, b.yTop, b.z,
        a.x, a.yBot, a.z,   b.x, b.yTop, b.z,   a.x, a.yTop, a.z,
      ]);
      wallGeo.setAttribute('position', new THREE.BufferAttribute(wVerts, 3));
      wallGeo.computeVertexNormals();
      const wMat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.06 + seededRandom(zBase + i * 10) * 0.05, 0.25 + difficulty * 0.2, 0.12 + seededRandom(zBase + i * 11) * 0.08),
        side: THREE.DoubleSide,
        flatShading: true,
        roughness: 0.95,
        metalness: 0.05,
      });
      const wall = new THREE.Mesh(wallGeo, wMat);
      chunk.meshes.push(wall);
      
      // Floor
      const floorGeo = new THREE.BufferGeometry();
      const fVerts = new Float32Array([
        a.x, a.yBot, a.z,   b.x, b.yBot, b.z,   side * canyonWidth, a.yBot - 5, a.z,
        b.x, b.yBot, b.z,   side * canyonWidth, b.yBot - 5, b.z,   side * canyonWidth, b.yBot - 5, b.z,
      ]);
      floorGeo.setAttribute('position', new THREE.BufferAttribute(fVerts, 3));
      floorGeo.computeVertexNormals();
      const fMat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.07, 0.15, 0.08 + seededRandom(zBase + i * 12) * 0.05),
        side: THREE.DoubleSide,
        flatShading: true,
        roughness: 1.0,
        metalness: 0.0,
      });
      const floor = new THREE.Mesh(floorGeo, fMat);
      chunk.meshes.push(floor);
      
      // Ceiling
      const ceilGeo = new THREE.BufferGeometry();
      const cVerts = new Float32Array([
        a.x, a.yTop, a.z,   side * canyonWidth, a.yTop + 5, a.z,   b.x, b.yTop, b.z,
        b.x, b.yTop, b.z,   side * canyonWidth, b.yTop + 5, b.z,   side * canyonWidth, b.yTop + 5, b.z,
      ]);
      ceilGeo.setAttribute('position', new THREE.BufferAttribute(cVerts, 3));
      ceilGeo.computeVertexNormals();
      const cMat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.72, 0.1, 0.06 + seededRandom(zBase + i * 13) * 0.04),
        side: THREE.DoubleSide,
        flatShading: true,
        roughness: 1.0,
        metalness: 0.0,
      });
      const ceiling = new THREE.Mesh(ceilGeo, cMat);
      chunk.meshes.push(ceiling);
    }
  }
  
  // Place obstacles with more variety
  const obstacleCount = Math.floor(2 + difficulty * 5);
  for (let i = 0; i < obstacleCount; i++) {
    const oz = zBase + CFG.CHUNK_SIZE * 0.15 + seededRandom(zBase + i * 7) * CFG.CHUNK_SIZE * 0.7;
    const oType = Math.floor(seededRandom(zBase + i * 13) * 4);
    let mesh;
    
    if (oType === 0) {
      // Monolith - tall pillar
      const w = 4 + seededRandom(zBase + i * 17) * 8;
      const h = 12 + seededRandom(zBase + i * 19) * 20;
      const d = 3 + seededRandom(zBase + i * 23) * 6;
      const geo = new THREE.BoxGeometry(w, h, d);
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.68 + seededRandom(zBase + i * 29) * 0.15, 0.12, 0.18 + seededRandom(zBase + i * 31) * 0.1),
        flatShading: true,
        roughness: 0.7,
        metalness: 0.3,
      });
      mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(
        (seededRandom(zBase + i * 37) - 0.5) * canyonWidth * 0.55,
        (seededRandom(zBase + i * 41) - 0.5) * canyonHeight * 0.25,
        oz
      );
      mesh.rotation.y = seededRandom(zBase + i * 43) * Math.PI;
    } else if (oType === 1) {
      // Floating crystal
      const s = 3 + seededRandom(zBase + i * 47) * 5;
      const geo = new THREE.OctahedronGeometry(s);
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.75, 0.4, 0.25 + seededRandom(zBase + i * 53) * 0.1),
        flatShading: true,
        roughness: 0.4,
        metalness: 0.6,
      });
      mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(
        (seededRandom(zBase + i * 59) - 0.5) * canyonWidth * 0.5,
        (seededRandom(zBase + i * 61) - 0.5) * canyonHeight * 0.3,
        oz
      );
      mesh.rotation.set(seededRandom(zBase + i * 67) * Math.PI, seededRandom(zBase + i * 71) * Math.PI, 0);
      mesh.userData.rotSpeed = { x: seededRandom(zBase + i * 73) * 0.5, y: seededRandom(zBase + i * 77) * 0.5 };
    } else if (oType === 2) {
      // Narrow gate - two pillars with a gap
      const gapWidth = 8 + seededRandom(zBase + i * 83) * 6;
      const pillarW = 3 + seededRandom(zBase + i * 89) * 3;
      const pillarH = 20 + seededRandom(zBase + i * 91) * 15;
      const geo = new THREE.BoxGeometry(gapWidth, pillarH, 4);
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.08, 0.2, 0.15 + seededRandom(zBase + i * 97) * 0.08),
        flatShading: true,
        roughness: 0.9,
        metalness: 0.1,
      });
      mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(
        (seededRandom(zBase + i * 101) - 0.5) * canyonWidth * 0.3,
        0,
        oz
      );
      mesh.userData.isGate = true;
      mesh.userData.gapWidth = gapWidth;
    } else {
      // Large boulder
      const s = 4 + seededRandom(zBase + i * 107) * 7;
      const geo = new THREE.DodecahedronGeometry(s, 0);
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.07, 0.25, 0.14 + seededRandom(zBase + i * 113) * 0.08),
        flatShading: true,
        roughness: 0.95,
        metalness: 0.05,
      });
      mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(
        (seededRandom(zBase + i * 119) - 0.5) * canyonWidth * 0.45,
        (seededRandom(zBase + i * 127) - 0.5) * canyonHeight * 0.2,
        oz
      );
    }
    
    chunk.meshes.push(mesh);
    const box = new THREE.Box3().setFromObject(mesh);
    const size = box.getSize(new THREE.Vector3());
    const radius = Math.max(size.x, size.y, size.z) / 2;
    chunk.obstacles.push({
      mesh: mesh,
      box: box,
      radius: radius,
    });
  }
  
  // Place collectibles
  const collectibleCount = Math.floor(2 + seededRandom(zBase * 0.5) * 3);
  for (let i = 0; i < collectibleCount; i++) {
    const cz = zBase + CFG.CHUNK_SIZE * 0.15 + seededRandom(zBase * 0.3 + i * 99) * CFG.CHUNK_SIZE * 0.7;
    const geo = new THREE.OctahedronGeometry(1.0, 0);
    const mat = new THREE.MeshBasicMaterial({ color: 0x00ffcc });
    const collectible = new THREE.Mesh(geo, mat);
    
    // Outer glow
    const glowGeo = new THREE.OctahedronGeometry(1.8, 0);
    const glowMat = new THREE.MeshBasicMaterial({
      color: 0x00ffcc,
      transparent: true,
      opacity: 0.15,
    });
    const glow = new THREE.Mesh(glowGeo, glowMat);
    collectible.add(glow);
    
    // Inner bright core
    const coreGeo = new THREE.SphereGeometry(0.3, 6, 6);
    const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
    const core = new THREE.Mesh(coreGeo, coreMat);
    collectible.add(core);
    
    // Light
    const light = new THREE.PointLight(0x00ffcc, 2, 10);
    collectible.add(light);
    
    collectible.position.set(
      (seededRandom(zBase * 0.2 + i * 101) - 0.5) * canyonWidth * 0.5,
      (seededRandom(zBase * 0.2 + i * 103) - 0.5) * canyonHeight * 0.3,
      cz
    );
    
    chunk.meshes.push(collectible);
    const cBox = new THREE.Box3().setFromObject(collectible);
    chunk.collectibles.push({
      mesh: collectible,
      box: cBox,
      radius: 2,
      collected: false,
      baseY: collectible.position.y,
      phase: seededRandom(zBase + i * 200) * Math.PI * 2,
    });
  }
  
  return chunk;
}

// ── function: createPlayerCraft ──
function createPlayerCraft() {
  playerMesh.clear();
  
  // Main body - sleek delta wing shape
  const bodyVerts = new Float32Array([
    // Nose
    0, 0, 5,   -2, 0.3, 0,   2, 0.3, 0,
    // Nose bottom
    0, 0, 5,   2, 0.3, 0,   0, -0.3, 0,
    // Left wing
    -2, 0.3, 0,   -8, 0.1, -3,   -2, -0.2, -1,
    // Right wing
    2, 0.3, 0,   2, -0.2, -1,   8, 0.1, -3,
    // Wing tips left
    -8, 0.1, -3,   -8, 0.3, -2,   -6, 0.1, -4,
    // Wing tips right
    8, 0.1, -3,   6, 0.1, -4,   8, 0.3, -2,
    // Tail
    -2, -0.2, -1,   0, -0.3, 0,   2, -0.2, -1,
    // Cockpit
    0, 0.5, 2,   -1.5, 0.3, 0,   1.5, 0.3, 0,
    // Cockpit top
    0, 0.5, 2,   1.5, 0.3, 0,   0, -0.3, 0,
  ]);
  const bodyGeo = new THREE.BufferGeometry();
  bodyGeo.setAttribute('position', new THREE.BufferAttribute(bodyVerts, 3));
  bodyGeo.computeVertexNormals();
  const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd0d0d0, flatShading: true, roughness: 0.2, metalness: 0.9 });
  const body = new THREE.Mesh(bodyGeo, bodyMat);
  playerMesh.add(body);
  
  // Cockpit glass
  const cockpitGeo = new THREE.BufferGeometry();
  const cockpitVerts = new Float32Array([
    0, 0.8, 3,   -1, 0.5, 1,   1, 0.5, 1,
    0, 0.8, 3,   1, 0.5, 1,   0, 0.3, 1,
  ]);
  cockpitGeo.setAttribute('position', new THREE.BufferAttribute(cockpitVerts, 3));
  cockpitGeo.computeVertexNormals();
  const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x44aaff, flatShading: true, roughness: 0.1, metalness: 0.5, transparent: true, opacity: 0.7 });
  const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
  playerMesh.add(cockpit);
  
  // Engine glow left
  const engineGeo = new THREE.ConeGeometry(0.6, 2.5, 6);
  engineGeo.rotateX(Math.PI / 2);
  const engineMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.9 });
  const engineL = new THREE.Mesh(engineGeo, engineMat);
  engineL.position.set(-1, 0, 2);
  engineL.name = 'engineGlow';
  playerMesh.add(engineL);
  
  const engineR = new THREE.Mesh(engineGeo.clone(), engineMat.clone());
  engineR.position.set(1, 0, 2);
  engineR.name = 'engineGlow';
  playerMesh.add(engineR);
  
  // Engine lights
  const eLight = new THREE.PointLight(0xff6600, 3, 20);
  eLight.position.set(0, 0, 3);
  eLight.name = 'engineLight';
  playerMesh.add(eLight);
  
  // Wing tip lights
  const tipGeo = new THREE.SphereGeometry(0.15, 6, 6);
  const tipL = new THREE.Mesh(tipGeo, new THREE.MeshBasicMaterial({ color: 0xff0000 }));
  tipL.position.set(-8, 0.1, -3);
  playerMesh.add(tipL);
  const tipR = new THREE.Mesh(tipGeo.clone(), new THREE.MeshBasicMaterial({ color: 0x00ff00 }));
  tipR.position.set(8, 0.1, -3);
  playerMesh.add(tipR);
}

// ── function: checkNearMisses ──
function checkNearMisses(dt) {
  if (!player.alive) return;
  
  let closestDist = Infinity;
  
  for (const chunk of canyonChunks) {
    for (const obs of chunk.obstacles) {
      const mesh = obs.mesh;
      const worldPos = new THREE.Vector3();
      mesh.getWorldPosition(worldPos);
      
      const dx = Math.abs(player.x - worldPos.x);
      const dz = Math.abs(player.z - worldPos.z);
      
      if (dz < obs.radius + 15) {
        const dist = Math.sqrt(dx * dx + dz * dz);
        if (dist < obs.radius + CFG.NEAR_MISS_THRESHOLD && dist > obs.radius) {
          if (dist < closestDist) closestDist = dist;
        }
      }
    }
    
    // Check canyon walls
    for (let i = 0; i < chunk.meshes.length; i += 3) {
      const wall = chunk.meshes[i];
      if (!wall) continue;
      const wallPos = new THREE.Vector3();
      wall.getWorldPosition(wallPos);
      
      const dz = Math.abs(player.z - wallPos.z);
      if (dz > 30) continue;
      
      const dx = Math.abs(player.x - wallPos.x);
      const canyonCenterX = (wallPos.x + (wallPos.x > 0 ? -CFG.CANYON_WIDTH : CFG.CANYON_WIDTH)) / 2;
      const wallDist = Math.abs(player.x - wallPos.x);
      
      if (wallDist < CFG.NEAR_MISS_THRESHOLD + 10 && wallDist > 5) {
        if (wallDist < closestDist) closestDist = wallDist;
      }
    }
  }
  
  if (closestDist < Infinity) {
    const proximity = 1 - (closestDist / (CFG.NEAR_MISS_THRESHOLD + 10));
    const bonus = Math.floor(proximity * CFG.NEAR_MISS_MAX * player.multiplier);
    if (bonus > 0) {
      player.score += bonus;
      player.multiplier = Math.min(10, player.multiplier + proximity * 0.5);
      player.nearMissTimer = 1.0;
      player.boost = Math.min(CFG.BOOST_MAX, player.boost + proximity * 15);
      player.energy = Math.min(CFG.ENERGY_MAX, player.energy + proximity * 5);
      
      cameraShake.intensity = proximity * 2;
      playNearMissSound(proximity);
    }
  }
  
  // Decay multiplier slowly
  if (player.nearMissTimer > 0) {
    player.nearMissTimer -= dt * CFG.NEAR_MISS_DECAY;
    if (player.nearMissTimer <= 0) {
      player.nearMissTimer = 0;
      player.multiplier = Math.max(1, player.multiplier - 0.1);
    }
  }
}

// ── function: createSkyDome ──
function createSkyDome() {
  // Distant mountain silhouettes for depth
  const mountainGeo = new THREE.BufferGeometry();
  const vertices = [];
  const segments = 100;
  const radius = 400;
  
  for (let i = 0; i <= segments; i++) {
    const angle = (i / segments) * Math.PI * 2;
    const x = Math.cos(angle) * radius;
    const z = Math.sin(angle) * radius;
    const height = 30 + seededRandom(Math.floor(angle * 10) + 100) * 60;
    vertices.push(x, height, z);
  }
  
  mountainGeo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
  mountainGeo.computeVertexNormals();
  
  const mountainMat = new THREE.MeshBasicMaterial({
    color: 0x1a0520,
    side: THREE.BackSide,
    fog: false,
  });
  
  const mountain = new THREE.Mesh(mountainGeo, mountainMat);
  mountain.position.y = 0;
  scene.add(mountain);
  
  // Second ring of mountains
  const m2Geo = new THREE.BufferGeometry();
  const m2Verts = [];
  for (let i = 0; i <= segments; i++) {
    const angle = (i / segments) * Math.PI * 2;
    const x = Math.cos(angle) * (radius - 50);
    const z = Math.sin(angle) * (radius - 50);
    const height = 20 + seededRandom(Math.floor(angle * 10) + 200) * 40;
    m2Verts.push(x, height, z);
  }
  m2Geo.setAttribute('position', new THREE.Float32BufferAttribute(m2Verts, 3));
  m2Geo.computeVertexNormals();
  
  const m2Mat = new THREE.MeshBasicMaterial({
    color: 0x2a0a30,
    side: THREE.BackSide,
    fog: false,
  });
  const m2 = new THREE.Mesh(m2Geo, m2Mat);
  m2.position.y = 0;
  scene.add(m2);
  
  // Sun glow
  const sunGeo = new THREE.CircleGeometry(40, 32);
  const sunMat = new THREE.MeshBasicMaterial({
    color: 0xff6600,
    transparent: true,
    opacity: 0.6,
    fog: false,
  });
  const sun = new THREE.Mesh(sunGeo, sunMat);
  sun.position.set(100, 80, -350);
  sun.lookAt(0, 0, 0);
  scene.add(sun);
  
  // Sun outer glow
  const sunGlowGeo = new THREE.CircleGeometry(80, 32);
  const sunGlowMat = new THREE.MeshBasicMaterial({
    color: 0xff4400,
    transparent: true,
    opacity: 0.2,
    fog: false,
  });
  const sunGlow = new THREE.Mesh(sunGlowGeo, sunGlowMat);
  sunGlow.position.copy(sun.position);
  sunGlow.lookAt(0, 0, 0);
  scene.add(sunGlow);
}

// ── main ──
function init() {
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x0a0010);
  scene.fog = new THREE.FogExp2(0x1a0520, 0.003);
  
  camera = new THREE.PerspectiveCamera(CFG.FOV_BASE, window.innerWidth / window.innerHeight, 0.1, 1000);
  
  const canvas = document.getElementById('gameCanvas');
  renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.3;
  
  composer = new THREE.EffectComposer(renderer);
  
  const renderPass = new THREE.RenderPass(scene, camera);
  composer.addPass(renderPass);
  
  const bloomPass = new THREE.UnrealBloomPass(
    new THREE.Vector2(window.innerWidth, window.innerHeight),
    1.5, 0.4, 0.85
  );
  bloomPass.threshold = 0.15;
  bloomPass.strength = 2.0;
  bloomPass.radius = 0.8;
  composer.addPass(bloomPass);
  composer.bloomPass = bloomPass;
  
  const rtA = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.LinearFilter,
    format: THREE.RGBAFormat,
    type: THREE.UnsignedByteType,
  });
  const rtB = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
    minFilter: THREE.LinearFilter,
    magFilter: THREE.LinearFilter,
    format: THREE.RGBAFormat,
    type: THREE.UnsignedByteType,
  });
  composer.motionBlurRTA = rtA;
  composer.motionBlurRTB = rtB;
  
  const motionBlurShader = {
    uniforms: {
      tDiffuse: { value: null },
      tPrevious: { value: null },
      intensity: { value: 0.3 },
    },
    vertexShader: `
      varying vec2 vUv;
      void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform sampler2D tDiffuse;
      uniform sampler2D tPrevious;
      uniform float intensity;
      varying vec2 vUv;
      void main() {
        vec4 current = texture2D(tDiffuse, vUv);
        vec4 previous = texture2D(tPrevious, vUv);
        gl_FragColor = mix(current, previous, intensity);
      }
    `,
  };
  const motionBlurPass = new THREE.ShaderPass(motionBlurShader);
  motionBlurPass.renderToScreen = true;
  composer.addPass(motionBlurPass);
  composer.motionBlurPass = motionBlurPass;
  
  ambientLight = new THREE.AmbientLight(0x331122, 0.35);
  scene.add(ambientLight);
  
  sunLight = new THREE.DirectionalLight(0xff8844, 1.8);
  sunLight.position.set(80, 120, -300);
  sunLight.castShadow = true;
  sunLight.shadow.mapSize.width = 2048;
  sunLight.shadow.mapSize.height = 2048;
  sunLight.shadow.camera.near = 1;
  sunLight.shadow.camera.far = 600;
  sunLight.shadow.camera.left = -150;
  sunLight.shadow.camera.right = 150;
  sunLight.shadow.camera.top = 150;
  sunLight.shadow.camera.bottom = -150;
  scene.add(sunLight);
  
  fillLight = new THREE.DirectionalLight(0x4422aa, 0.4);
  fillLight.position.set(-80, 80, 200);
  scene.add(fillLight);
  
  const rimLight = new THREE.DirectionalLight(0xff4400, 0.6);
  rimLight.position.set(0, 30, -100);
  scene.add(rimLight);
  
  createSkyDome();
  createPlayerCraft();
  scene.add(playerMesh);
  
  initParticles();
  scene.add(particleSystem.points);
  
  initChunks(0);
  for (const chunk of canyonChunks) {
    for (const mesh of chunk.meshes) {
      scene.add(mesh);
    }
  }
  
  scoreDisplay = document.getElementById('score');
  multiplierDisplay = document.getElementById('multiplier');
  energyBar = document.getElementById('energy-bar');
  boostBar = document.getElementById('boost-bar');
  speedBar = document.getElementById('speed-bar');
  gameOverScreen = document.getElementById('game-over');
  startScreen = document.getElementById('start-screen');
  nearMissFlash = document.getElementById('near-miss-flash');
  distanceValue = document.getElementById('distance-value');
  
  window.addEventListener('keydown', (e) => {
    keys[e.code] = true;
    if (e.code === 'Space') e.preventDefault();
  });
  window.addEventListener('keyup', (e) => {
    keys[e.code] = false;
  });
  
  document.getElementById('start-btn').addEventListener('click', startGame);
  document.getElementById('restart-btn').addEventListener('click', restartGame);
  
  window.addEventListener('resize', onResize);
  
  lastTime = 0;
  requestAnimationFrame(gameLoop);
}

function startGame() {
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
  else initAudio();
  
  startScreen.classList.add('hidden');
  gameOverScreen.classList.add('hidden');
  gameState = 'playing';
  player.alive = true;
  player.x = 0;
  player.y = CFG.PLAYER_START_Y;
  player.z = 0;
  player.vx = 0;
  player.vy = 0;
  player.speed = CFG.BASE_SPEED;
  player.energy = CFG.ENERGY_MAX;
  player.boost = 0;
  player.boostActive = false;
  player.multiplier = 1;
  player.nearMissTimer = 0;
  player.score = 0;
  player.maxSpeed = 0;
  totalScore = 0;
  distanceScore = 0;
  cameraShake.intensity = 0;
  
  for (const chunk of canyonChunks) {
    for (const mesh of chunk.meshes) {
      scene.remove(mesh);
    }
  }
  canyonChunks.length = 0;
  initChunks(0);
  for (const chunk of canyonChunks) {
    for (const mesh of chunk.meshes) {
      scene.add(mesh);
    }
  }
}

function restartGame() {
  gameOverScreen.classList.add('hidden');
  startGame();
}

function onResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  composer.setSize(window.innerWidth, window.innerHeight);
}

let distanceValue;

function updateHUD() {
  if (!scoreDisplay) return;
  
  const currentScore = Math.floor(player.score + distanceScore);
  scoreDisplay.textContent = currentScore;
  multiplierDisplay.textContent = `x${Math.max(1, player.multiplier.toFixed(1))}`;
  
  if (distanceValue) {
    distanceValue.textContent = `${Math.floor(player.z)}m`;
  }
  
  if (player.nearMissTimer > 0.5) {
    multiplierDisplay.classList.add('pulse');
    nearMissFlash.classList.add('active');
  } else {
    multiplierDisplay.classList.remove('pulse');
    nearMissFlash.classList.remove('active');
  }
  
  const energyPct = (player.energy / CFG.ENERGY_MAX) * 100;
  energyBar.style.width = `${energyPct}%`;
  
  const energyContainer = document.getElementById('energy-container');
  if (energyPct < 25) {
    energyContainer.classList.add('low');
  } else {
    energyContainer.classList.remove('low');
  }
  
  boostBar.style.width = `${(player.boost / CFG.BOOST_MAX) * 100}%`;
  speedBar.style.width = `${(player.speed / CFG.BOOST_SPEED) * 100}%`;
}

function animateCollectibles(time) {
  for (const chunk of canyonChunks) {
    for (const col of chunk.collectibles) {
      if (col.collected) continue;
      const mesh = col.mesh;
      mesh.position.y = col.baseY + Math.sin(time * 0.003 + col.phase) * 1.5;
      mesh.rotation.y = time * 0.002 + col.phase;
      mesh.rotation.x = Math.sin(time * 0.001 + col.phase) * 0.2;
      
      // Pulse glow
      const glow = mesh.children[0];
      if (glow) {
        const pulse = 1 + Math.sin(time * 0.005 + col.phase) * 0.3;
        glow.scale.set(pulse, pulse, pulse);
        glow.material.opacity = 0.1 + Math.sin(time * 0.004 + col.phase) * 0.08;
      }
    }
  }
}

function animateObstacles(time) {
  for (const chunk of canyonChunks) {
    for (const obs of chunk.obstacles) {
      const mesh = obs.mesh;
      if (mesh.userData.rotSpeed) {
        mesh.rotation.x += mesh.userData.rotSpeed.x * 0.01;
        mesh.rotation.y += mesh.userData.rotSpeed.y * 0.01;
      }
    }
  }
}

function gameLoop(time) {
  requestAnimationFrame(gameLoop);
  
  const dt = Math.min((time - lastTime) / 1000, 0.05);
  lastTime = time;
  
  if (gameState === 'start') {
    camera.position.set(0, 20, 50);
    camera.lookAt(0, 0, -50);
    camera.position.x += Math.sin(time * 0.0003) * 10;
    renderer.render(scene, camera);
    return;
  }
  
  if (gameState !== 'playing') {
    renderer.render(scene, camera);
    return;
  }
  
  updatePlayerPhysics(dt);
  
  if (player.alive) {
    checkNearMisses();
    checkCollectibles();
    checkCollisions();
    
    distanceScore += player.speed * dt * 0.5 * player.multiplier;
    
    emitTrail(dt);
    emitDebris(dt);
    updateParticles(dt);
    
    animateCollectibles(time);
    animateObstacles(time);
    
    // Camera follow
    const camOffset = new THREE.Vector3(
      -player.vx * 0.15,
      8 + player.vy * 0.1,
      25 + (player.boostActive ? 8 : 0)
    );
    camera.position.set(
      player.x + camOffset.x,
      player.y + camOffset.y,
      player.z + camOffset.z
    );
    
    // Dynamic FOV
    const targetFov = CFG.FOV_BASE + (player.speed - CFG.BASE_SPEED) / (CFG.BOOST_SPEED - CFG.BASE_SPEED) * (CFG.FOV_BOOST - CFG.FOV_BASE);
    camera.fov += (targetFov - camera.fov) * dt * 5;
    camera.updateProjectionMatrix();
    
    // Bloom intensity based on speed
    const bloomIntensity = 1.5 + (player.speed / CFG.MAX_SPEED) * 1.5 + (player.boostActive ? 1.0 : 0);
    composer.bloomPass.strength = bloomIntensity;
    
    const lookTarget = new THREE.Vector3(
      player.x + player.vx * 0.3,
      player.y + player.vy * 0.3,
      player.z - 50
    );
    camera.lookAt(lookTarget);
    
    // Camera shake
    if (cameraShake.intensity > 0) {
      camera.position.x += (Math.random() - 0.5) * cameraShake.intensity;
      camera.position.y += (Math.random() - 0.5) * cameraShake.intensity;
      cameraShake.intensity *= (1 - 5 * dt);
      if (cameraShake.intensity < 0.01) cameraShake.intensity = 0;
    }
    
    // Move sun light with player
    sunLight.position.set(player.x + 80, 120, player.z - 300);
    sunLight.target.position.set(player.x, player.y, player.z);
    sunLight.target.updateMatrixWorld();
    
    updateChunks();
    updateHUD();
  } else {
    gameState = 'gameover';
    const finalScore = Math.floor(player.score + distanceScore);
    document.getElementById('final-score').textContent = `SCORE: ${finalScore}`;
    document.getElementById('max-speed').textContent = `MAX SPEED: ${Math.floor(player.maxSpeed)}`;
    gameOverScreen.classList.remove('hidden');
    cameraShake.intensity = 5;
  }
  
  // Motion blur render pipeline
  const rtA = composer.motionBlurRTA;
  const rtB = composer.motionBlurRTB;
  const mbPass = composer.motionBlurPass;
  
  // Render scene to rtB
  renderer.setRenderTarget(rtB);
  renderer.render(scene, camera);
  
  // Copy previous to rtA
  const copyPass = new THREE.ShaderPass(THREE.CopyShader);
  copyPass.renderToScreen = false;
  copyPass.uniforms['tDiffuse'].value = rtA.texture;
  copyPass.uniforms['opacity'].value = 1.0;
  copyPass.setSize(window.innerWidth, window.innerHeight);
  renderer.setRenderTarget(rtA);
  renderer.render(copyPass, camera);
  
  // Blend current + previous to screen
  mbPass.uniforms['tDiffuse'].value = rtB.texture;
  mbPass.uniforms['tPrevious'].value = rtA.texture;
  const speedIntensity = 0.15 + (player.speed / CFG.MAX_SPEED) * 0.25 + (player.boostActive ? 0.15 : 0);
  mbPass.uniforms['intensity'].value = Math.min(speedIntensity, 0.5);
  mbPass.renderToScreen = true;
  renderer.setRenderTarget(null);
  renderer.render(scene, camera);
  
  // Swap render targets
  const tempRT = rtA;
  composer.motionBlurRTA = rtB;
  composer.motionBlurRTB = tempRT;
}

function updateChunks() {
  for (let i = canyonChunks.length - 1; i >= 0; i--) {
    const chunk = canyonChunks[i];
    if (chunk.zEnd < player.z - 80) {
      for (const mesh of chunk.meshes) {
        scene.remove(mesh);
        if (mesh.geometry) mesh.geometry.dispose();
      }
      canyonChunks.splice(i, 1);
    }
  }
  
  const frontChunk = canyonChunks[canyonChunks.length - 1];
  if (frontChunk && frontChunk.zEnd < player.z + CFG.CHUNK_SIZE * (CFG.CHUNK_COUNT - 1)) {
    const newZ = frontChunk.zEnd + 1;
    const newChunk = generateCanyonChunk(newZ);
    canyonChunks.push(newChunk);
    for (const mesh of newChunk.meshes) {
      scene.add(mesh);
    }
  }
}

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"b6869931-89aa-4d7b-9f27-3b4261f229aa","tokensIn":1142908,"tokensOut":26373,"tokensTotal":1169281,"turns":39,"toolCalls":51,"failedToolCalls":9,"timestamp":"2026-07-20T22:10:33.776Z"} -->