← Canyon Run results

Canyon Run

ornith 1.0 9b · full

Initial view of Canyon Run

Reviews & generation details

Reviews

Human review · Jul 17, 2026 · rubric 1

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

The reviewer marked this result as broken. It remains in the archive.

Visual design
Unscored
UX / feel
Unscored
Works
Unscored
Prompt spirit
Unscored
Delight
Unscored

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 51,216 bytes · SHA-256 1669bdca2e10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CANYON RACER</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
canvas { display: block; }

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

#speedometer {
  position: absolute; bottom: 30px; right: 40px;
  width: 200px; height: 80px;
}
.speed-line {
  position: absolute; bottom: 0; left: 15px;
  width: 170px; height: 6px; background: rgba(255,255,255,0.15);
  border-radius: 3px; overflow: hidden;
}
.speed-fill {
  position: absolute; bottom: 0; left: 0;
  width: 0%; height: 100%; background: linear-gradient(90deg, #ff8c42, #ffd700);
  border-radius: 3px; transition: width 0.1s;
}
.speed-label {
  position: absolute; top: -22px; left: 50%; transform: translateX(-50%);
  color: rgba(255,200,100,0.7); font-size: 11px; letter-spacing: 3px;
}

#multiplier {
  position: absolute; top: 40px; left: 50%; transform: translateX(-50%);
  font-size: 64px; color: #fff; text-shadow: 0 0 20px rgba(255,100,0,0.8), 0 0 40px rgba(255,50,0,0.5);
  font-weight: bold; letter-spacing: 6px; opacity: 0; transition: opacity 0.3s;
}

#energy-hud {
  position: absolute; bottom: 30px; left: 40px;
  width: 180px; height: 24px; background: rgba(0,0,0,0.5);
  border-radius: 12px; border: 1px solid rgba(255,100,50,0.3); overflow: hidden;
}
.energy-fill {
  position: absolute; top: 0; left: 0; height: 100%; width: 100%;
  background: linear-gradient(90deg, rgba(255,80,40,0.8), rgba(255,160,40,0.9));
  border-radius: 11px; transition: width 0.1s; box-shadow: 0 0 10px rgba(255,80,40,0.5);
}
.energy-label {
  position: absolute; top: -22px; left: 50%; transform: translateX(-50%);
  color: rgba(255,200,100,0.7); font-size: 11px; letter-spacing: 3px;
}

#game-over {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
  align-items: center; justify-content: center; z-index: 20; opacity: 0;
  pointer-events: none; transition: opacity 0.6s;
}
#game-over.visible { opacity: 1; pointer-events: auto; }

#go-title { color: #ffd700; font-size: 48px; margin-bottom: 20px; letter-spacing: 6px; text-shadow: 0 0 30px rgba(255,215,0,0.5); }
#go-stats { color: rgba(255,255,255,0.7); font-size: 18px; margin-bottom: 40px; text-align: center; line-height: 2; }

#restart-btn {
  background: transparent; border: 2px solid #ffd700; color: #ffd700;
  padding: 16px 40px; font-size: 18px; cursor: pointer; letter-spacing: 3px;
  transition: all 0.3s; text-transform: uppercase;
}
#restart-btn:hover { background: rgba(255,215,0,0.15); box-shadow: 0 0 20px rgba(255,215,0,0.4); }

#start-screen {
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.9); display: flex; flex-direction: column;
  align-items: center; justify-content: center; z-index: 20; opacity: 1; pointer-events: auto;
}
#start-title { color: #ffd700; font-size: 56px; margin-bottom: 20px; letter-spacing: 8px; text-shadow: 0 0 40px rgba(255,215,0,0.6); }
#start-subtitle { color: rgba(255,100,100,0.7); font-size: 18px; margin-bottom: 60px; letter-spacing: 4px; }
#start-btn { background: transparent; border: 2px solid #ffd700; color: #ffd700; padding: 16px 40px; font-size: 18px; cursor: pointer; letter-spacing: 3px; transition: all 0.3s; text-transform: uppercase; }
#start-btn:hover { background: rgba(255,215,0,0.15); box-shadow: 0 0 20px rgba(255,215,0,0.4); }

@keyframes pulse {
  0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); }
  50% { opacity: 0.7; transform: translateX(-50%) scale(1.1); }
}
.multiplier-pulse { animation: pulse 0.4s ease-in-out infinite; color: #ff6b35 !important; }

</style>
</head>
<body>

<div id="hud-overlay">
  <div id="multiplier"></div>
  <div class="speed-label" id="speed-label">SPEED</div>
  <div class="speed-line" id="speedometer"><div class="speed-fill" id="speed-fill"></div></div>
  <div class="energy-label" id="energy-label">ENERGY</div>
  <div id="energy-hud"><div class="energy-fill" id="energy-fill"></div></div>
</div>

<div id="game-over">
  <div id="go-title">WASTED</div>
  <div id="go-stats"></div>
  <button id="restart-btn">Restart Run</button>
</div>

<div id="start-screen">
  <div id="start-title">CANYON RACER</div>
  <div id="start-subtitle">FLOW • BANK • SURVIVE</div>
  <button id="start-btn">IGNITE ENGINE</button>
</div>

<script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>

<script>
/* ============================================================
   CANYON RACER - High-Speed Procedural Canyon Game
   Flight-based physics, procedural generation, bloom, audio
   ============================================================ */

// --- CONFIGURATION ---
const CFG = {
  worldChunkSize: 60,
  chunksAhead: 8,
  chunksBehind: 4,
  playerSpeedMax: 12,
  playerAccel: 0.03,
  playerFriction: 0.985,
  playerTurnRate: 0.015,
  playerBankStrength: 0.8,
  energyDecayRate: 0.45,
  energyRechargeRate: 2.5,
  energyRechargePerOrb: 35,
  multiplierBase: 1,
  multiplierNearMissBonus: 3,
  multiplierDecayRate: 0.85,
  boostMaxCharge: 100,
  boostChargeRate: 0.6,
  boostConsumptionRate: 2.0,
  boostSpeedMultiplier: 1.4,
  cameraLerp: 0.08,
  cameraVerticalOffset: 5.5,
  cameraHorizontalOffset: 9,
  groundY: -1,
  skyColorTop: new THREE.Color(0x2a0a3e),
  skyColorBottom: new THREE.Color(0xff6b35)
};

// --- AUDIO SYSTEM (Web Audio API) ---
class AudioSystem {
  constructor() { this.ctx = null; this.masterGain = null; }
  
  init() {
    if (!this.ctx) {
      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
      this.masterGain = this.ctx.createGain();
      this.masterGain.gain.value = 0.35;
      this.masterGain.connect(this.ctx.destination);
    }
    if (this.ctx.state === 'suspended') this.ctx.resume();
  }

  engineHum(speed) {
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    let osc = null, gain = null, filter = null;
    try {
      osc = this.ctx.createOscillator();
      gain = this.ctx.createGain();
      filter = this.ctx.createBiquadFilter();
      osc.type = 'sawtooth';
      osc.frequency.value = 40 + speed * 2.5;
      filter.type = 'lowpass';
      filter.frequency.value = 200 + speed * 30;
      gain.gain.setValueAtTime(0.15, now);
      osc.connect(filter).connect(gain).connect(this.masterGain);
      osc.start(now);
    } catch(e) {}
  }

  playPing() {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    try {
      const osc = this.ctx.createOscillator();
      const gain = this.ctx.createGain();
      osc.type = 'sine';
      osc.frequency.setValueAtTime(800, t);
      osc.frequency.exponentialRampToValueAtTime(1200, t + 0.15);
      gain.gain.setValueAtTime(0.3, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.3);
      osc.connect(gain).connect(this.masterGain);
      osc.start(t); osc.stop(t + 0.35);
    } catch(e) {}
  }

  playWhoosh() {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    try {
      const bufferSize = this.ctx.sampleRate * 1.2;
      const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
      const data = buffer.getChannelData(0);
      for (let i = 0; i < bufferSize; i++) {
        data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.3));
      }
      const src = this.ctx.createBufferSource();
      src.buffer = buffer;
      const gain = this.ctx.createGain();
      gain.gain.setValueAtTime(0.2, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 1.2);
      src.connect(gain).connect(this.masterGain);
      src.start(t);
    } catch(e) {}
  }

  playBoost() {
    if (!this.ctx) return;
    const now = this.ctx.currentTime;
    try {
      for (let i = 0; i < 5; i++) {
        setTimeout(() => {
          if (!this.ctx) return;
          const osc = this.ctx.createOscillator();
          const gain = this.ctx.createGain();
          osc.type = 'square';
          osc.frequency.value = 120 + Math.random() * 80;
          gain.gain.setValueAtTime(0.1, now);
          gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
          osc.connect(gain).connect(this.masterGain);
          osc.start(now); osc.stop(now + 0.2);
        }, i * 80);
      }
    } catch(e) {}
  }

  playCrash() {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    try {
      const bufferSize = this.ctx.sampleRate * 1.5;
      const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
      const data = buffer.getChannelData(0);
      for (let i = 0; i < bufferSize; i++) {
        data[i] = Math.random() * 2 - 1;
      }
      const src = this.ctx.createBufferSource();
      src.buffer = buffer;
      const gain = this.ctx.createGain();
      gain.gain.setValueAtTime(0.5, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 1);
      src.connect(gain).connect(this.masterGain);
      src.start(t);
    } catch(e) {}
  }
}

// --- PARTICLE SYSTEM ---
class ParticleSystem {
  constructor() { this.particles = []; this.count = 80; }

  addEngineTrail(pos, velocity, speedMult) {
    for (let i = 0; i < Math.min(3, CFG.playerSpeedMax / 2 + speedMult * 2); i++) {
      const offset = -velocity.length() * (i * 0.15);
      this.particles.push({
        pos: new THREE.Vector3(pos.x + (Math.random()-0.5)*1.5, pos.y + offset + (Math.random()-0.5)*2, pos.z),
        vel: velocity.clone().multiplyScalar(-speedMult * 0.3).add(new THREE.Vector3((Math.random()-0.5)*0.5, -0.2, Math.random()*0.3)),
        life: 1.0, maxLife: 0.8 + Math.random()*0.4,
        size: 0.3 + Math.random() * 0.7,
        color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1.0, 0.5 + Math.random()*0.3)
      });
    }
  }

  addDust(pos, velocity) {
    for (let i = 0; i < 4; i++) {
      this.particles.push({
        pos: new THREE.Vector3(pos.x + (Math.random()-0.5)*3, pos.y + 1 - Math.random()*2, pos.z),
        vel: velocity.clone().multiplyScalar(1 - 0.7*Math.random()).add(new THREE.Vector3((Math.random()-0.5)*0.8, -0.5-Math.random(), (Math.random()+1)*Math.random())),
        life: 0.6 + Math.random()*0.6, maxLife: 1.2,
        size: 0.4 + Math.random() * 1.2,
        color: new THREE.Color().setHSL(0.05, 0.7 + Math.random()*0.3, 0.4 + Math.random()*0.2)
      });
    }
  }

  update(dt) {
    for (let i = this.particles.length - 1; i >= 0; i--) {
      const p = this.particles[i];
      p.life -= dt * 60;
      if (p.life <= 0) continue;
      
      // Move
      p.pos.add(p.vel.clone().multiplyScalar(dt * 60));
      // Age color
      const ratio = p.life / p.maxLife;
      p.color.lerp(new THREE.Color(0.2, 0.1, 0.3), (1 - ratio) * 0.5);
      
      // Update geometry
      if (p.mesh && p.mesh.geometry.attributes.position.needsUpdate) {
        const pos = p.mesh.geometry.attributes.position;
        for (let j = 0; j < pos.count; j++) {
          pos.setXYZ(j, p.pos.x, p.pos.y + p.size * Math.sin(ratio * Math.PI), p.pos.z);
        }
        pos.needsUpdate = true;
      }
      if (p.mesh.material) {
        const mat = p.mesh.material;
        if (mat instanceof THREE.MeshBasicMaterial) mat.color.copy(p.color);
        if (mat instanceof THREE.PointsMaterial) mat.color.copy(p.color);
      }
    }
    
    // Cleanup dead particles
    this.particles = this.particles.filter((_, i) => {
      if (!this.particles[i]) return false;
      const p = this.particles[i];
      if (p.life <= 0 && p.mesh && p.parent) {
        p.parent.remove(p.mesh);
        return true;
      }
      return false;
    });
  }

  clear() { this.particles.length = 0; }
}

// --- SCENE SETUP ---
function createScene() {
  const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 0.8;
  
  const scene = new THREE.Scene();
  // Gradient sky
  const skyGeo = new THREE.SphereGeometry(200, 32, 16);
  const skyMat = new THREE.ShaderMaterial({
    uniforms: { topColor: { value: CFG.skyColorTop }, bottomColor: { value: CFG.skyColorBottom } },
    vertexShader: `varying vec3 vWorldPos; void main() { 
      vec4 wp = modelMatrix * vec4(position, 1.0); 
      vWorldPos = wp.xyz; 
      gl_Position = viewMatrix * wp; }`,
    fragmentShader: `uniform vec3 topColor; uniform vec3 bottomColor; 
      varying vec3 vWorldPos;
      void main() {
        float h = clamp((vWorldPos.y + 50.0) / 120.0, 0.0, 1.0);
        gl_FragColor = mix(vec4(bottomColor, 1), vec4(topColor, 1), h);
      }`,
    side: THREE.BackSide
  });
  scene.add(new THREE.Mesh(skyGeo, skyMat));

  // Ground plane (faint)
  const groundGeo = new THREE.PlaneGeometry(500, 200);
  const groundMat = new THREE.ShaderMaterial({
    uniforms: { uTime: { value: 0 }, uSpeed: { value: CFG.playerSpeedMax } },
    vertexShader: `varying vec2 vUv; varying float vYOffset; void main() {
      vUv = uv;
      vec4 pos = modelMatrix * vec4(position, 1.0);
      vYOffset = sin(pos.x * uSpeed * 0.3 + pos.z * uSpeed * 0.2) * 0.5;
      gl_Position = viewMatrix * pos; }`,
    fragmentShader: `uniform float uTime; uniform vec3 topColor; uniform vec3 bottomColor; 
      varying vec2 vUv; varying float vYOffset;
      void main() {
        vec3 color = mix(bottomColor, topColor, 0.4);
        float wave = sin(vUv.x * 8.0 + uTime) * 0.15 + cos(vUv.y * 6.0 + uTime * 0.7) * 0.1;
        color += vec3(wave, wave*0.5, wave*2);
        gl_FragColor = vec4(color, 0.8);
      }`,
    side: THREE.DoubleSide
  });
  scene.add(new THREE.Mesh(groundGeo, groundMat));

  // Lighting - warm sunset feel
  const ambientLight = new THREE.AmbientLight(0x3a1a5e, 0.6);
  scene.add(ambientLight);

  const dirLight = new THREE.DirectionalLight(0xffaa44, 2.5);
  dirLight.position.set(-20, 50, -10);
  dirLight.castShadow = true;
  dirLight.shadow.mapSize.set(2048, 2048);
  scene.add(dirLight);

  const fillLight = new THREE.DirectionalLight(0x7733aa, 0.8);
  fillLight.position.set(20, 15, -10);
  scene.add(fillLight);

  // Add renderer to DOM
  document.body.appendChild(renderer.domElement);

  // Camera
  const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 400);

  return { scene, renderer, camera };
}

// --- CANON GEOMETRY SHARED MESHES ---
let wallMeshLeft, wallMeshRight, floorMesh;
function createCanyonWalls() {
  // Left canyon wall (jagged polygon)
  const leftGeo = new THREE.PlaneGeometry(20, 180);
  wallMeshLeft = new THREE.Mesh(leftGeo, new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true, roughness: 0.9 }));
  wallMeshLeft.rotation.y = Math.PI / 2;
  wallMeshLeft.position.x = -100;
  
  const rightGeo = new THREE.PlaneGeometry(20, 180);
  wallMeshRight = new THREE.Mesh(rightGeo, new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true, roughness: 0.9 }));
  wallMeshRight.rotation.y = -Math.PI / 2;
  wallMeshRight.position.x = 100;

  // Floor strip (part of terrain)
  const floorGeo = new THREE.PlaneGeometry(60, 4);
  const floorMat = new THREE.MeshStandardMaterial({ color: 0x3a2518, flatShading: true });
  for (let i = -CFG.worldChunkSize/2; i < CFG.worldChunkSize/2; i++) {
    const strip = new THREE.Mesh(floorGeo.clone(), floorMat);
    strip.rotation.x = Math.PI / 2;
    strip.position.z = i * 60 + CFG.worldChunkSize / 2;
    scene.add(strip);
  }
}

// --- WORLD GENERATION ---
class WorldGenerator {
  constructor(scene) { this.scene = scene; this.chunks = []; this.playerZ = 0; }

  getChunkIndex(z) { return Math.floor(-z / CFG.worldChunkSize); }
  
  ensureChunksAhead() {
    const targetIdx = this.getChunkIndex(this.playerZ + CFG.worldChunkSize * CFG.chunksAhead);
    while (this.chunks.length <= targetIdx) {
      this.generateChunk(this.chunks.length);
    }
  }

  generateChunk(idx) {
    // Determine canyon width variation for this chunk
    let baseWidth = 70 + Math.sin(idx * 1.3 + 5) * 8;
    const squeezeIdx = idx % 15;
    if (squeezeIdx === 0 || squeezeIdx === 2 || squeezeIdx === 4) {
      baseWidth = 18 + Math.random() * 8; // Tight squeeze
    } else if (idx % 10 === 1) {
      baseWidth = 55 + Math.sin(idx * 2.7) * 12; // Moderate width with monoliths
    }

    const wallOffset = baseWidth / 2 - 8;
    
    // Generate left wall vertices (jagged)
    const leftVerts = [];
    for (let y = -80; y <= 50; y++) {
      let x = -wallOffset + Math.sin(y * 0.3 + idx * 2) * 1.5;
      if (y < -40) x += Math.random() * 2;
      leftVerts.push(new THREE.Vector3(x, y, this.chunks.length * CFG.worldChunkSize));
    }

    // Generate right wall vertices
    const rightVerts = [];
    for (let y = -80; y <= 50; y++) {
      let x = wallOffset + Math.sin(y * 0.3 + idx * 2) * 1.5 + 3;
      if (y < -40) x += Math.random() * 2;
      rightVerts.push(new THREE.Vector3(x, y, this.chunks.length * CFG.worldChunkSize));
    }

    // Create wall meshes using BufferGeometry
    const leftGeo = new THREE.BufferGeometry();
    leftGeo.setAttribute('position', new THREE.Float32BufferAttribute(leftVerts.map(v => [v.x, v.y, v.z]), 3));
    const leftMat = new THREE.MeshStandardMaterial({ color: squeezeIdx % 5 === 0 ? 0x6b4a35 : 0x4a3528, flatShading: true });
    const wallLeft = new THREE.Mesh(leftGeo, leftMat);
    this.scene.add(wallLeft);

    const rightGeo = new THREE.BufferGeometry();
    rightGeo.setAttribute('position', new THREE.Float32BufferAttribute(rightVerts.map(v => [v.x, v.y, v.z]), 3));
    const wallRight = new THREE.Mesh(rightGeo, leftMat);
    this.scene.add(wallRight);

    // Floor strip for this chunk
    const floorSegs = [];
    for (let i = -CFG.worldChunkSize/2; i < CFG.worldChunkSize/2; i += 5) {
      floorSegs.push(new THREE.Vector3(0, -1, i * CFG.worldChunkSize));
    }

    // Floating monoliths in moderate chunks
    if (idx % 10 === 1 && squeezeIdx !== 0) {
      this.placeMonolith(idx);
    }

    // Near-miss trigger zones
    const nearMissZ = (squeezeIdx !== 0 && squeezeIdx > 4) ? idx : -99;
    if (nearMissZ >= 0) {
      this.scene.userData.nearMissChunks.push(nearMissZ);
    }

    // Dust/debris particles floating in air for speed effect
    const debrisCount = baseWidth < 25 ? 12 : 8 + Math.floor(Math.random() * 4);
    for (let d = 0; d < debrisCount; d++) {
      const z = this.chunks.length * CFG.worldChunkSize - 30 + Math.random() * CFG.worldChunkSize;
      const x = (Math.random() - 0.5) * (baseWidth - 10);
      const y = -40 + Math.random() * 20 + Math.sin(z * 0.1 + d) * 3;
      this.scene.add(this.createDebris(x, y, z));
    }

    this.chunks.push({ baseWidth: baseWidth, wallOffset: wallOffset });
  }

  placeMonolith(idx) {
    const monoliths = [];
    for (let m = -3; m <= 3; m++) {
      if (Math.random() > 0.5 && Math.abs(m) < 2) {
        const x = (m * baseWidth / 6 + (Math.random()-0.5)*4) || -baseWidth/2 + 18;
        const size = 3 + Math.random() * 6;
        const geo = new THREE.ConeGeometry(size, size * 2.5, 4);
        const mat = new THREE.MeshStandardMaterial({ color: 0x5a4035, flatShading: true });
        const mono = new THREE.Mesh(geo, mat);
        mono.position.set(x, -1 + Math.random()*10, idx * CFG.worldChunkSize);
        if (m < 0) mono.rotation.y = -Math.PI / 2; else mono.rotation.y = Math.PI / 2;
        this.scene.add(mono);
        monoliths.push({ x: x || -baseWidth/2 + 18, z: idx * CFG.worldChunkSize });
      }
    }
    // Also place one in front of player if close enough
    const distToPlayer = Math.abs(idx * CFG.worldChunkSize);
    if (distToPlayer > 30 && distToPlayer < 50) {
      const mx = -baseWidth/2 + 18;
      const mz = idx * CFG.worldChunkSize;
      this.scene.userData.monoliths.push({ x: mx, z: mz, size: 4 });
    }
    return monoliths;
  }

  createDebris(x, y, z) {
    const geo = new THREE.SphereGeometry(0.5 + Math.random() * 1.5, 6, 6);
    const mat = new THREE.MeshBasicMaterial({ color: 0x8a7a60, wireframe: false });
    const d = new THREE.Mesh(geo, mat);
    d.position.set(x, y, z);
    this.scene.add(d);
    return d;
  }

  updatePlayerZ(z) {
    this.playerZ = z;
  }

  cleanupOldChunks() {
    const targetIdx = Math.floor(this.getChunkIndex(this.playerZ + CFG.worldChunkSize * CFG.chunksBehind));
    while (this.chunks.length > targetIdx && this.chunks[0]) {
      // Remove the oldest chunk's meshes from scene
      for (let m of this.scene.children) {
        if (m.userData.chunkIndex === 0 && !m.userData.isDebris) {
          this.scene.remove(m);
          break;
        }
      }
      this.chunks.shift();
    }
  }

  getWallPositions(z, width) {
    const wallOffset = width / 2 - 8;
    // Add some noise for natural feel
    return {
      left: -wallOffset + Math.sin((z % CFG.worldChunkSize) * 0.15 + 5) * 3,
      right: wallOffset + Math.cos((z % CFG.worldChunkSize) * 0.12 + 7) * 3 + 4
    };
  }

  getMonoliths() { return this.scene.userData.monoliths || []; }
}

// --- PLAYER PHYSICS & CRAFT ---
class PlayerCraft {
  constructor(sceneRef) {
    this.scene = sceneRef;
    // Craft geometry - sleek geometric shape
    const group = new THREE.Group();
    
    // Main body (elongated wedge)
    const bodyGeo = new THREE.ConeGeometry(0.8, 3, 6);
    bodyGeo.rotateX(Math.PI / 2);
    const bodyMat = new THREE.MeshStandardMaterial({ 
      color: 0x2a2a44, flatShading: true, metalness: 0.9, roughness: 0.15 
    });
    const body = new THREE.Mesh(bodyGeo, bodyMat);
    group.add(body);

    // Nose cone glow
    const noseGeo = new THREE.SphereGeometry(0.4, 8, 6);
    const noseMat = new THREE.MeshBasicMaterial({ color: 0xffaa33 });
    const nose = new THREE.Mesh(noseGeo, noseMat);
    nose.position.z = -1.5;
    group.add(nose);

    // Wing (left)
    const wingLGeo = new THREE.BoxGeometry(2.8, 0.15, 1.2);
    const wingL = new THREE.Mesh(wingLGeo, bodyMat.clone());
    wingL.position.set(-1.3, -0.1, 0.4);
    group.add(wingL);

    // Wing (right)
    const wingR = new THREE.Mesh(wingLGeo, bodyMat.clone());
    wingR.position.set(1.3, -0.1, 0.4);
    group.add(wingR);

    // Engine glow at back
    const engineGeo = new THREE.SphereGeometry(0.5, 8, 6);
    const engineMat = new THREE.MeshBasicMaterial({ color: 0xff4433 });
    this.engineGlow = new THREE.Mesh(engineGeo, engineMat);
    this.engineGlow.position.z = 1.2;
    group.add(this.engineGlow);

    // Engine light cone (spotlight)
    const light = new THREE.SpotLight(0xff6633, 5, 40, Math.PI / 6, 0.5, 1);
    light.position.set(0, -0.3, 0);
    light.target.position.set(0, -2, -8);
    group.add(light);
    group.add(light.target);

    // Small vertical stabilizer fins
    const finGeo = new THREE.BoxGeometry(0.15, 0.6, 0.4);
    const finL = new THREE.Mesh(finGeo, bodyMat.clone());
    finL.position.set(-0.8, -0.3, 0);
    group.add(finL);
    const finR = new THREE.Mesh(finGeo, bodyMat.clone());
    finR.position.set(0.8, -0.3, 0);
    group.add(finR);

    this.scene.add(group);
    
    // Physics state
    this.pos = new THREE.Vector3(0, CFG.groundY + 1, 50);
    this.vel = new THREE.Vector3(0, 0, -CFG.playerSpeedMax * 0.7);
    this.targetVel = new THREE.Vector3(0, 0, -CFG.playerSpeedMax * 0.7);
    
    // Orientation
    this.rotation = { x: Math.PI / 4, y: 0, z: 0 };
    this.pitchInput = 0;
    this.rollInput = 0;
    this.targetRoll = 0;

    // Flight state
    this.energy = CFG.energyRechargePerOrb;
    this.maxEnergy = 100;
    this.multiplier = CFG.multiplierBase;
    this.boostCharge = 0;
    this.boostActive = false;
    
    // Trail particles
    this.trailParticles = [];

    // Boost charge
    setInterval(() => { this.updateBoost(); }, 50);
    setInterval(() => { this.updateEnergyDecay(); }, 16);
  }

  updateBoost() {
    if (this.boostActive) {
      this.boostCharge -= CFG.boostConsumptionRate;
      this.vel.length(this.vel.x + Math.abs(this.targetVel.z) * 2.5 > CFG.playerSpeedMax * CFG.boostSpeedMultiplier ? 
        Math.min(this.vel.length(), CFG.playerSpeedMax * CFG.boostSpeedMultiplier + 10, CFG.playerSpeedMax * CFG.boostSpeedMultiplier) :
        Math.max(this.vel.length(), this.targetVel.length() - 3));
    } else {
      this.boostCharge += CFG.boostChargeRate;
    }
    if (this.boostCharge <= 0) { this.boostActive = false; }
    if (this.boostCharge >= CFG.boostMaxCharge) { this.boostActive = true; }
  }

  updateEnergyDecay() {
    this.energy -= CFG.energyDecayRate / 60;
    if (this.energy < 0) this.energy = 0;
  }

  applyInput(lateral, pitch) {
    // Lateral movement with inertia
    const lateralVel = this.vel.x - this.targetVel.z * CFG.playerTurnRate * CFG.playerSpeedMax / 20;
    this.targetRoll += lateral * CFG.playerBankStrength;
    
    // Pitch for acceleration/deceleration
    if (pitch > 0) {
      this.targetVel.z -= CFG.playerAccel;
    } else if (pitch < 0) {
      this.targetVel.z += CFG.playerAccel;
    }

    // Clamp speed
    const maxSpeed = this.boostActive ? CFG.playerSpeedMax * CFG.boostSpeedMultiplier : CFG.playerSpeedMax;
    this.targetVel.z = Math.max(-maxSpeed, Math.min(maxSpeed, this.targetVel.z));
    
    // Banking decay (roll returns to 0 when no lateral input)
    this.targetRoll *= 0.92;

    // Update rotation smoothly
    const rotSpeed = CFG.playerTurnRate * 60;
    const targetZRot = -Math.atan2(this.vel.x, this.targetVel.z);
    
    // Smooth yaw tracking with slight lead/lag for weight
    if (this.rollInput > 0) { this.rotation.y += rotSpeed * 0.5; }
    else if (this.rollInput < 0) { this.rotation.y -= rotSpeed * 0.5; }
    
    // Apply roll to craft
    const bankAngle = Math.min(this.targetRoll, Math.PI / 2.5);
    group.rotateZ(bankAngle);

    // Pitch for visual tilt
    if (pitch > 0) { group.rotation.x += 0.01; }
    else if (pitch < 0) { group.rotation.x -= 0.01; }
    
    this.rotation.z = bankAngle;
  }

  update(dt, world) {
    // Update velocity toward target with momentum
    const maxSpeed = this.boostActive ? CFG.playerSpeedMax * CFG.boostSpeedMultiplier : CFG.playerSpeedMax;
    let currentZ = this.vel.z;
    
    if (this.boostActive) {
      currentZ += dt * 60 * CFG.playerAccel * 1.5;
    } else {
      currentZ += dt * 60 * CFG.playerAccel * 0.3;
    }

    this.vel.z = Math.max(-maxSpeed, Math.min(maxSpeed, currentZ));
    
    // Apply friction to lateral velocity
    const lateralFriction = this.boostActive ? 0.98 : 0.975;
    this.vel.x *= lateralFriction;
    this.vel.y *= CFG.playerFriction;

    // Move forward
    const moveZ = this.vel.z * dt * 60;
    this.pos.z += moveZ;
    
    // Apply banking offset to position (visual effect of rolling)
    const bankOffsetX = -this.rotation.z * Math.tan(Math.abs(this.targetRoll)) * 0.5;
    this.pos.x += this.vel.x * dt * 60 + bankOffsetX;

    // Update energy based on speed
    if (Math.abs(this.vel.z) > maxSpeed * 0.8) {
      this.energy -= CFG.energyDecayRate * Math.abs(this.vel.z / maxSpeed);
    }

    // Collect orbs and interact with world
    this.checkOrbCollection(world);
    
    // Trail particles
    const speedMult = this.boostActive ? 1.5 : 0.8;
    this.trailParticles.push({ pos: new THREE.Vector3(this.pos.clone()), vel: this.vel.clone(), time: 0 });
    if (this.trailParticles.length > 6) this.trailParticles.shift();

    // Boost visual effects
    this.engineGlow.material = new THREE.MeshBasicMaterial({ color: this.boostActive ? 0xff2244 : 0xff8833 });
    const glowPulse = Math.sin(Date.now() * 0.01) * 0.3 + 0.7;
    this.engineGlow.scale.setScalar(glowPulse);

    // Update camera to follow
    const lookTarget = new THREE.Vector3(this.pos.x, CFG.groundY + CFG.cameraVerticalOffset, this.pos.z - CFG.cameraHorizontalOffset * 1.2);
    const camOffsetX = bankOffsetX;
    
    const targetCamPos = new THREE.Vector3(lookTarget.x - 0.5 * (this.rotation.z), lookTarget.y, lookTarget.z);
    this.scene.camera.position.lerp(targetCamPos, CFG.cameraLerp);
    
    // Camera FOV widens with boost
    if (this.boostActive) {
      this.scene.camera.fov = THREE.MathUtils.lerp(this.scene.camera.fov, 95, dt * 60 * 0.1);
    } else {
      this.scene.camera.fov = THREE.MathUtils.lerp(this.scene.camera.fov, 70, dt * 60 * 0.1);
    }
    
    this.scene.camera.updateProjectionMatrix();

    // Camera shake on boost or near-miss
    if (this.boostActive) {
      const shakeX = (Math.random() - 0.5) * 2;
      const shakeY = (Math.random() - 0.5) * 1.5;
      this.scene.camera.position.x += shakeX;
      this.scene.camera.position.y += shakeY + this.rotation.z;
    }

    // Update engine glow position with craft rotation
    const groupPos = new THREE.Vector3();
    group.getWorldPosition(groupPos);
    this.engineGlow.position.copy(this.pos).add(new THREE.Vector3(0, 1.5 * Math.sin(Date.now()*0.02), 0));

    return { pos: this.pos.clone(), vel: this.vel.clone() };
  }

  checkOrbCollection(world) {
    // Look ahead for orbs and collectibles
    const lookDist = CFG.worldChunkSize * 3;
    const zMin = this.pos.z - lookDist;
    const zMax = this.pos.z + lookDist;

    for (let i = world.orbs.length - 1; i >= 0; i--) {
      const orb = world.orbs[i];
      if (orb.collected || orb.mesh) continue;
      
      const distZ = Math.abs(orb.z - this.pos.z);
      if (distZ < lookDist && Math.abs(orb.x - this.pos.x) < 12 && Math.abs(orb.y - this.pos.y) < 3) {
        // Collect!
        world.orbs.splice(i, 1);
        this.energy = Math.min(this.maxEnergy, this.energy + CFG.energyRechargePerOrb);
        AudioSystem.playPing();
        
        // Create collection particle burst
        for (let p = 0; p < 8; p++) {
          const geo = new THREE.SphereGeometry(0.3, 6, 6);
          const mat = new THREE.MeshBasicMaterial({ color: 0xffdd44 });
          const mesh = new THREE.Mesh(geo, mat);
          mesh.position.copy(orb.pos.clone());
          this.scene.add(mesh);
          
          // Animate and remove
          const animStart = Date.now();
          setTimeout(() => {
            const elapsed = (Date.now() - animStart) / 100;
            if (elapsed < 300) {
              mesh.position.y += 2 * (elapsed / 300);
              mesh.material.opacity = 1 - (elapsed / 300);
            } else {
              this.scene.remove(mesh);
            }
          }, p * 50 + 200);
        }
      }
    }

    // Check monolith collision avoidance
    for (const mono of world.monoliths) {
      const dx = mono.x - this.pos.x;
      const dz = mono.z - this.pos.z;
      const dist = Math.sqrt(dx*dx + dz*dz);
      
      if (dist < 5) {
        // Too close to monolith - push back slightly
        AudioSystem.playWhoosh();
        const pushZ = MonoPos > this.pos.z ? -1 : 1;
        this.vel.z += pushZ * 20;
      }

      if (dist < 8 && dist > 3) {
        // Near-miss! Score multiplier increases
        const proximityBonus = Math.max(0, (8 - dist)) * 5;
        this.multiplier = Math.min(this.multiplier + proximityBonus / 10, 20);
      }
    }

    // Check near-miss with canyon walls from wall positions
    for (const z of world.nearMissZones) {
      if (z > this.pos.z && z < this.pos.z + CFG.worldChunkSize * 2) {
        const wx = world.getWallPosition(this.pos.z, world.currentWidth);
        const wallDist = Math.abs(wx - this.pos.x);
        if (wallDist < 6) {
          AudioSystem.playWhoosh();
          this.multiplier += 5;
        }
      }
    }

    // Decay multiplier slowly
    this.multiplier *= CFG.multiplierDecayRate / 60;
    if (this.multiplier <= CFG.multiplierBase) this.multiplier = CFG.multiplierBase;
  }

  getBoostCharge() { return this.boostCharge; }
}

// --- GAME ENGINE ---
const game = {
  audio: new AudioSystem(),
  particles: null,
  player: null,
  world: null,
  
  init() {
    const { scene, renderer, camera } = createScene();
    
    // Build initial canyon wall meshes
    function makeWall(verts) {
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.Float32BufferAttribute(verts.map(v => v.slice()), 3));
      return new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true }));
    }
    
    const lVerts = [];
    for (let y = -80; y <= 50; y += 2) {
      let x = -31 + Math.sin((y / 2) * 0.3 + 5) * 1.5; if (y < -40) x += Math.random() * 2; lVerts.push([x, y, 0]);
    }
    const rVerts = [];
    for (let y = -80; y <= 50; y += 2) {
      let x = 31 + Math.cos((y / 2) * 0.3 + 7) * 1.5 + 4; if (y < -40) x += Math.random() * 2; rVerts.push([x, y, 0]);
    }
    
    this.walls = { leftMesh: makeWall(lVerts), rightMesh: makeWall(rVerts) };
    scene.add(this.walls.leftMesh);
    scene.add(this.walls.rightMesh);

    // Store the actual THREE.Scene for later use (debris, orbs, monoliths all go here)
    this.sceneData = { scene };
    
    // Re-apply canyon wall generation as part of world
    this.worldChunks = [];
    for (let i = 0; i < CFG.worldChunkSize/60 + 3; i++) {
      const offset = 75 + Math.sin(i * 1.3) * 8;
      this.worldChunks.push({ baseWidth: offset, wallOffset: offset });
      
      // Add debris for speed effect
      const debrisCount = (offset < 20) ? 14 : 8 + Math.floor(Math.random() * 6);
      for (let d = 0; d < debrisCount; d++) {
        this.sceneData.scene.add(this.createDebris(
          (Math.random()-0.5) * (offset - 12),
          -40 + Math.random()*18 + Math.sin(i*0.1+d)*3,
          i * CFG.worldChunkSize - 30 + Math.random() * CFG.worldChunkSize
        ));
      }
    }

    this.sceneData.scene.userData.monoliths = [];
    this.sceneData.scene.userData.nearMissChunks = [-99];
    
    // Create initial orbs scattered in the canyon
    const orbPositions = [];
    for (let i = 0; i < 25; i++) {
      const z = -100 + Math.random() * CFG.worldChunkSize * 3;
      const chunkIdx = Math.max(0, Math.min(this.worldChunks.length - 1, Math.floor(-z / CFG.worldChunkSize)));
      const widthRange = this.worldChunks[chunkIdx] || { baseWidth: 80 };
      const orbW = (widthRange.baseWidth / 2) - 8 - 5; // Keep away from walls
      if (Math.abs(orbW) < 3) continue;
      
      const x = (Math.random() - 0.5) * Math.min(Math.abs(orbW), orbW + 1);
      const y = -20 + Math.random() * 40;
      orbPositions.push({ x, y, z });
    }

    // Player craft
    this.player = new PlayerCraft(this.sceneData.scene);
    
    // Particle system
    this.particles = new ParticleSystem();
    
    // World state
    this.worldState = {
      orbs: [],
      monoliths: [],
      nearMissZones: [-99],
      currentWidth: 80,
      widthVariation: 10,
      generationIndex: 0
    };

    for (const p of orbPositions) {
      this.worldState.orbs.push({ 
        pos: new THREE.Vector3(p.x + (Math.random()-0.5)*2, p.y, p.z),
        x: p.x, y: p.y, z: p.z
      });
      
      // Create orbital mesh with glow shader
      const geo = new THREE.SphereGeometry(1.8, 16, 8);
      const mat = new THREE.MeshBasicMaterial({ 
        color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.7),
        transparent: true, opacity: 0.9 
      });
      const mesh = new THREE.Mesh(geo, mat);
      this.sceneData.scene.add(mesh);
      
      // Outer glow ring
      const ringGeo = new THREE.TorusGeometry(3.5, 0.12, 8, 24);
      const ringMat = new THREE.MeshBasicMaterial({ 
        color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.6),
        transparent: true, opacity: 0.3
      });
      const ring = new THREE.Mesh(ringGeo, ringMat);
      ring.rotation.x = Math.PI / 2;
      this.sceneData.scene.add(ring);
      
      this.worldState.orbs[this.worldState.orbs.length - 1].mesh = mesh;
    }

    // Event listeners
    const btns = document.getElementById('start-btn');
    if (btns) btns.addEventListener('click', () => { 
      AudioSystem.init(); 
      this.startGame(); 
    });

    document.getElementById('restart-btn').addEventListener('click', () => {
      this.restart();
    });

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

    // Input handling
    this.keys = {};
    document.addEventListener('keydown', e => { this.keys[e.code] = true; });
    document.addEventListener('keyup', e => { this.keys[e.code] = false; });
    
    this.lastTime = performance.now();
  },

  createDebris(x, y, z) {
    const geo = new THREE.SphereGeometry(0.5 + Math.random() * 1.2, 6, 6);
    const mat = new THREE.MeshBasicMaterial({ 
      color: new THREE.Color().setHSL(0.03, 0.8, 0.4),
      wireframe: false 
    });
    const d = new THREE.Mesh(geo, mat);
    d.position.set(x, y, z);
    this.sceneData.scene.add(d);
    
    // Animate with speed flow effect
    const animateStart = Date.now();
    setTimeout(() => {
      const elapsed = (Date.now() - animateStart) / 100;
      if (elapsed < 250) {
        d.position.x += Math.sin(elapsed * 0.05) * 30;
        d.position.y -= (elapsed / 250) * 2;
      } else {
        this.sceneData.scene.remove(d);
      }
    }, 100 + Math.random() * 300);

    return d;
  },

  startGame() {
    document.getElementById('game-over').classList.remove('visible');
    document.getElementById('start-screen').style.opacity = '0';
    setTimeout(() => { this.isRunning = true; this.loop(); }, 500);
  },

  restart() {
    if (this.player) {
      // Remove old meshes that might be stuck
      this.sceneData.scene.children.forEach(c => {
        if (c.userData && c.userData.orbMesh) {
          this.sceneData.scene.remove(c);
        }
      });
    }
    
    const orbPositions = [];
    for (let i = 0; i < 25; i++) {
      const z = -150 + Math.random() * CFG.worldChunkSize * 3;
      const widthRange = this.worldChunks[Math.min(Math.floor(-z/60), this.worldChunks.length-1) || 0] || { baseWidth: 80 };
      const orbW = (widthRange.baseWidth / 2) - 8 - 5;
      if (Math.abs(orbW) < 3) continue;
      
      const x = (Math.random() - 0.5) * Math.min(Math.abs(orbW), orbW + 1);
      const y = -20 + Math.random() * 40;
      orbPositions.push({ x, y, z });
    }

    // Recreate orbs
    this.worldState.orbs.forEach((o, i) => {
      if (o.mesh) this.sceneData.scene.remove(o.mesh);
    });
    
    for (const p of orbPositions) {
      const orbEntry = { pos: new THREE.Vector3(p.x + (Math.random()-0.5)*2, p.y, p.z), x: p.x, y: p.y, z: p.z };
      
      const geo = new THREE.SphereGeometry(1.8, 16, 8);
      const mat = new THREE.MeshBasicMaterial({ 
        color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.7),
        transparent: true, opacity: 0.9 
      });
      const mesh = new THREE.Mesh(geo, mat);
      this.sceneData.scene.add(mesh);
      
      const ringGeo = new THREE.TorusGeometry(3.5, 0.12, 8, 24);
      const ringMat = new THREE.MeshBasicMaterial({ 
        color: new THREE.Color().setHSL(0.05 + Math.random()*0.02, 1, 0.6),
        transparent: true, opacity: 0.3
      });
      const ring = new THREE.Mesh(ringGeo, ringMat);
      ring.rotation.x = Math.PI / 2;
      this.sceneData.scene.add(ring);
      
      orbEntry.mesh = mesh;
      this.worldState.orbs.push(orbEntry);
    }

    // Reset player
    this.player.pos.set(0, CFG.groundY + 1, 50);
    const maxSpeed = CFG.playerSpeedMax * 0.7;
    this.player.vel.set(-maxSpeed, 0, -maxSpeed * 2.5);
    
    // Reset state
    this.player.energy = CFG.maxEnergy;
    this.player.multiplier = CFG.multiplierBase;
    this.player.boostCharge = 0;
    this.player.boostActive = false;
    this.player.rotation.set(0, 0, 0);
    this.player.targetRoll = 0;
    
    // Reset world state
    this.worldState.generationIndex = 0;
    this.worldState.nearMissZones = [-99];
    
    // Update player reference
    if (this.gamePlayerRef) {
      this.sceneData.remove(this.gamePlayerRef);
    }
    this.gamePlayerRef = this.player.group;
    this.sceneData.add(this.gamePlayerRef);

    this.lastTime = performance.now();
  },

  loop() {
    if (!this.isRunning || !this.player) return;
    
    const now = performance.now();
    const dt = Math.min((now - this.lastTime) / 1000, 0.05); // Cap delta time
    this.lastTime = now;

    // Get player position
    const playerPos = new THREE.Vector3();
    this.player.group.getWorldPosition(playerPos);
    
    // Update world generation ahead of player
    this.worldState.generationIndex = Math.floor(-playerPos.z / CFG.worldChunkSize) + CFG.chunksAhead;
    while (this.worldChunks.length <= this.worldState.generationIndex) {
      const idx = this.worldChunks.length - 1;
      const baseWidth = 70 + Math.sin(idx * 1.3 + 5) * 8;
      
      if ((idx % 15 === 0 || idx % 15 === 2 || idx % 15 === 4) && idx > 0) {
        this.worldState.currentWidth = 18 + Math.random() * 8; // Squeeze!
      } else if (idx % 10 === 1) {
        this.worldState.currentWidth = 55 + Math.sin(idx * 2.7) * 12; // Monolith chunk
      } else {
        this.worldState.currentWidth = baseWidth;
      }

      const wallOffset = this.worldState.currentWidth / 2 - 8;
      
      // Generate debris for speed effect in this chunk
      if (this.worldChunks[idx] && !this.worldChunks[idx].debrisGenerated) {
        const debrisCount = (this.worldState.currentWidth < 25) ? 14 : 8 + Math.floor(Math.random() * 6);
        for (let d = 0; d < debrisCount; d++) {
          const z = idx * CFG.worldChunkSize - 30 + Math.random() * CFG.worldChunkSize;
          const x = (Math.random() - 0.5) * (this.worldState.currentWidth - 12);
          const y = -40 + Math.random() * 20 + Math.sin(z * 0.1 + d) * 3;
          this.sceneData.scene.add(this.createDebris(x, y, z));
        }
        this.worldChunks[idx].debrisGenerated = true;
      }

      // Add monolith if needed
      const distToPlayer = Math.abs(idx * CFG.worldChunkSize);
      if (idx % 10 === 1 && idx > 2 && distToPlayer > 30 && distToPlayer < 60) {
        for (let m = -3; m <= 3; m++) {
          const mx = (m * 8 + (Math.random()-0.5)*4) || -this.worldState.currentWidth/2 + 18;
          const mz = idx * CFG.worldChunkSize;
          
          const monoGeo = new THREE.ConeGeometry(3, 6, 4);
          const monoMat = new THREE.MeshStandardMaterial({ color: 0x5a4035, flatShading: true });
          const monoMesh = new THREE.Mesh(monoGeo, monoMat);
          this.sceneData.scene.add(monoMesh);

          if (m < 0) monoMesh.rotation.y = -Math.PI / 2; else {
            monoMesh.rotation.y = Math.PI / 2;
            // Add second face for flat look
            const monoGeo2 = new THREE.ConeGeometry(3, 6, 4);
            const monoMesh2 = new THREE.Mesh(monoGeo2, monoMat.clone());
            this.sceneData.scene.add(monoMesh2);
          }

          if (m < 0) {
            this.worldState.monoliths.push({ x: mx || -this.worldState.currentWidth/2 + 18, z: mz, size: 4 });
          } else {
            this.worldState.monoliths.push({ x: mx || this.worldState.currentWidth/2 - 18, z: mz, size: 4 });
          }

          // Near-miss zone for monolith proximity scoring
          if (idx % 10 === 1 && idx > 3) {
            const nz = Math.floor(mz / CFG.worldChunkSize);
            if (!this.worldState.nearMissZones.includes(nz)) {
              this.worldState.nearMissZones.push(nz);
            }
          }
        }
      }

      // Update wall meshes positions (keep them in sync with generation)
      if (this.walls.leftMesh && this.walls.rightMesh) {
        const zPos = idx * CFG.worldChunkSize;
        const lVerts = [];
        for (let y = -80; y <= 50; y += 2) {
          let x = -wallOffset + Math.sin((y / 2) * 0.3 + idx * 2) * 1.5;
          if (y < -40) x += Math.random() * 2;
          lVerts.push([x, y, zPos]);
        }
        this.walls.leftMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute(lVerts, 3));
        
        const rVerts = [];
        for (let y = -80; y <= 50; y += 2) {
          let x = wallOffset + Math.sin((y / 2) * 0.3 + idx * 2) * 1.5 + 3;
          if (y < -40) x += Math.random() * 2;
          rVerts.push([x, y, zPos]);
        }
        this.walls.rightMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute(rVerts, 3));
      }

      this.worldChunks.push({ baseWidth: this.worldState.currentWidth, wallOffset: wallOffset, debrisGenerated: false });
    }

    // Apply player input (flight controls)
    const lateralInput = (this.keys['KeyA'] ? -1 : 0) + (this.keys['KeyD'] ? 1 : 0);
    const pitchInput = (this.keys['ArrowUp'] || this.keys['Space']) ? 1 : 
                       (this.keys['ArrowDown'] ? -1 : 0);

    // Only respond to input if moving forward
    if (Math.abs(this.player.vel.z) > CFG.playerSpeedMax * 0.3) {
      this.player.applyInput(lateralInput, pitchInput);
    } else {
      this.player.targetRoll *= 0.92;
    }

    // Update player physics
    const playerData = this.player.update(dt, this.worldState);

    // Check collisions with canyon walls
    const wallW = Math.max(this.worldChunks[Math.floor(-playerPos.z / CFG.worldChunkSize) || 0] || { baseWidth: 80 }).baseWidth;
    const wx = this.worldChunks[Math.floor(-playerPos.z / CFG.worldChunkSize) || 0] || { wallOffset: 31 };
    const leftWallX = -wx.wallOffset + Math.sin((-playerPos.z % CFG.worldChunkSize) * 0.15 + 5) * 3;
    const rightWallX = wx.wallOffset + Math.cos((-playerPos.z % CFG.worldChunkSize) * 0.12 + 7) * 3 + 4;
    
    // Simple distance-based collision with canyon walls (cylinder approximation)
    for (const wallX of [leftWallX, rightWallX]) {
      const dx = Math.abs(playerPos.x - wallX);
      if (dx < 6 && Math.abs(playerPos.z - ((playerPos.z % CFG.worldChunkSize) + cfg.worldChunkSize / 2)) < CFG.worldChunkSize * 0.5) {
        // Near-wall penalty
        const distToWall = dx;
        if (distToWall < 4) {
          this.player.multiplier += 3;
          AudioSystem.playWhoosh();
          
          // Push player back from wall
          const pushDir = playerPos.x > wallX ? -1 : 1;
          this.player.vel.z += pushDir * 8;
        }
      }
    }

    // Check game over (energy depleted)
    if (this.player.energy <= 0) {
      AudioSystem.playCrash();
      this.gameOver();
      return;
    }

    // Update particles
    this.particles.addEngineTrail(playerData.pos, playerData.vel);
    
    // Add dust when boosting
    if (this.player.boostActive || Math.abs(this.player.vel.z) > CFG.playerSpeedMax * 0.9) {
      this.particles.addDust(playerData.pos, playerData.vel);
    }

    this.particles.update(dt);

    // Update engine glow position
    const groupPos = new THREE.Vector3();
    this.player.group.getWorldPosition(groupPos);
    if (this.player.engineGlow) {
      this.player.engineGlow.position.copy(playerData.pos).add(new THREE.Vector3(0, 1.5 * Math.sin(Date.now()*0.02), 0));
    }

    // Update HUD
    const speedPct = Math.min(Math.abs(this.player.vel.z) / CFG.playerSpeedMax, 1);
    document.getElementById('speed-fill').style.width = (speedPct * 100) + '%';
    
    // Energy bar
    this.player.group.rotation.x += -this.keys['ArrowUp'] ? 0 : Math.min(this.keys['Space'], 0.02);
    document.getElementById('energy-fill').style.width = (this.player.energy / CFG.maxEnergy * 100) + '%';
    
    // Multiplier display
    const multEl = document.getElementById('multiplier');
    if (this.player.multiplier > 1.5) {
      multEl.textContent = Math.floor(this.player.multiplier);
      multEl.classList.add('multiplier-pulse');
    } else {
      multEl.style.opacity = '0';
    }

    // Update camera FOV and shake for boost
    if (this.player.boostActive) {
      const cam = this.sceneData.scene;
      const targetFOV = THREE.MathUtils.lerp(camera.fov, 95, dt * 60 * 0.1);
      
      const shakeX = (Math.random() - 0.5) * 2;
      const shakeY = (Math.random() - 0.5) * 1.5 + this.player.rotation.z;
      cam.camera.position.x += shakeX;
      cam.camera.position.y += shakeY;
    } else {
      camera.fov = THREE.MathUtils.lerp(camera.fov, 70, dt * 60 * 0.1);
    }

    // Engine glow pulse
    if (this.player.engineGlow) {
      const glowPulse = Math.sin(Date.now() * 0.015) * 0.3 + 0.7;
      this.player.engineGlow.scale.setScalar(glowPulse);
      const groupPos2 = new THREE.Vector3();
      this.player.group.getWorldPosition(groupPos2);
      this.player.engineGlow.position.copy(playerData.pos).add(new THREE.Vector3(0, 1.5 * Math.sin(Date.now()*0.02), 0));
    }

    // Ambient engine hum
    AudioSystem.engineHum(Math.abs(this.player.vel.z));
    
    // Render
    renderer.render(this.sceneData.scene, camera);

    requestAnimationFrame(() => this.loop());
  },

  gameOver() {
    this.isRunning = false;
    const statsEl = document.getElementById('go-stats');
    statsEl.innerHTML = `Final Score: ${Math.floor(this.player.multiplier * 1000)}<br>Max Speed: ${this.player.boostActive ? CFG.playerSpeedMax * CFG.boostSpeedMultiplier.toFixed(1) : CFG.playerSpeedMax.toFixed(1)} km<br>Energy at end: ${(this.player.energy / CFG.maxEnergy * 100).toFixed(0)}%`;
    document.getElementById('game-over').classList.add('visible');
  }
};

// Set up initial wall meshes in scene after creation
function createWallMeshes(scene) {
  const leftVerts = [];
  for (let y = -80; y <= 50; y += 2) {
    let x = -31 + Math.sin((y / 2) * 0.3 + 5) * 1.5;
    if (y < -40) x += Math.random() * 2;
    leftVerts.push([x, y, 0]);
  }
  
  const rightVerts = [];
  for (let y = -80; y <= 50; y += 2) {
    let x = 31 + Math.cos((y / 2) * 0.3 + 7) * 1.5 + 4;
    if (y < -40) x += Math.random() * 2;
    rightVerts.push([x, y, 0]);
  }

  const leftGeo = new THREE.BufferGeometry();
  leftGeo.setAttribute('position', new THREE.Float32BufferAttribute(leftVerts, 3));
  const leftMat = new THREE.MeshStandardMaterial({ color: 0x4a3528, flatShading: true });
  
  const wallLeft = new THREE.Mesh(leftGeo, leftMat);
  scene.add(wallLeft);

  const rightGeo = new THREE.BufferGeometry();
  rightGeo.setAttribute('position', new THREE.Float32BufferAttribute(rightVerts, 3));
  const wallRight = new THREE.Mesh(rightGeo, leftMat.clone());
  scene.add(wallRight);
  
  game.wallMeshLeft = wallLeft;
  game.wallMeshRight = wallRight;
}

// Create the debris generator function once
function createDebris(x, y, z) {
  const geo = new THREE.SphereGeometry(0.5 + Math.random() * 1.2, 6, 6);
  const mat = new THREE.MeshBasicMaterial({ 
    color: new THREE.Color().setHSL(0.03, 0.8, 0.4) 
  });
  const d = new THREE.Mesh(geo, mat);
  d.position.set(x, y, z);
  return d;
}

// Initialize everything on DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
  game.init();
});

</script>
</body>
</html>
<!-- agent-meta {"model":"ornith-1.0-9b","provider":"lmstudio","persona":"full","sessionId":"ff5d6ea8-695c-4597-bb1f-3c632e596a7e","tokensIn":2784622,"tokensOut":29413,"tokensTotal":2814035,"turns":40,"toolCalls":39,"failedToolCalls":2,"timestamp":"2026-07-13T17:26:35.399Z"} -->