← Canyon Run results

Canyon Run

Qwen 3.6 27B Fable Fusion 711 Uncensored Heretic NM DAU NEO MAX MTP GGUF · full

◈Preview unavailable — try playing the result

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

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 46,709 bytes · SHA-256 77bf4569b576
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canyon Rush - High-Speed Procedural Canyon Racer</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    overflow: hidden;
    background: #0a0012;
    font-family: 'Courier New', monospace;
    cursor: none;
  }
  #gameCanvas {
    display: block;
    width: 100vw;
    height: 100vh;
  }

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

  /* Speed bar */
  #speedBarContainer {
    position: absolute;
    right: 40px;
    top: 50%;
    transform: translateY(-50%);
    width: 4px;
    height: 200px;
    background: rgba(255,255,255,0.1);
    border-radius: 2px;
  }
  #speedBar {
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 0%;
    background: linear-gradient(to top, #ff6b35, #ff9500, #ffcc00);
    border-radius: 2px;
    transition: height 0.1s ease;
    box-shadow: 0 0 10px rgba(255,149,0,0.8);
  }
  #speedText {
    position: absolute;
    right: 55px;
    top: 50%;
    transform: translateY(-50%);
    color: #ff9500;
    font-size: 14px;
    text-align: right;
    text-shadow: 0 0 8px rgba(255,149,0,0.8);
  }

  /* Score & Multiplier */
  #scoreContainer {
    position: absolute;
    top: 40px;
    left: 50%;
    transform: translateX(-50%);
    text-align: center;
  }
  #scoreText {
    color: #fff;
    font-size: 28px;
    text-shadow: 0 0 15px rgba(255,255,255,0.8);
  }
  #multiplierText {
    color: #ff6b35;
    font-size: 36px;
    text-shadow: 0 0 20px rgba(255,107,53,1);
    transition: transform 0.1s, opacity 0.2s;
  }

  /* Energy bar */
  #energyBarContainer {
    position: absolute;
    bottom: 40px;
    left: 50%;
    transform: translateX(-50%);
    width: 300px;
    height: 6px;
    background: rgba(255,255,255,0.1);
    border-radius: 3px;
    overflow: hidden;
  }
  #energyBar {
    width: 100%;
    height: 100%;
    background: linear-gradient(to right, #00e5ff, #64ffda);
    border-radius: 3px;
    box-shadow: 0 0 10px rgba(0,229,255,0.6);
    transition: width 0.1s ease, box-shadow 0.3s;
  }
  #energyBar.critical {
    background: linear-gradient(to right, #ff1744, #ff9100);
    box-shadow: 0 0 15px rgba(255,23,68,0.9);
  }

  /* Boost meter */
  #boostBarContainer {
    position: absolute;
    left: 40px;
    top: 50%;
    transform: translateY(-50%);
    width: 4px;
    height: 150px;
    background: rgba(255,255,255,0.1);
    border-radius: 2px;
  }
  #boostBar {
    position: absolute;
    bottom: 0;
    width: 100%;
    height: 0%;
    background: linear-gradient(to top, #ff00ff, #aa00ff, #7c4dff);
    border-radius: 2px;
    transition: height 0.2s ease;
    box-shadow: 0 0 10px rgba(186,48,255,0.8);
  }
  #boostReadyText {
    position: absolute;
    left: 55px;
    top: 50%;
    transform: translateY(-50%);
    color: #e040fb;
    font-size: 11px;
    text-shadow: 0 0 10px rgba(224,64,251,1);
    opacity: 0;
    transition: opacity 0.3s;
  }

  /* Game Over */
  #gameOver {
    position: fixed;
    top: 0; left: 0; width: 100%; height: 100%;
    background: radial-gradient(ellipse at center, rgba(10,0,18,0.7) 0%, rgba(10,0,18,0.95) 100%);
    display: none;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    pointer-events: all;
    z-index: 100;
    cursor: default;
  }
  #gameOver h1 {
    color: #ff6b35;
    font-size: 60px;
    text-shadow: 0 0 40px rgba(255,107,53,1);
    margin-bottom: 30px;
    letter-spacing: 8px;
  }
  .stat-row {
    color: #fff;
    font-size: 22px;
    margin: 10px 0;
    text-shadow: 0 0 10px rgba(255,255,255,0.6);
  }
  .stat-label { color: #64ffda; margin-right: 15px; }
  .stat-value { color: #ffcc00; }
  #restartBtn {
    margin-top: 40px;
    padding: 16px 48px;
    background: transparent;
    border: 2px solid #ff6b35;
    color: #ff6b35;
    font-size: 18px;
    font-family: 'Courier New', monospace;
    cursor: pointer;
    letter-spacing: 4px;
    transition: all 0.3s;
    pointer-events: all;
    text-transform: uppercase;
  }
  #restartBtn:hover {
    background: #ff6b35;
    color: #0a0012;
    box-shadow: 0 0 30px rgba(255,107,53,0.8);
  }

  /* Start Screen */
  #startScreen {
    position: fixed;
    top: 0; left: 0; width: 100%; height: 100%;
    background: radial-gradient(ellipse at center, rgba(40,10,40,0.95) 0%, rgba(10,0,18,0.98) 100%);
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    pointer-events: all;
    z-index: 100;
    cursor: default;
  }
  #startScreen h1 {
    color: #ff9500;
    font-size: 52px;
    text-shadow: 0 0 50px rgba(255,149,0,1);
    margin-bottom: 10px;
    letter-spacing: 10px;
  }
  #startScreen .subtitle {
    color: #64ffda;
    font-size: 16px;
    margin-bottom: 40px;
    letter-spacing: 4px;
    text-shadow: 0 0 10px rgba(100,255,218,0.6);
  }
  #startBtn {
    padding: 16px 48px;
    background: transparent;
    border: 2px solid #ff9500;
    color: #ff9500;
    font-size: 18px;
    font-family: 'Courier New', monospace;
    cursor: pointer;
    letter-spacing: 6px;
    transition: all 0.3s;
    text-transform: uppercase;
  }
  #startBtn:hover {
    background: #ff9500;
    color: #0a0012;
    box-shadow: 0 0 30px rgba(255,149,0,0.8);
  }
  .controls-info {
    margin-top: 40px;
    color: rgba(255,255,255,0.5);
    font-size: 12px;
    text-align: center;
    line-height: 1.8;
  }
  .controls-info span { color: #ff9500; }

  /* Near miss flash */
  #nearMissFlash {
    position: fixed;
    top: 0; left: 0; width: 100%; height: 100%;
    background: radial-gradient(circle, rgba(255,107,53,0.3), transparent 60%);
    opacity: 0;
    pointer-events: none;
    z-index: 5;
    transition: opacity 0.15s;
  }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>

<div id="hud">
  <div id="scoreContainer">
    <div id="scoreText">0</div>
    <div id="multiplierText">x1.0</div>
  </div>

  <div id="speedBarContainer"><div id="speedBar"></div></div>
  <div id="speedText">0 km/h</div>

  <div id="boostBarContainer"><div id="boostBar"></div></div>
  <div id="boostReadyText">BOOST READY</div>

  <div id="energyBarContainer"><div id="energyBar"></div></div>
</div>

<div id="nearMissFlash"></div>

<div id="startScreen">
  <h1>CANYON RUSH</h1>
  <div class="subtitle">HIGH-SPEED PROCEDURAL CANYON RACER</div>
  <button id="startBtn">START RUN</button>
  <div class="controls-info">
    <span>MOUSE</span> — steer &amp; bank<br>
    <span>CLICK / SHIFT</span> — boost<br>
    <span>F</span> — force boost when ready
  </div>
</div>

<div id="gameOver">
  <h1>RUN ENDED</h1>
  <div class="stat-row"><span class="stat-label">DISTANCE</span><span class="stat-value" id="finalScore">0</span> m</div>
  <div class="stat-row"><span class="stat-label">MAX SPEED</span><span class="stat-value" id="finalSpeed">0</span> km/h</div>
  <div class="stat-row"><span class="stat-label">BEST MULT</span><span class="stat-value" id="finalMult">x1.0</span></div>
  <div class="stat-row"><span class="stat-label">NEAR MISSES</span><span class="stat-value" id="finalNearMisses">0</span></div>
  <button id="restartBtn">RUN AGAIN</button>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.163.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.163.0/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

// ============================================================
// AUDIO SYSTEM (Web Audio API)
// ============================================================
class AudioSys {
  constructor() {
    this.ctx = null;
    this.engineOsc = null;
    this.engineGain = null;
    this.windNode = null;
    this.windGain = null;
    this.initialized = false;
  }

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

    // Engine hum
    this.engineOsc = this.ctx.createOscillator();
    this.engineOsc.type = 'sawtooth';
    this.engineOsc.frequency.value = 60;
    this.engineGain = this.ctx.createGain();
    this.engineGain.gain.value = 0.03;
    const engineFilter = this.ctx.createBiquadFilter();
    engineFilter.type = 'lowpass';
    engineFilter.frequency.value = 200;
    this.engineFilter = engineFilter;
    this.engineOsc.connect(this.engineFilter);
    this.engineFilter.connect(this.engineGain);
    this.engineGain.connect(this.ctx.destination);
    this.engineOsc.start();

    // Wind noise
    const bufferSize = 2 * this.ctx.sampleRate;
    const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const output = noiseBuffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) output[i] = Math.random() * 2 - 1;
    this.windNode = this.ctx.createBufferSource();
    this.windNode.buffer = noiseBuffer;
    this.windNode.loop = true;
    this.windGain = this.ctx.createGain();
    this.windGain.gain.value = 0.02;
    const windFilter = this.ctx.createBiquadFilter();
    windFilter.type = 'bandpass';
    windFilter.frequency.value = 400;
    windFilter.Q.value = 0.5;
    this.windNode.connect(windFilter);
    windFilter.connect(this.windGain);
    this.windGain.connect(this.ctx.destination);
    this.windNode.start();
  }

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

  updateWind(speedRatio) {
    if (!this.initialized) return;
    this.windGain.gain.setTargetAtTime(0.015 + speedRatio * 0.04, this.ctx.currentTime, 0.1);
  }

  playCollect() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sine';
    osc.frequency.setValueAtTime(880, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(1320, this.ctx.currentTime + 0.08);
    gain.gain.setValueAtTime(0.12, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.2);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.2);
  }

  playNearMiss() {
    if (!this.initialized) return;
    // Whoosh
    const bufferSize = this.ctx.sampleRate * 0.3;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      const t = i / bufferSize;
      data[i] = (Math.random() * 2 - 1) * Math.exp(-t * 6) * Math.sin(t * Math.PI * 20);
    }
    const source = this.ctx.createBufferSource();
    source.buffer = buffer;
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'bandpass';
    filter.frequency.value = 2000;
    const gain = this.ctx.createGain();
    gain.gain.value = 0.15;
    source.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    source.start();
  }

  playBoost() {
    if (!this.initialized) return;
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(120, this.ctx.currentTime);
    osc.frequency.exponentialRampToValueAtTime(400, this.ctx.currentTime + 0.3);
    gain.gain.setValueAtTime(0.08, this.ctx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.5);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + 0.5);
  }

  playCrash() {
    if (!this.initialized) return;
    const bufferSize = this.ctx.sampleRate * 0.6;
    const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) {
      const t = i / bufferSize;
      data[i] = (Math.random() * 2 - 1) * Math.exp(-t * 3);
    }
    const source = this.ctx.createBufferSource();
    source.buffer = buffer;
    const filter = this.ctx.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.value = 800;
    const gain = this.ctx.createGain();
    gain.gain.value = 0.3;
    source.connect(filter);
    filter.connect(gain);
    gain.connect(this.ctx.destination);
    source.start();

    this.engineOsc.stop();
    this.windNode.stop();
  }
}

// ============================================================
// GAME STATE
// ============================================================
const STATE = {
  STARTED: 0,
  PLAYING: 1,
  GAMEOVER: 2
};

// ============================================================
// CONSTANTS
// ============================================================
const CHUNK_LENGTH = 100;
const CHUNK_COUNT = 12;
const CANON_HALF_WIDTH_BASE = 40;
const CANON_HEIGHT_BASE = 35;
const PLAYER_SPEED_BASE = 180;
const PLAYER_SPEED_MAX = 500;
const ENERGY_DECAY_RATE = 3.5;
const BOOST_CONSUME_RATE = 45;
const BOOST_MAX = 100;
const BOOST_SPEED_MULT = 2.0;
const BOOST_DURATION = 2.0;
const NEAR_MISS_THRESHOLD = 6;
const NEAR_MISS_BONUS_THRESHOLD = 4;
const ORB_CHARGE = 18;

// ============================================================
// THREE.JS SETUP
// ============================================================
const canvas = document.getElementById('gameCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: false, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a0525);
scene.fog = new THREE.FogExp2(0x1a0525, 0.007);

const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 600);
camera.position.set(0, 8, -20);

// Post-processing
const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.2, 0.4, 0.1
);
composer.addPass(bloomPass);

// Lighting
const ambientLight = new THREE.AmbientLight(0x402040, 0.6);
scene.add(ambientLight);

const sunLight = new THREE.DirectionalLight(0xff8844, 1.8);
sunLight.position.set(30, 60, -80);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 1024;
sunLight.shadow.mapSize.height = 1024;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 300;
sunLight.shadow.camera.left = -60;
sunLight.shadow.camera.right = 60;
sunLight.shadow.camera.top = 60;
sunLight.shadow.camera.bottom = -60;
scene.add(sunLight);

const rimLight = new THREE.DirectionalLight(0x6600aa, 0.7);
rimLight.position.set(-20, 30, 50);
scene.add(rimLight);

// ============================================================
// MATERIALS (flat shading, no textures)
// ============================================================
const matCanyonRock = new THREE.MeshStandardMaterial({
  color: 0x3d1a2e,
  flatShading: true,
  roughness: 0.9,
  metalness: 0.1
});

const matCanyonRockWarm = new THREE.MeshStandardMaterial({
  color: 0x6b2e4a,
  flatShading: true,
  roughness: 0.85,
  metalness: 0.15
});

const matMonolith = new THREE.MeshStandardMaterial({
  color: 0x222233,
  flatShading: true,
  roughness: 0.3,
  metalness: 0.8,
  emissive: 0x111133,
  emissiveIntensity: 0.3
});

const matPlayer = new THREE.MeshStandardMaterial({
  color: 0xcccccc,
  flatShading: true,
  roughness: 0.2,
  metalness: 0.9,
  emissive: 0x00e5ff,
  emissiveIntensity: 0.4
});

const matPlayerAccent = new THREE.MeshStandardMaterial({
  color: 0xff6b35,
  flatShading: true,
  roughness: 0.3,
  metalness: 0.7,
  emissive: 0xff6b35,
  emissiveIntensity: 0.6
});

const matOrb = new THREE.MeshStandardMaterial({
  color: 0x00ffcc,
  flatShading: true,
  emissive: 0x00ffcc,
  emissiveIntensity: 1.5,
  transparent: true,
  opacity: 0.9
});

const matGround = new THREE.MeshStandardMaterial({
  color: 0x2a1520,
  flatShading: true,
  roughness: 1.0,
  metalness: 0.0
});

const matDebris = new THREE.MeshBasicMaterial({ color: 0xff8844 });

// ============================================================
// REUSABLE GEOMETRIES
// ============================================================
const geoMonolith = new THREE.CylinderGeometry(1.2, 1.2, 12, 6);
const geoOrb = new THREE.OctahedronGeometry(0.8, 0);
const geoDebris = new THREE.TetrahedronGeometry(0.15, 0);

// ============================================================
// PLAYER CRAFT
// ============================================================
class Player {
  constructor() {
    this.mesh = new THREE.Group();
    this.speed = PLAYER_SPEED_BASE;
    this.maxSpeed = PLAYER_SPEED_MAX;
    this.targetX = 0;
    this.targetY = 8;
    this.currentX = 0;
    this.currentY = 8;
    this.roll = 0;
    this.pitch = 0;
    this.energy = 100;
    this.boost = 0;
    this.isBoosting = false;
    this.boostTimer = 0;
    this.maxSpeedReached = 0;
    this.distance = 0;

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

  createModel() {
    // Main fuselage - sleek tetrahedral shape
    const fuselage = new THREE.Mesh(
      new THREE.ConeGeometry(0.8, 4, 4),
      matPlayer
    );
    fuselage.rotation.x = Math.PI / 2;
    fuselage.castShadow = true;
    this.mesh.add(fuselage);

    // Nose accent
    const nose = new THREE.Mesh(
      new THREE.ConeGeometry(0.3, 1.5, 4),
      matPlayerAccent
    );
    nose.rotation.x = Math.PI / 2;
    nose.position.z = -2.75;
    nose.castShadow = true;
    this.mesh.add(nose);

    // Wings
    const wingGeo = new THREE.BufferGeometry();
    const wingVerts = new Float32Array([
      0, 0, -0.5,   2.5, -0.5, 0,   0, 0, 1.5,
      0, 0, -0.5,   0, 0, 1.5,   -2.5, -0.5, 0,
      0, 0, -0.5,   -2.5, -0.5, 0,   0, 0, 1.5
    ]);
    wingGeo.setAttribute('position', new THREE.BufferAttribute(wingVerts, 3));
    wingGeo.computeVertexNormals();
    const wings = new THREE.Mesh(wingGeo, matPlayer);
    wings.castShadow = true;
    this.mesh.add(wings);

    // Wing tips
    const wingTipGeo = new THREE.ConeGeometry(0.15, 0.6, 4);
    const wingTipL = new THREE.Mesh(wingTipGeo, matPlayerAccent);
    wingTipL.position.set(2.5, -0.5, -0.2);
    wingTipL.rotation.z = -Math.PI / 2;
    this.mesh.add(wingTipL);
    const wingTipR = new THREE.Mesh(wingTipGeo, matPlayerAccent);
    wingTipR.position.set(-2.5, -0.5, -0.2);
    wingTipR.rotation.z = Math.PI / 2;
    this.mesh.add(wingTipR);

    // Engine glow
    const engineGlow = new THREE.Mesh(
      new THREE.SphereGeometry(0.4, 6, 6),
      new THREE.MeshBasicMaterial({ color: 0x00e5ff })
    );
    engineGlow.position.z = 2.2;
    this.engineGlow = engineGlow;
    this.mesh.add(engineGlow);

    // Engine light
    const engineLight = new THREE.PointLight(0x00e5ff, 1.5, 15);
    engineLight.position.z = 2.2;
    this.mesh.add(engineLight);
    this.engineLight = engineLight;
  }

  update(dt, mouseX, mouseY) {
    // Movement interpolation (weighty feel)
    const moveSpeed = this.isBoosting ? 22 : 16;
    this.currentX += (mouseX - this.currentX) * moveSpeed * dt;
    this.currentY += (mouseY - this.currentY) * moveSpeed * dt;

    // Banking / rolling based on lateral movement
    const targetRoll = (mouseX - this.currentX) * -1.2;
    this.roll += (targetRoll - this.roll) * 12 * dt;

    // Pitch based on vertical movement
    const targetPitch = (mouseY - this.currentY) * 0.8;
    this.pitch += (targetPitch - this.pitch) * 12 * dt;

    this.mesh.position.x = this.currentX;
    this.mesh.position.y = this.currentY;
    this.mesh.rotation.z = this.roll;
    this.mesh.rotation.x = this.pitch;

    // Engine effects
    const boostScale = this.isBoosting ? 2.5 : 1.0;
    this.engineGlow.scale.setScalar(boostScale);
    this.engineLight.intensity = 1.5 + (this.isBoosting ? 4 : 0);
    if (this.isBoosting) {
      this.engineLight.color.setHex(0xff6b35);
      this.engineGlow.material.color.setHex(0xff6b35);
    } else {
      this.engineLight.color.setHex(0x00e5ff);
      this.engineGlow.material.color.setHex(0x00e5ff);
    }

    // Speed
    let effectiveMaxSpeed = this.maxSpeed;
    if (this.isBoosting) {
      effectiveMaxSpeed = this.maxSpeed * BOOST_SPEED_MULT;
      this.boostTimer -= dt;
      if (this.boostTimer <= 0) this.endBoost();
    }

    // Gradual speed increase with distance
    const speedProgress = this.distance / 5000;
    const baseSpeed = PLAYER_SPEED_BASE + speedProgress * 60;
    this.speed += (baseSpeed - this.speed) * 2 * dt;

    if (this.maxSpeedReached < this.speed * 3.6) this.maxSpeedReached = this.speed * 3.6;

    // Energy
    this.energy -= ENERGY_DECAY_RATE * dt;
    if (this.isBoosting) this.energy -= BOOST_CONSUME_RATE * dt;

    // Boost charge (slow passive + near-miss bonuses)
    if (!this.isBoosting) {
      this.boost += (100 - this.boost) * 1.5 * dt;
    }

    this.distance += this.speed * dt;

    return this.speed * dt;
  }

  startBoost() {
    if (this.boost >= BOOST_MAX && !this.isBoosting && this.energy > 20) {
      this.isBoosting = true;
      this.boostTimer = BOOST_DURATION;
      this.boost = 0;
      audio.playBoost();
      return true;
    }
    return false;
  }

  endBoost() {
    this.isBoosting = false;
    this.boostTimer = 0;
  }

  addEnergy(amount) {
    this.energy = Math.min(100, this.energy + amount);
  }

  addBoost(amount) {
    this.boost = Math.min(100, this.boost + amount);
  }

  isDead() {
    return this.energy <= 0;
  }
}

// ============================================================
// CHUNK SYSTEM
// ============================================================
class Chunk {
  constructor(startZ, difficulty = 0) {
    this.startZ = startZ;
    this.meshes = [];
    this.obstacles = [];
    this.orbs = [];
    this.debrisEmitters = [];
    this.generate(difficulty);
  }

  generate(difficulty) {
    // Canyon width decreases with difficulty
    const widthShrink = difficulty * 8;
    const halfWidth = CANON_HALF_WIDTH_BASE - Math.max(0, widthShrink);
    const halfHeight = CANON_HEIGHT_BASE - Math.max(0, difficulty * 3);

    // Generate canyon walls (procedural segments)
    const segments = 8;
    const segLength = CHUNK_LENGTH / segments;
    let prevLeftX = -halfWidth;
    let prevRightX = halfWidth;
    let prevTopY = halfHeight;
    let prevBottomY = -halfHeight + 2;

    for (let i = 0; i < segments; i++) {
      const z = this.startZ + i * segLength;
      const nextLeftX = -halfWidth + (Math.random() - 0.5) * 10 * (1 - difficulty * 0.3);
      const nextRightX = halfWidth + (Math.random() - 0.5) * 10 * (1 - difficulty * 0.3);
      const nextTopY = halfHeight + (Math.random() - 0.5) * 8;
      const nextBottomY = -halfHeight + 2 + (Math.random() - 0.5) * 5;

      // Left wall
      this.addWallSegment(z, prevLeftX, nextLeftX, prevBottomY, prevTopY, nextBottomY, nextTopY);
      // Right wall
      this.addWallSegment(z, prevRightX, nextRightX, prevBottomY, prevTopY, nextBottomY, nextTopY, true);
      // Floor
      this.addFloorSegment(z, prevLeftX, prevRightX, nextLeftX, nextRightX, prevBottomY, nextBottomY);

      prevLeftX = nextLeftX;
      prevRightX = nextRightX;
      prevTopY = nextTopY;
      prevBottomY = nextBottomY;
    }

    // Add obstacles based on difficulty
    if (difficulty > 0.1) this.addFloatingMonoliths(halfWidth, halfHeight, difficulty);
    if (difficulty > 0.3) this.addTightSqueezes(halfWidth, halfHeight, difficulty);

    // Add orbs (energy pickups)
    this.addOrbs(halfWidth, halfHeight, difficulty);

    // Add debris emitters for visual effect
    this.addDebrisEmitters(halfWidth, halfHeight);
  }

  addWallSegment(z, x1, x2, yBottom1, yTop1, yBottom2, yTop2, isRight = false) {
    const segLength = CHUNK_LENGTH / 8;
    const depth = 2 + Math.random() * 4;
    const geo = new THREE.BufferGeometry();
    const verts = new Float32Array([
      x1, yTop1, z,       x2, yTop2, z + segLength,  x2, yBottom2, z + segLength,
      x1, yTop1, z,       x2, yBottom2, z + segLength, x1, yBottom1, z,
      x1, yTop1, z + depth, x1, yBottom1, z + depth, x2, yBottom2, z + segLength + depth,
      x1, yTop1, z + depth, x2, yBottom2, z + segLength + depth, x2, yTop2, z + segLength + depth,
    ]);
    geo.setAttribute('position', new THREE.BufferAttribute(verts, 3));
    geo.computeVertexNormals();
    const mat = Math.random() > 0.5 ? matCanyonRock : matCanyonRockWarm;
    const mesh = new THREE.Mesh(geo, mat);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    scene.add(mesh);
    this.meshes.push(mesh);

    // Add collision box
    const minX = Math.min(x1, x2) - 0.5;
    const maxX = Math.max(x1, x2) + 0.5;
    const minY = Math.min(yBottom1, yBottom2);
    const maxY = Math.max(yTop1, yTop2);
    this.obstacles.push({ minX, maxX, minY, maxY, zStart: z, zEnd: z + segLength });
  }

  addFloorSegment(z, leftX1, rightX1, leftX2, rightX2, y1, y2) {
    const segLength = CHUNK_LENGTH / 8;
    const geo = new THREE.BufferGeometry();
    const verts = new Float32Array([
      leftX1, y1 - 0.5, z, rightX1, y1 - 0.5, z, rightX2, y2 - 0.5, z + segLength,
      leftX1, y1 - 0.5, z, rightX2, y2 - 0.5, z + segLength, leftX2, y2 - 0.5, z + segLength
    ]);
    geo.setAttribute('position', new THREE.BufferAttribute(verts, 3));
    geo.computeVertexNormals();
    const mesh = new THREE.Mesh(geo, matGround);
    mesh.receiveShadow = true;
    scene.add(mesh);
    this.meshes.push(mesh);

    // Floor collision
    const minX = Math.min(leftX1, leftX2, rightX1, rightX2) - 0.5;
    const maxX = Math.max(leftX1, leftX2, rightX1, rightX2) + 0.5;
    this.obstacles.push({ minX, maxX, minY: -100, maxY: y1 - 0.8, zStart: z, zEnd: z + segLength });
  }

  addFloatingMonoliths(halfWidth, halfHeight, difficulty) {
    const count = Math.floor(2 + difficulty * 4);
    for (let i = 0; i < count; i++) {
      const z = this.startZ + 10 + Math.random() * (CHUNK_LENGTH - 20);
      const x = (Math.random() - 0.5) * (halfWidth * 1.4);
      const y = -halfHeight + 5 + Math.random() * (halfHeight * 2);
      const height = 6 + Math.random() * 8;

      const geo = new THREE.CylinderGeometry(
        0.8 + Math.random() * 1.2,
        0.8 + Math.random() * 1.2,
        height,
        5 + Math.floor(Math.random() * 3)
      );
      const mesh = new THREE.Mesh(geo, matMonolith);
      mesh.position.set(x, y, z);
      mesh.rotation.y = Math.random() * Math.PI;
      mesh.rotation.z = (Math.random() - 0.5) * 0.2;
      mesh.castShadow = true;
      scene.add(mesh);
      this.meshes.push(mesh);

      this.obstacles.push({
        minX: x - 1.5, maxX: x + 1.5,
        minY: y - height / 2, maxY: y + height / 2,
        zStart: z - 1, zEnd: z + 1
      });
    }
  }

  addTightSqueezes(halfWidth, halfHeight, difficulty) {
    const count = Math.floor(1 + difficulty * 2);
    for (let i = 0; i < count; i++) {
      const z = this.startZ + 15 + Math.random() * (CHUNK_LENGTH - 30);
      // Create two pillars with a narrow gap
      const gapX = (Math.random() - 0.5) * halfWidth * 0.6;
      const gapWidth = 5 + (1 - difficulty) * 5;
      const gapY = -halfHeight + 3 + Math.random() * halfHeight;

      for (let side = -1; side <= 1; side += 2) {
        const x = gapX + side * gapWidth;
        const height = 8 + Math.random() * 6;
        const mesh = new THREE.Mesh(geoMonolith.clone(), matMonolith);
        mesh.position.set(x, gapY, z);
        mesh.castShadow = true;
        scene.add(mesh);
        this.meshes.push(mesh);

        this.obstacles.push({
          minX: x - 1.5, maxX: x + 1.5,
          minY: gapY - height / 2, maxY: gapY + height / 2,
          zStart: z - 1, zEnd: z + 1
        });
      }
    }
  }

  addOrbs(halfWidth, halfHeight, difficulty) {
    const count = 4 + Math.floor(Math.random() * 4);
    for (let i = 0; i < count; i++) {
      const z = this.startZ + 5 + Math.random() * (CHUNK_LENGTH - 10);
      // Place orbs in interesting spots - sometimes in narrow paths
      const x = (Math.random() - 0.5) * halfWidth * 1.6;
      const y = -halfHeight + 4 + Math.random() * (halfHeight * 1.6);

      const mesh = new THREE.Mesh(geoOrb, matOrb);
      mesh.position.set(x, y, z);
      scene.add(mesh);
      this.meshes.push(mesh);
      this.orbs.push({ mesh, collected: false });
    }
  }

  addDebrisEmitters(halfWidth, halfHeight) {
    // Points along walls where debris particles spawn
    const count = 3 + Math.floor(Math.random() * 4);
    for (let i = 0; i < count; i++) {
      this.debrisEmitters.push({
        x: (Math.random() > 0.5 ? halfWidth : -halfWidth) + (Math.random() - 0.5) * 5,
        y: (Math.random() - 0.5) * halfHeight * 1.5,
        z: this.startZ + Math.random() * CHUNK_LENGTH,
        timer: Math.random() * 2
      });
    }
  }

  dispose() {
    for (const mesh of this.meshes) {
      scene.remove(mesh);
      if (mesh.geometry) mesh.geometry.dispose();
    }
    this.meshes = [];
    this.obstacles = [];
    this.orbs = [];
    this.debrisEmitters = [];
  }
}

// ============================================================
// PARTICLE SYSTEM
// ============================================================
class ParticleSystem {
  constructor() {
    // Engine trail
    this.trailPool = [];
    const trailGeo = new THREE.SphereGeometry(0.12, 3, 3);
    for (let i = 0; i < 80; i++) {
      const mesh = new THREE.Mesh(trailGeo, new THREE.MeshBasicMaterial({
        color: 0x00e5ff, transparent: true, opacity: 0.7
      }));
      mesh.visible = false;
      scene.add(mesh);
      this.trailPool.push({ mesh, life: 0, maxLife: 0.6 + Math.random() * 0.4 });
    }

    // Debris particles
    this.debrisPool = [];
    for (let i = 0; i < 100; i++) {
      const mesh = new THREE.Mesh(geoDebris, matDebris.clone());
      mesh.visible = false;
      scene.add(mesh);
      this.debrisPool.push({ mesh, life: 0, vx: 0, vy: 0, vz: 0 });
    }

    this.trailTimer = 0;
  }

  emitTrail(pos, isBoosting) {
    this.trailTimer += 0.016;
    const rate = isBoosting ? 0.02 : 0.04;
    if (this.trailTimer < rate) return;
    this.trailTimer = 0;

    const p = this.trailPool.find(p => p.life <= 0);
    if (!p) return;

    p.mesh.position.copy(pos);
    p.mesh.position.z += 2.5 + Math.random() * 1;
    p.mesh.position.x += (Math.random() - 0.5) * 0.3;
    p.mesh.position.y += (Math.random() - 0.5) * 0.3;
    p.life = p.maxLife;
    p.mesh.visible = true;
    p.mesh.material.color.setHex(isBoosting ? 0xff6b35 : 0x00e5ff);
    p.mesh.material.opacity = 0.8;
    p.mesh.scale.setScalar(isBoosting ? 1.8 : 1);
  }

  emitDebris(pos, speed) {
    const p = this.debrisPool.find(p => p.life <= 0);
    if (!p) return;

    p.mesh.position.copy(pos);
    p.mesh.position.x += (Math.random() - 0.5) * 8;
    p.mesh.position.y += (Math.random() - 0.5) * 8;
    p.vx = (Math.random() - 0.5) * speed * 0.3;
    p.vy = (Math.random() - 0.5) * speed * 0.3;
    p.vz = -speed * 0.5 - Math.random() * speed * 0.5;
    p.life = 1.0;
    p.mesh.visible = true;
    p.mesh.material.opacity = 0.6;
  }

  update(dt, playerSpeed, playerPos) {
    // Trail
    for (const p of this.trailPool) {
      if (p.life <= 0) continue;
      p.life -= dt;
      if (p.life <= 0) {
        p.mesh.visible = false;
        continue;
      }
      // Move trail back relative to player speed
      p.mesh.position.z += playerSpeed * dt * 0.5;
      p.mesh.material.opacity = (p.life / p.maxLife) * 0.7;
      p.mesh.scale.multiplyScalar(1 - dt * 0.5);
    }

    // Debris
    for (const p of this.debrisPool) {
      if (p.life <= 0) continue;
      p.life -= dt;
      if (p.life <= 0) {
        p.mesh.visible = false;
        continue;
      }
      p.mesh.position.x += p.vx * dt;
      p.mesh.position.y += p.vy * dt;
      p.mesh.position.z += p.vz * dt;
      p.mesh.rotation.x += dt * 8;
      p.mesh.rotation.z += dt * 6;
      p.mesh.material.opacity = p.life * 0.6;
    }
  }

  dispose() {
    for (const p of this.trailPool) {
      scene.remove(p.mesh);
      p.mesh.geometry.dispose();
      p.mesh.material.dispose();
    }
    for (const p of this.debrisPool) {
      scene.remove(p.mesh);
      p.mesh.material.dispose();
    }
  }
}

// ============================================================
// CAMERA CONTROLLER
// ============================================================
class CameraController {
  constructor() {
    this.shakeAmount = 0;
    this.shakeDecay = 8;
    this.targetFOV = 70;
    this.currentFOV = 70;
  }

  shake(intensity) {
    this.shakeAmount = Math.min(this.shakeAmount + intensity, 3);
  }

  update(dt, playerPos, isBoosting, playerSpeed) {
    // Follow player
    const targetX = playerPos.x * 0.3;
    const targetY = playerPos.y * 0.4 + 8;
    camera.position.x += (targetX - camera.position.x) * 6 * dt;
    camera.position.y += (targetY - camera.position.y) * 5 * dt;
    camera.position.z = playerPos.z - 22 - (isBoosting ? 4 : 0);

    // Look ahead
    const lookTarget = new THREE.Vector3(
      playerPos.x * 0.2,
      playerPos.y * 0.3 + 2,
      playerPos.z + 80
    );
    camera.lookAt(lookTarget);

    // Shake
    if (this.shakeAmount > 0) {
      camera.position.x += (Math.random() - 0.5) * this.shakeAmount;
      camera.position.y += (Math.random() - 0.5) * this.shakeAmount * 0.6;
      this.shakeAmount *= Math.exp(-this.shakeDecay * dt);
      if (this.shakeAmount < 0.02) this.shakeAmount = 0;
    }

    // FOV for speed/boost effect
    const speedRatio = playerSpeed / PLAYER_SPEED_MAX;
    this.targetFOV = 70 + speedRatio * 10 + (isBoosting ? 15 : 0);
    this.currentFOV += (this.targetFOV - this.currentFOV) * 8 * dt;
    camera.fov = this.currentFOV;
    camera.updateProjectionMatrix();
  }
}

// ============================================================
// COLLISION SYSTEM
// ============================================================
function checkCollisions(player, chunks, particles) {
  const px = player.mesh.position.x;
  const py = player.mesh.position.y;
  const pz = player.mesh.position.z - 1.5;
  const playerRadius = 1.2;

  let nearestDistance = Infinity;
  let hadNearMiss = false;

  for (const chunk of chunks) {
    // Wall collisions
    for (const obs of chunk.obstacles) {
      if (pz < obs.zStart - 1 || pz > obs.zEnd + 1) continue;

      // Check X bounds
      if (px + playerRadius > obs.minX && px - playerRadius < obs.maxX) {
        // Check Y bounds
        if (py + playerRadius > obs.minY && py - playerRadius < obs.maxY) {
          return { dead: true, reason: 'collision' };
        }

        // Near miss check
        const edgeDist = Math.min(
          Math.abs(px - obs.minX) - playerRadius,
          Math.abs(px - obs.maxX) - playerRadius,
          Math.abs(py - obs.minY) - playerRadius,
          Math.abs(py - obs.maxY) - playerRadius
        );
        if (edgeDist >= 0 && edgeDist < NEAR_MISS_THRESHOLD) {
          nearestDistance = Math.min(nearestDistance, edgeDist);
        }
      }
    }

    // Orb collection
    for (const orb of chunk.orbs) {
      if (orb.collected) continue;
      const dx = px - orb.mesh.position.x;
      const dy = py - orb.mesh.position.y;
      const dz = pz - orb.mesh.position.z;
      const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
      if (dist < 2.5) {
        orb.collected = true;
        orb.mesh.visible = false;
        player.addEnergy(ORB_CHARGE);
        player.addBoost(10);
        score.addEnergyCollect();
        audio.playCollect();

        // Spawn collect particles
        for (let i = 0; i < 8; i++) {
          particles.emitDebris(orb.mesh.position, player.speed);
        }
      }
    }
  }

  // Near miss scoring
  if (nearestDistance < NEAR_MISS_THRESHOLD && nearestDistance >= 0) {
    hadNearMiss = true;
    score.addNearMiss(nearestDistance);
    cam.shake(0.5 + (NEAR_MISS_THRESHOLD - nearestDistance) * 0.3);
    if (nearestDistance < NEAR_MISS_BONUS_THRESHOLD) {
      audio.playNearMiss();
      document.getElementById('nearMissFlash').style.opacity = '0.4';
      setTimeout(() => {
        document.getElementById('nearMissFlash').style.opacity = '0';
      }, 150);
    }
  }

  return { dead: false, hadNearMiss };
}

// ============================================================
// SCORE SYSTEM
// ============================================================
const score = {
  value: 0,
  multiplier: 1.0,
  nearMissCounter: 0,
  energyCollected: 0,
  maxMultiplier: 1.0,

  update(dt, playerSpeed) {
    // Base score from distance
    this.value += (playerSpeed * dt) * this.multiplier * 0.5;

    // Multiplier decay
    if (this.multiplier > 1) {
      this.multiplier -= dt * 0.15;
      if (this.multiplier < 1) this.multiplier = 1;
    }
  },

  addNearMiss(distance) {
    this.nearMissCounter++;
    const bonus = 1 + (NEAR_MISS_THRESHOLD - distance) * 0.5;
    this.multiplier = Math.min(this.multiplier + bonus * 0.3, 10);
    if (this.multiplier > this.maxMultiplier) this.maxMultiplier = this.multiplier;
  },

  addEnergyCollect() {
    this.energyCollected++;
  },

  reset() {
    this.value = 0;
    this.multiplier = 1.0;
    this.nearMissCounter = 0;
    this.energyCollected = 0;
    this.maxMultiplier = 1.0;
  }
};

// ============================================================
// GAME VARIABLES
// ============================================================
let gameState = STATE.STARTED;
let chunks = [];
let player;
let particles;
let cam = new CameraController();
let audio = new AudioSys();
let lastTime = 0;
let difficulty = 0;

// Mouse state
let mouseX = 0;
let mouseY = 0;
let mouseNormX = 0;
let mouseNormY = 0;
let mouseDown = false;
let keysPressed = {};

// ============================================================
// INIT
// ============================================================
function init() {
  player = new Player();
  particles = new ParticleSystem();

  // Generate initial chunks
  for (let i = 0; i < CHUNK_COUNT; i++) {
    const diff = Math.min(1, i / CHUNK_COUNT);
    chunks.push(new Chunk(i * CHUNK_LENGTH, diff));
  }

  // Set player starting Z
  player.mesh.position.z = 20;
}

// ============================================================
// CHUNK MANAGEMENT
// ============================================================
function updateChunks(dt, distanceTraveled) {
  // Move chunks backward as player moves forward
  const playerZ = player.mesh.position.z;

  // Remove chunks behind player
  while (chunks.length > 0 && chunks[0].startZ + CHUNK_LENGTH < playerZ - 50) {
    chunks.shift().dispose();
  }

  // Add chunks ahead
  const furthestZ = chunks[chunks.length - 1].startZ;
  while (furthestZ < playerZ + CHUNK_COUNT * CHUNK_LENGTH) {
    const diff = Math.min(1, (furthestZ - 20) / 3000);
    chunks.push(new Chunk(furthestZ, diff));
    difficulty = diff;
  }
}

// ============================================================
// HUD UPDATE
// ============================================================
function updateHUD() {
  const speedKmh = Math.round(player.speed * 3.6);
  const speedPercent = (player.speed / (player.maxSpeed * BOOST_SPEED_MULT)) * 100;

  document.getElementById('scoreText').textContent = Math.round(score.value).toLocaleString();
  document.getElementById('speedText').textContent = speedKmh + ' km/h';
  document.getElementById('speedBar').style.height = speedPercent + '%';

  // Multiplier display
  const multEl = document.getElementById('multiplierText');
  multEl.textContent = 'x' + score.multiplier.toFixed(1);
  if (score.multiplier > 1.5) {
    const pulseScale = 1 + (score.multiplier - 1.5) * 0.15;
    multEl.style.transform = `scale(${pulseScale})`;
    multEl.style.color = score.multiplier > 5 ? '#ff1744' : '#ff6b35';
  } else {
    multEl.style.transform = 'scale(1)';
    multEl.style.color = '#ff6b35';
  }

  // Energy bar
  const energyEl = document.getElementById('energyBar');
  energyEl.style.width = Math.max(0, player.energy) + '%';
  energyEl.classList.toggle('critical', player.energy < 25);

  // Boost bar
  const boostEl = document.getElementById('boostBar');
  boostEl.style.height = player.boost + '%';
  document.getElementById('boostReadyText').style.opacity = player.boost >= BOOST_MAX ? '1' : '0';
}

// ============================================================
// GAME OVER
// ============================================================
function showGameOver() {
  gameState = STATE.GAMEOVER;
  audio.playCrash();

  document.getElementById('hud').style.display = 'none';
  document.getElementById('gameOver').style.display = 'flex';
  document.getElementById('finalScore').textContent = Math.round(player.distance).toLocaleString();
  document.getElementById('finalSpeed').textContent = Math.round(player.maxSpeedReached).toLocaleString();
  document.getElementById('finalMult').textContent = 'x' + score.maxMultiplier.toFixed(1);
  document.getElementById('finalNearMisses').textContent = score.nearMissCounter;
}

function resetGame() {
  // Clean up
  if (player) player.mesh.parent?.remove(player.mesh);
  if (particles) particles.dispose();
  for (const chunk of chunks) chunk.dispose();
  chunks = [];

  score.reset();
  difficulty = 0;

  // Re-init
  init();
  camera.position.set(0, 8, 0);
  camera.fov = 70;
  camera.updateProjectionMatrix();

  document.getElementById('hud').style.display = 'block';
  document.getElementById('gameOver').style.display = 'none';
  gameState = STATE.PLAYING;
}

// ============================================================
// INPUT HANDLING
// ============================================================
window.addEventListener('mousemove', (e) => {
  if (gameState !== STATE.PLAYING) return;
  // Map mouse position to normalized -1..1 range, then scale to gameplay area
  mouseNormX = (e.clientX / window.innerWidth) * 2 - 1;
  mouseNormY = (e.clientY / window.innerHeight) * 2 - 1;

  // Scale based on canyon width (decreases with difficulty)
  const scale = CANON_HALF_WIDTH_BASE - difficulty * 5;
  mouseX = mouseNormX * scale;
  mouseY = mouseNormY * scale;
});

window.addEventListener('mousedown', () => {
  mouseDown = true;
  if (gameState === STATE.PLAYING) player.startBoost();
});
window.addEventListener('mouseup', () => {
  mouseDown = false;
  if (gameState === STATE.PLAYING) player.endBoost();
});

window.addEventListener('keydown', (e) => {
  keysPressed[e.key.toLowerCase()] = true;
  if (e.key === 'f' && gameState === STATE.PLAYING) player.startBoost();
});
window.addEventListener('keyup', (e) => {
  keysPressed[e.key.toLowerCase()] = false;
});

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

// Start button
document.getElementById('startBtn').addEventListener('click', () => {
  document.getElementById('startScreen').style.display = 'none';
  audio.init();
  init();
  gameState = STATE.PLAYING;
  lastTime = performance.now();
});

// Restart button
document.getElementById('restartBtn').addEventListener('click', () => {
  resetGame();
  lastTime = performance.now();
});

// ============================================================
// RENDER LOOP
// ============================================================
function render(timestamp) {
  requestAnimationFrame(render);

  if (gameState !== STATE.PLAYING) {
    // Still render the scene in background for start screen
    if (gameState === STATE.STARTED && chunks.length === 0) {
      init();
      player.mesh.position.z = 20;
    }
    composer.render();
    return;
  }

  const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
  lastTime = timestamp;

  // Move player forward
  const distanceTraveled = player.update(dt, mouseX, mouseY);
  player.mesh.position.z += distanceTraveled;

  // Update chunks
  updateChunks(dt, distanceTraveled);

  // Update difficulty based on distance
  difficulty = Math.min(1, player.distance / 3000);

  // Check collisions
  const collisionResult = checkCollisions(player, chunks, particles);
  if (collisionResult.dead || player.isDead()) {
    showGameOver();
    composer.render();
    return;
  }

  // Update score
  score.update(dt, player.speed);

  // Animate orbs
  for (const chunk of chunks) {
    for (const orb of chunk.orbs) {
      if (orb.collected) continue;
      orb.mesh.rotation.y += dt * 3;
      orb.mesh.position.y += Math.sin(timestamp * 0.003 + orb.mesh.position.x) * 0.01;
    }
  }

  // Particles
  particles.emitTrail(player.mesh.position, player.isBoosting);
  for (const chunk of chunks) {
    for (const emitter of chunk.debrisEmitters) {
      emitter.timer -= dt * 5;
      if (emitter.timer <= 0 && Math.abs(emitter.z - player.mesh.position.z) < 60) {
        particles.emitDebris(
          new THREE.Vector3(emitter.x, emitter.y, emitter.z),
          player.speed
        );
        emitter.timer = 0.3 + Math.random() * 0.5;
      }
    }
  }
  particles.update(dt, player.speed, player.mesh.position);

  // Camera
  cam.update(dt, player.mesh.position, player.isBoosting, player.speed);

  // Audio update
  const speedRatio = player.speed / player.maxSpeed;
  audio.updateEngine(speedRatio);
  audio.updateWind(speedRatio);

  // HUD
  updateHUD();

  // Fog adjustment
  const fogDensity = 0.006 + (player.isBoosting ? 0.004 : 0) - difficulty * 0.001;
  scene.fog.density = fogDensity;

  // Render
  composer.render();
}

requestAnimationFrame(render);
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-Fable-Fusion-711-Uncensored-Heretic-NM-DAU-NEO-MAX-MTP-GGUF","provider":"brigid","persona":"full","sessionId":"934732f0-437c-4ae6-bb8d-bd39f1fbb19c","tokensIn":615229,"tokensOut":19946,"tokensTotal":635175,"cost":0,"turns":25,"toolCalls":24,"failedToolCalls":0,"timestamp":"2026-08-06T02:53:11.035Z"} -->