← Canyon Run results

Canyon Run

muse spark 1.1 · composer

Initial view of Canyon Run

Reviews & generation details

Reviews

Human review · Jul 17, 2026 · rubric 1

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

Visual design
3 / 5
UX / feel
3 / 5
Works
4 / 5
Prompt spirit
3 / 5
Delight
3 / 5
Pretty good design; backwards ship model; plain terrain with weird artifacting

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 42,175 bytes · SHA-256 cfb570e69692
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CANYON RUNNER — Proximity Racer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.min.js"></script>
<style>
/* ── css: base ── */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700;800&family=Orbitron:wght@600;800&display=swap');
*{margin:0;padding:0;box-sizing:border-box;user-select:none}
html,body{width:100%;height:100%;overflow:hidden;background:#0b0713}
#app{position:relative;width:100vw;height:100vh}
#c{display:block;width:100%;height:100%}

#hud{position:absolute;inset:0;pointer-events:none;z-index:2}
#topBar{position:absolute;top:0;left:0;right:0;display:flex;justify-content:space-between;padding:18px 28px;align-items:flex-start}
.hud-group{font-family:'JetBrains Mono',monospace}
.left #score{font-family:'Orbitron',sans-serif;font-size:42px;font-weight:800;letter-spacing:-1px;color:#fff;text-shadow:0 0 12px rgba(255,126,41,0.8)}
#subScore{font-size:11px;color:rgba(255,255,255,0.55);letter-spacing:1.2px;margin-top:2px}
.center{display:flex;flex-direction:column;align-items:center;margin-top:6px}
#multiWrap{text-align:center}
#multiplier{font-family:'Orbitron',sans-serif;font-size:52px;font-weight:800;color:#ffe66d;transition:transform 0.1s ease;line-height:1;text-shadow:0 0 20px rgba(255,230,109,0.9)}
#multiplier.pulse{transform:scale(1.25)}
#multiLabel{font-size:9px;letter-spacing:3px;color:rgba(255,255,255,0.45);margin-top:4px}
.right{text-align:right}
#speedNum{font-family:'Orbitron',sans-serif;font-size:42px;font-weight:800;color:#fff;line-height:1}
#speedLabel{font-size:9px;letter-spacing:2.5px;color:rgba(255,255,255,0.4)}

#bottomBar{position:absolute;bottom:0;left:0;right:0;padding:18px 28px 22px;display:flex;gap:24px;align-items:flex-end}
#energyWrap{flex:1.2;max-width:420px}
#energyLabel,#turboLabel{font-family:'JetBrains Mono';font-size:9px;letter-spacing:2.5px;color:rgba(255,255,255,0.5);margin-bottom:8px}
#energyTrack{position:relative;height:6px;background:rgba(255,255,255,0.08);border-radius:99px;overflow:hidden}
#energyFill{position:absolute;left:0;top:0;bottom:0;width:100%;background:linear-gradient(90deg,#ff7b1a,#ffd23f);box-shadow:0 0 12px #ff7b1a;border-radius:99px;transition:width 0.15s linear}
#energyGlow{position:absolute;right:0;top:50%;width:22px;height:22px;transform:translate(50%,-50%);background:radial-gradient(circle,#ffdc6a 0%,transparent 70%);opacity:0.9;pointer-events:none}
#turboWrap{flex:0.9;max-width:320px}
#turboTrack{position:relative;height:6px;background:rgba(255,255,255,0.08);border-radius:99px;overflow:hidden}
#turboFill{position:absolute;left:0;top:0;bottom:0;width:0%;background:linear-gradient(90deg,#7afbff,#a78bfa);box-shadow:0 0 12px #7afbff;border-radius:99px;transition:width 0.1s linear}
#speedTrack{flex:0.6;max-width:220px;height:2px;background:rgba(255,255,255,0.06);position:relative;margin-bottom:2px;border-radius:99px;overflow:hidden}
#speedFill{position:absolute;left:0;top:0;bottom:0;width:40%;background:#fff;box-shadow:0 0 8px #fff;transition:width 0.2s ease}

#nearMiss{position:absolute;left:50%;top:48%;transform:translate(-50%,-50%) scale(0.8);font-family:'Orbitron';font-size:26px;font-weight:800;color:#fff;letter-spacing:2px;opacity:0;pointer-events:none;text-shadow:0 0 20px #ff7e2a;transition:all 0.18s cubic-bezier(.2,.8,.3,1)}
#nearMiss.show{opacity:1;transform:translate(-50%,-70%) scale(1.1)}
#centerReticle{position:absolute;left:50%;top:52%;width:26px;height:26px;border:1px solid rgba(255,255,255,0.13);border-radius:50%;transform:translate(-50%,-50%);pointer-events:none}
#centerReticle::after{content:'';position:absolute;left:50%;top:50%;width:4px;height:4px;background:rgba(255,255,255,0.3);transform:translate(-50%,-50%);border-radius:50%}

#overlay{position:absolute;inset:0;z-index:10;background:radial-gradient(120% 80% at 50% 10%, #2d1450 0%, #140a22 40%, #08050d 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity 0.6s ease, transform 0.6s ease}
#overlay.hidden{opacity:0;pointer-events:none;transform:scale(1.05)}
#titleBlock{text-align:center;margin-bottom:38px}
#gameTitle{font-family:'Orbitron';font-size:76px;font-weight:800;letter-spacing:-2px;color:#fff;line-height:0.9}
#gameTitle span{font-weight:600;color:#ff7e2a;text-shadow:0 0 30px rgba(255,126,42,0.8)}
#tagline{font-family:'JetBrains Mono';margin-top:14px;font-size:11px;letter-spacing:3px;color:rgba(255,255,255,0.5)}
#startCard{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);backdrop-filter:blur(18px);border-radius:16px;padding:22px 28px;min-width:420px}
.kbd{font-family:'JetBrains Mono';font-size:12px;color:rgba(255,255,255,0.7);margin:8px 0;letter-spacing:0.2px}
.kbd b{color:#fff;background:rgba(255,255,255,0.08);padding:2px 6px;border-radius:4px;font-size:11px;margin-right:4px}
.kbd i{color:#ffe66d;font-style:normal}
#playBtn{margin-top:18px;width:100%;font-family:'Orbitron';font-size:15px;font-weight:800;letter-spacing:2px;color:#0b0713;background:linear-gradient(90deg,#ff8c2a,#ffd23f);border:none;border-radius:10px;padding:14px;cursor:pointer;box-shadow:0 0 24px rgba(255,141,42,0.5);transition:transform 0.1s ease, box-shadow 0.2s ease}
#playBtn:hover{transform:translateY(-1px);box-shadow:0 0 34px rgba(255,141,42,0.7)}
#playBtn:active{transform:translateY(1px) scale(0.99)}
#statsEnd{margin-top:18px;border-top:1px solid rgba(255,255,255,0.08);padding-top:16px;display:grid;grid-template-columns:1fr 1fr;gap:10px}
#statsEnd.hidden{display:none}
.stat{display:flex;flex-direction:column;gap:2px}
.stat span{font-family:'JetBrains Mono';font-size:9px;letter-spacing:2px;color:rgba(255,255,255,0.4)}
.stat b{font-family:'Orbitron';font-size:18px;color:#fff}
#footerHint{position:absolute;bottom:16px;font-family:'JetBrains Mono';font-size:9px;letter-spacing:2.5px;color:rgba(255,255,255,0.22)}

@media (max-width:640px){#gameTitle{font-size:44px}#startCard{min-width:88vw}#bottomBar{flex-wrap:wrap}}
</style>
</head>
<body>
<div id="app">
  <canvas id="c"></canvas>
  <div id="hud">
    <div id="topBar">
      <div class="hud-group left">
        <div id="score">00000</div>
        <div id="subScore"><span id="dist">0m</span> • <span id="maxSpeed">0 km/h</span></div>
      </div>
      <div class="hud-group center">
        <div id="multiWrap">
          <div id="multiplier">x1.0</div>
          <div id="multiLabel">MULTIPLIER</div>
        </div>
      </div>
      <div class="hud-group right">
        <div id="speedNum">0</div>
        <div id="speedLabel">KM/H</div>
      </div>
    </div>
    <div id="bottomBar">
      <div id="energyWrap">
        <div id="energyLabel">ENERGY</div>
        <div id="energyTrack"><div id="energyFill"></div><div id="energyGlow"></div></div>
      </div>
      <div id="turboWrap">
        <div id="turboLabel">TURBO <span id="turboKey">[ SHIFT ]</span></div>
        <div id="turboTrack"><div id="turboFill"></div></div>
      </div>
      <div id="speedTrack"><div id="speedFill"></div></div>
    </div>
    <div id="nearMiss">NEAR MISS +<span id="nearVal">0</span></div>
    <div id="centerReticle"></div>
  </div>
  <div id="overlay">
    <div id="titleBlock">
      <h1 id="gameTitle">CANYON<span>RUNNER</span></h1>
      <p id="tagline">FLIGHT PHYSICS • PROXIMITY SCORING • INFINITE PROCEDURAL</p>
    </div>
    <div id="startCard">
      <div class="kbd"><b>WASD / ARROWS / MOUSE</b> — BANK &amp; PITCH</div>
      <div class="kbd"><b>SHIFT / SPACE</b> — TURBO BURN</div>
      <div class="kbd">GRAZE WALLS FOR <i>xMULTIPLIER</i> • COLLECT ORBS FOR ENERGY</div>
      <button id="playBtn">IGNITE ENGINES [ ENTER ]</button>
      <div id="statsEnd" class="hidden">
        <div class="stat"><span>FINAL SCORE</span><b id="finalScore">0</b></div>
        <div class="stat"><span>MAX SPEED</span><b id="finalMax">0</b></div>
        <div class="stat"><span>MAX MULTIPLIER</span><b id="finalMulti">x1.0</b></div>
        <div class="stat"><span>ORB COLLECTED</span><b id="finalOrbs">0</b></div>
      </div>
    </div>
    <div id="footerHint">No assets • 60fps Low-Poly • Audio Synthetic</div>
  </div>
</div>
<script>
'use strict';

// ── const: CFG ──
const CFG = {
  CHUNK_LEN: 140,
  VISIBLE_CHUNKS: 7,
  CANYON_BASE_WIDTH: 52,
  CANYON_MIN_WIDTH: 18,
  PLAYER_BOUNDS_X: 22,
  PLAYER_BOUNDS_Y: 14,
  BASE_SPEED: 62,
  MAX_SPEED: 260,
  ACCEL: 18,
  TURBO_SPEED: 120,
  ENERGY_DECAY: 9.5,
  ENERGY_ORB: 22,
  TURBO_GAIN_NEAR: 12,
  TURBO_GAIN_ORB: 8,
  FOV_BASE: 74,
  FOV_TURBO: 98,
};

// ── const: PAL ──
const PAL = {
  fog: 0x130a28,
  ground: 0x201436,
  wall: 0x3a2a5e,
  wall2: 0xff7e2a,
  orb: 0xffd23f,
  ship: 0xe8e6ef,
};

// ── function: lerp ──
function lerp(a,b,t){return a+(b-a)*t}

// ── function: clamp ──
function clamp(v,a,b){return Math.max(a,Math.min(b,v))}

// ── function: smoothstep ──
function smoothstep(e0,e1,x){ const t=clamp((x-e0)/(e1-e0),0,1); return t*t*(3-2*t)}

// ── function: rand ──
function rand(a=0,b=1){return a+(b-a)*Math.random()}

// ── function: hash ──
function hash(n){ // deterministic pseudo
  const x = Math.sin(n*127.1)*43758.5453;
  return x - Math.floor(x);
}

// ── class: AudioEngine ──
class AudioEngine {
  constructor(){
    this.ctx = null;
    this.master = null;
    this.engineOsc = null;
    this.engineGain = null;
    this.on = false;
  }
  init(){
    if(this.ctx) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.28;
    this.master.connect(this.ctx.destination);
    // engine hum
    this.engineGain = this.ctx.createGain();
    this.engineGain.gain.value = 0;
    this.engineGain.connect(this.master);
    this.engineOsc = this.ctx.createOscillator();
    this.engineOsc.type = 'sawtooth';
    this.engineOsc.frequency.value = 58;
    const low = this.ctx.createBiquadFilter();
    low.type='lowpass'; low.frequency.value=420;
    this.engineOsc.connect(low);
    low.connect(this.engineGain);
    this.engineOsc.start();
    // second layer
    this.engineOsc2 = this.ctx.createOscillator();
    this.engineOsc2.type='sine';
    this.engineOsc2.frequency.value=110;
    this.engineGain2 = this.ctx.createGain(); this.engineGain2.gain.value=0;
    this.engineOsc2.connect(this.engineGain2);
    this.engineGain2.connect(this.master);
    this.engineOsc2.start();
    this.on=true;
  }
  setEngine(speedNorm, turbo){
    if(!this.ctx) return;
    const f = 48 + speedNorm*140 + turbo*120;
    if(this.engineOsc) this.engineOsc.frequency.linearRampToValueAtTime(f, this.ctx.currentTime+0.12);
    if(this.engineOsc2) this.engineOsc2.frequency.linearRampToValueAtTime(f*1.015, this.ctx.currentTime+0.12);
    this.engineGain.gain.linearRampToValueAtTime(0.06+speedNorm*0.16+turbo*0.12, this.ctx.currentTime+0.08);
    this.engineGain2.gain.linearRampToValueAtTime(0.02+speedNorm*0.06, this.ctx.currentTime+0.08);
  }
  blip(type='orb'){
    if(!this.ctx) return;
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.connect(g); g.connect(this.master);
    const t = this.ctx.currentTime;
    if(type==='orb'){
      o.type='sine'; o.frequency.setValueAtTime(660,t); o.frequency.exponentialRampToValueAtTime(1320,t+0.18);
      g.gain.setValueAtTime(0.6,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.35);
      o.start(t); o.stop(t+0.36);
    }else if(type==='near'){
      o.type='triangle'; o.frequency.setValueAtTime(300,t); o.frequency.linearRampToValueAtTime(880,t+0.12);
      g.gain.setValueAtTime(0.25,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.25);
      o.start(t); o.stop(t+0.26);
    }else if(type==='hit'){
      o.type='sawtooth'; o.frequency.setValueAtTime(160,t); o.frequency.linearRampToValueAtTime(20,t+0.4);
      const f = this.ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=800;
      o.disconnect(); o.connect(f); f.connect(g);
      g.gain.setValueAtTime(0.7,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.5);
      o.start(t); o.stop(t+0.5);
    }else if(type==='turbo'){
      o.type='sine'; o.frequency.setValueAtTime(200,t); o.frequency.exponentialRampToValueAtTime(900,t+0.6);
      g.gain.setValueAtTime(0.5,t); g.gain.linearRampToValueAtTime(0,t+0.6);
      o.start(t); o.stop(t+0.6);
    }
  }
}

// ── class: InputSys ──
class InputSys {
  constructor(){
    this.x=0; this.y=0; // -1..1
    this.turboHeld=false;
    this._keys={};
    this._mouseDown=false;
    this._mx=0; this._my=0;
  }
  attach(canvas){
    window.addEventListener('keydown', (e)=>{
      this._keys[e.code]=true;
      if(e.code==='Space'||e.code==='ShiftLeft'||e.code==='ShiftRight') this.turboHeld=true;
    });
    window.addEventListener('keyup', (e)=>{
      this._keys[e.code]=false;
      if(e.code==='Space'||e.code==='ShiftLeft'||e.code==='ShiftRight') this.turboHeld=false;
    });
    canvas.addEventListener('pointerdown', (e)=>{ this._mouseDown=true; canvas.setPointerCapture(e.pointerId); });
    window.addEventListener('pointerup', ()=>{ this._mouseDown=false; });
    canvas.addEventListener('pointermove', (e)=>{
      if(this._mouseDown){
        this._mx = (e.clientX / window.innerWidth -0.5)*2;
        this._my = -(e.clientY / window.innerHeight -0.5)*2;
      }
    });
    // gyro fallback not needed
  }
  update(dt){
    let ix=0, iy=0;
    if(this._keys['KeyA']||this._keys['ArrowLeft']) ix-=1;
    if(this._keys['KeyD']||this._keys['ArrowRight']) ix+=1;
    if(this._keys['KeyW']||this._keys['ArrowUp']) iy+=1;
    if(this._keys['KeyS']||this._keys['ArrowDown']) iy-=1;
    // blend keyboard and mouse
    if(this._mouseDown){
      ix = lerp(ix, this._mx*1.2, 0.5);
      iy = lerp(iy, this._my*0.9, 0.5);
    }
    // smooth input
    this.x = lerp(this.x, clamp(ix,-1,1), 1 - Math.pow(0.001, dt*60));
    this.y = lerp(this.y, clamp(iy,-1,1), 1 - Math.pow(0.001, dt*60));
    if(!this._mouseDown && Math.abs(ix)<0.01 && Math.abs(iy)<0.01){
      // decay
      this.x *= Math.pow(0.9, dt*60);
      this.y *= Math.pow(0.9, dt*60);
    }
  }
}

// ── function: makeShipGroup ──
function makeShipGroup(){
  const g = new THREE.Group();
  // main hull - sleek dart
  const hullGeo = new THREE.BufferGeometry();
  // vertices for low-poly ship: front tip, back, wings
  const verts = new Float32Array([
    0,0,6,   -1.6,0,0,   1.6,0,0, // top face?
    0,0,6,   1.6,0,0,   0, -0.6, 1.2,
    0,0,6,   0,-0.6,1.2, -1.6,0,0,
    -1.6,0,0, 0,-0.6,1.2, 0,0,-3,
    1.6,0,0, 0,0,-3, 0,-0.6,1.2,
    -1.6,0,0, 0,0,-3, 0,0.45, -0.6,
    1.6,0,0, 0,0.45,-0.6, 0,0,-3,
    -1.6,0,0, 0,0.45,-0.6, 0,0,0,
    1.6,0,0, 0,0,0, 0,0.45,-0.6,
  ]);
  hullGeo.setAttribute('position', new THREE.BufferAttribute(verts,3));
  hullGeo.computeVertexNormals();
  const hullMat = new THREE.MeshStandardMaterial({color:PAL.ship, flatShading:true, metalness:0.3, roughness:0.45, emissive:0x101018, emissiveIntensity:0.2});
  const hull = new THREE.Mesh(hullGeo, hullMat);
  g.add(hull);

  // cockpit glow
  const cockGeo = new THREE.BoxGeometry(0.6,0.28,1.2);
  const cockMat = new THREE.MeshStandardMaterial({color:0x89dfff, emissive:0x2ab8ff, emissiveIntensity:1.2, transparent:true, opacity:0.9});
  const cock = new THREE.Mesh(cockGeo, cockMat);
  cock.position.set(0,0.28,0.2);
  g.add(cock);

  // engine glows
  const engGeo = new THREE.ConeGeometry(0.45,1.2,6);
  engGeo.rotateX(Math.PI);
  const engMat = new THREE.MeshStandardMaterial({color:0xff7e2a, emissive:0xff4f00, emissiveIntensity:2.2, transparent:true, opacity:0.9});
  const eng = new THREE.Mesh(engGeo, engMat);
  eng.position.set(0,-0.2,-3.2);
  g.add(eng);
  g.userData.engMat = engMat;
  g.userData.hullMat = hullMat;

  // wing tip lights
  const lightGeo = new THREE.SphereGeometry(0.18,6,6);
  const lightMat = new THREE.MeshStandardMaterial({emissive:0xffffff, emissiveIntensity:1.5, color:0xffffff});
  const lw = new THREE.Mesh(lightGeo, lightMat); lw.position.set(-1.6,0,0); g.add(lw);
  const rw = new THREE.Mesh(lightGeo, lightMat.clone()); rw.position.set(1.6,0,0); g.add(rw);
  rw.material.emissive.setHex(0xff2a2a);
  lw.material.emissive.setHex(0x2aff9c);

  // trail plane
  const trailGeo = new THREE.PlaneGeometry(1.2, 5);
  const trailMat = new THREE.MeshBasicMaterial({color:0xff8a2a, transparent:true, opacity:0.0, blending:THREE.AdditiveBlending, side:THREE.DoubleSide});
  const trail = new THREE.Mesh(trailGeo, trailMat);
  trail.position.set(0,-0.2,-5.7);
  trail.rotation.x = Math.PI*0.45;
  g.add(trail);
  g.userData.trailMat = trailMat;

  return g;
}

// ── function: makeOrbMesh ──
function makeOrbMesh(){
  const geo = new THREE.OctahedronGeometry(1.1,0);
  const mat = new THREE.MeshStandardMaterial({color:0xffe9a0, emissive:0xff9b00, emissiveIntensity:1.8, flatShading:true});
  const m = new THREE.Mesh(geo, mat);
  // core
  const coreGeo = new THREE.SphereGeometry(0.35,8,8);
  const coreMat = new THREE.MeshBasicMaterial({color:0xfffff0});
  const core = new THREE.Mesh(coreGeo, coreMat);
  m.add(core);
  // glow sprite
  const sprMat = new THREE.MeshBasicMaterial({color:0xffb347, transparent:true, opacity:0.35, blending:THREE.AdditiveBlending});
  const glowGeo = new THREE.SphereGeometry(2.2,10,10);
  const glow = new THREE.Mesh(glowGeo, sprMat);
  m.add(glow);
  m.userData.glowMat = sprMat;
  return m;
}

// ── class: Chunk ──
class Chunk {
  constructor(index, zStart){
    this.index=index;
    this.zStart=zStart;
    this.zEnd=zStart-CFG.CHUNK_LEN;
    this.group=new THREE.Group();
    this.group.position.z=zStart;
    this.colliders=[]; // {box:THREE.Box3, type:'wall'|'obs', mesh}
    this.orbs=[];
    this.lights=[];
  }
  dispose(scene){
    scene.remove(this.group);
    this.group.traverse(o=>{
      if(o.geometry) o.geometry.dispose();
    });
  }
}

// ── class: WorldGenerator ──
class WorldGenerator {
  constructor(scene){
    this.scene=scene;
    this.chunks=[];
    this.nextZ=0;
    this.nextIndex=0;
  }
  drand(i, salt){
    return hash(i*12.9898 + salt*78.233 + 0.123) ;
  }
  generateInitial(){
    for(let k=0;k<CFG.VISIBLE_CHUNKS;k++){
      this.pushChunk();
    }
  }
  pushChunk(){
    const idx=this.nextIndex++;
    const z0=this.nextZ;
    this.nextZ-=CFG.CHUNK_LEN;
    const ch=new Chunk(idx,z0);
    const difficulty = Math.min(1.9, 0.2 + idx*0.04);

    const canyonGap = lerp(CFG.CANYON_BASE_WIDTH, CFG.CANYON_MIN_WIDTH, clamp(difficulty/1.6,0,1));
    const centerOffset = (this.drand(idx,1)-0.5) * 14 * Math.min(1, difficulty*0.7);
    const wallHeight=70 + this.drand(idx,2)*30;
    const wallThickness=60;
    const leftWallLen = CFG.CHUNK_LEN + 4;
    const leftGeo = new THREE.BoxGeometry(wallThickness, wallHeight, leftWallLen, 2, 3, 4);
    const posAttr = leftGeo.attributes.position;
    for(let v=0; v<posAttr.count; v++){
      const y = posAttr.getY(v);
      if(y> wallHeight*0.3){
        const disp = (this.drand(idx+v*0.1,9)-0.5)*8;
        posAttr.setY(v, y+disp);
        posAttr.setX(v, posAttr.getX(v)+ (this.drand(idx+v,11)-0.5)*4);
      }
    }
    posAttr.needsUpdate=true; leftGeo.computeVertexNormals();
    const hueShift = this.drand(idx,3) >0.5 ? 0x3d2e5e : 0x4a2d3a;
    const matA = new THREE.MeshStandardMaterial({color:hueShift, flatShading:true, roughness:0.85, metalness:0.1});
    const leftMesh = new THREE.Mesh(leftGeo, matA);
    leftMesh.position.set(centerOffset - canyonGap/2 - wallThickness/2, wallHeight/2-12, -leftWallLen/2+2);
    ch.group.add(leftMesh);
    ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(leftMesh.position.clone(), new THREE.Vector3(wallThickness, wallHeight, leftWallLen)), mesh:leftMesh, type:'wall', half:new THREE.Vector3(wallThickness/2, wallHeight/2, leftWallLen/2)});

    const rightGeo = new THREE.BoxGeometry(wallThickness, wallHeight, leftWallLen, 2,3,4);
    const pos2 = rightGeo.attributes.position;
    for(let v=0; v<pos2.count; v++){
      const y = pos2.getY(v);
      if(y> wallHeight*0.3){
        const disp = (this.drand(idx+v*0.13,13)-0.5)*8;
        pos2.setY(v, y+disp);
        pos2.setX(v, pos2.getX(v)+ (this.drand(idx+v,17)-0.5)*4);
      }
    }
    pos2.needsUpdate=true; rightGeo.computeVertexNormals();
    const rightMesh = new THREE.Mesh(rightGeo, matA.clone());
    rightMesh.position.set(centerOffset + canyonGap/2 + wallThickness/2, wallHeight/2-12, -leftWallLen/2+2);
    ch.group.add(rightMesh);
    ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(rightMesh.position.clone(), new THREE.Vector3(wallThickness, wallHeight, leftWallLen)), mesh:rightMesh, type:'wall', half:new THREE.Vector3(wallThickness/2, wallHeight/2, leftWallLen/2)});

    const floorGeo = new THREE.PlaneGeometry(200, CFG.CHUNK_LEN+2, 4,2);
    const floorMat = new THREE.MeshStandardMaterial({color:0x1a0f2f, roughness:1, metalness:0});
    const fp = floorGeo.attributes.position;
    for(let v=0; v<fp.count; v++){
      const z = fp.getY(v);
      fp.setZ(v, (this.drand(idx+v*0.21,19)-0.5)*3 + Math.sin(z*0.02+idx)*2);
    }
    fp.needsUpdate=true;
    const floor = new THREE.Mesh(floorGeo, floorMat);
    floor.rotation.x = -Math.PI/2;
    floor.position.set(centerOffset, -8, -CFG.CHUNK_LEN/2);
    ch.group.add(floor);

    const obsCount = Math.floor(1 + difficulty*2.2 + this.drand(idx,5)*1.2);
    for(let o=0;o<obsCount;o++){
      const t = this.drand(idx*10+o,7);
      const zLocal = - (t*0.78+0.08)*CFG.CHUNK_LEN - rand(0,10);
      const typeRoll = this.drand(idx+o,23);
      if(typeRoll < 0.44){
        const w = 2.5 + this.drand(idx+o,29)*4;
        const h = 10 + this.drand(idx+o,31)*24;
        const d = 2.5 + this.drand(idx+o,37)*3;
        const geo = new THREE.BoxGeometry(w,h,d);
        const mat = new THREE.MeshStandardMaterial({color: typeRoll<0.22?0xff8552:0x6d507a, flatShading:true, roughness:0.7});
        const m = new THREE.Mesh(geo, mat);
        const xOff = (this.drand(idx+o*1.7,41)-0.5)* (canyonGap-8);
        m.position.set(centerOffset + xOff, -4 + h/2, zLocal);
        ch.group.add(m);
        ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(m.position.clone(), new THREE.Vector3(w,h,d)), mesh:m, type:'obs', half:new THREE.Vector3(w/2,h/2,d/2)});
      }else if(typeRoll < 0.74){
        const gap = Math.max(7.5 - difficulty*2, 5.2) + this.drand(idx+o,43)*1.2;
        const gapX = centerOffset + (this.drand(idx+o,47)-0.5)*(canyonGap*0.25);
        const sideW=5, sideH=34, sideD=4;
        for(let side of [-1,1]){
          const geo = new THREE.BoxGeometry(sideW, sideH, sideD);
          const mat = new THREE.MeshStandardMaterial({color:0xb86f4c, flatShading:true});
          const mm = new THREE.Mesh(geo, mat);
          mm.position.set(gapX + side*(gap/2+sideW/2), sideH/2-8, zLocal);
          ch.group.add(mm);
          ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(mm.position.clone(), new THREE.Vector3(sideW,sideH,sideD)), mesh:mm, type:'obs', half:new THREE.Vector3(sideW/2,sideH/2,sideD/2)});
        }
        if(this.drand(idx+o,53)>0.5){
          const beamGeo = new THREE.BoxGeometry(gap+sideW*2, 2.2, 2.8);
          const beamMat = new THREE.MeshStandardMaterial({color:0xffb074, emissive:0xff6a2a, emissiveIntensity:0.25, flatShading:true});
          const beam = new THREE.Mesh(beamGeo, beamMat);
          beam.position.set(gapX, 12 + this.drand(idx+o,59)*6, zLocal);
          ch.group.add(beam);
          ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(beam.position.clone(), new THREE.Vector3(gap+sideW*2,2.2,2.8)), mesh:beam, type:'obs', half:new THREE.Vector3(gap/2+sideW,1.1,1.4)});
        }
      }else{
        const w=14, h=2.3, d=2.0;
        const geo = new THREE.BoxGeometry(w,h,d);
        const mat = new THREE.MeshStandardMaterial({color:0x8a5ae0, flatShading:true});
        const mm = new THREE.Mesh(geo, mat);
        const xOff = (this.drand(idx+o,61)-0.5)*(canyonGap-12);
        mm.position.set(centerOffset+xOff, 2 + this.drand(idx+o,67)*10, zLocal);
        mm.rotation.z = (this.drand(idx+o,71)-0.5)*0.9;
        ch.group.add(mm);
        ch.colliders.push({box:new THREE.Box3().setFromCenterAndSize(mm.position.clone(), new THREE.Vector3(w/2+1, h/2+1, d/2+1)), mesh:mm, type:'obs', half:new THREE.Vector3(w/2+1,h/2+1,d/2+1)});
      }
    }

    const orbCount = 2 + Math.floor(this.drand(idx,83)*2 + difficulty*0.85);
    for(let k=0;k<orbCount;k++){
      const tk = (k+0.5)/orbCount + (this.drand(idx+k,89)-0.5)*0.18;
      const zz = -tk*CFG.CHUNK_LEN;
      const xx = centerOffset + (this.drand(idx+k,107)-0.5)*(canyonGap*0.62);
      const yy = -1 + this.drand(idx+k,109)*12;
      const orb = makeOrbMesh();
      orb.position.set(xx, yy, zz);
      orb.rotation.set(rand(0,Math.PI), rand(0,Math.PI), 0);
      orb.userData.baseY = yy;
      orb.userData.phase = rand(0,Math.PI*2);
      orb.userData.collected=false;
      ch.group.add(orb);
      ch.orbs.push({mesh:orb, collected:false});
    }

    this.scene.add(ch.group);
    this.chunks.push(ch);
  }

  // worldOffset is total forward distance traveled (positive). Chunks have been moved by +worldOffset?
  updateCollidersWorld(worldOffsetZ){
    for(const ch of this.chunks){
      for(const c of ch.colliders){
        c.box.setFromCenterAndSize(
          new THREE.Vector3(c.mesh.position.x, c.mesh.position.y, c.mesh.position.z + ch.group.position.z + worldOffsetZ),
          new THREE.Vector3(c.half.x*2, c.half.y*2, c.half.z*2)
        );
      }
      for(const o of ch.orbs){
        if(o.collected) continue;
        const wp = o.mesh.getWorldPosition(new THREE.Vector3());
        wp.z += worldOffsetZ;
        o._wp = wp;
      }
    }
  }

  recycleIfNeeded(worldOffsetZ){
    // chunks are at position.z + worldOffsetZ moving toward player. Player at 0,0,0. We want to remove chunks that are behind (z + offset > +60)
    while(this.chunks.length && this.chunks[0].group.position.z + worldOffsetZ > 80){
      const old = this.chunks.shift();
      old.group.traverse(obj=>{ if(obj.geometry) obj.geometry.dispose(); });
      this.scene.remove(old.group);
      this.pushChunk();
      // new chunk group position is already set, but needs to account for worldOffset? Its group pos is absolute; worldOffset is added externally via chunkGroupOffset? We'll keep worldOffset applied as translation of whole world manager: instead of shifting each chunk, we shift a parent. Simpler: after push, set its group.z += worldOffset? No.
      // Let's adjust: new chunk's group.position.z is already this.nextZ (increasing negative). But worldOffset makes older chunks move positive. So we need to not re-add offset.
      // For consistency, after push we must subtract worldOffset from its pos because update will add worldOffset later? Alternative: keep worldOffset applied as separate group offset to all chunks.
    }
    // Ensure we have enough chunks ahead
    while(this.chunks.length < CFG.VISIBLE_CHUNKS){
      this.pushChunk();
    }
  }
}

// ── class: DustSystem ──
class DustSystem {
  constructor(scene, count=320){
    this.scene=scene;
    const geo = new THREE.BufferGeometry();
    const pos = new Float32Array(count*3);
    const vel = new Float32Array(count);
    for(let i=0;i<count;i++){
      pos[i*3+0]=rand(-50,50);
      pos[i*3+1]=rand(-12,32);
      pos[i*3+2]=rand(-240,40);
      vel[i]=rand(0.8,1.6);
    }
    geo.setAttribute('position', new THREE.BufferAttribute(pos,3));
    geo.setAttribute('vel', new THREE.BufferAttribute(vel,1));
    const mat = new THREE.PointsMaterial({color:0xffc68a, size:0.22, transparent:true, opacity:0.55, depthWrite:false});
    this.points = new THREE.Points(geo, mat);
    this.scene.add(this.points);
    this.count=count;
  }
  update(dt, speed, worldOffset){
    const pos = this.points.geometry.attributes.position;
    for(let i=0;i<this.count;i++){
      let z = pos.getZ(i);
      // move towards player (positive Z) due to speed
      z += speed * dt * (0.6 + pos.getX(i)*0.002);
      // wrap
      if(z > 30){
        z = -240 + rand(-10,0);
        pos.setX(i, rand(-48,48));
        pos.setY(i, rand(-10,30));
      }
      pos.setZ(i, z);
    }
    pos.needsUpdate=true;
  }
}

// ── class: TrailSystem ──
class TrailSystem {
  constructor(scene){
    this.scene=scene;
    this.particles=[];
    const geo = new THREE.SphereGeometry(0.35,4,4);
    const mat = new THREE.MeshBasicMaterial({color:0xff8a2a, transparent:true, opacity:0.9, blending:THREE.AdditiveBlending});
    this.protoMat = mat;
    this.geo = geo;
  }
  spawn(pos, speed){
    if(this.particles.length>45) return;
    const m = new THREE.Mesh(this.geo, this.protoMat.clone());
    m.position.copy(pos);
    m.scale.setScalar(rand(0.6,1.0));
    this.scene.add(m);
    this.particles.push({mesh:m, life:1.0, vel:new THREE.Vector3(rand(-0.8,0.8), rand(-0.8,0.8), rand(1.5,3.5))});
  }
  update(dt){
    for(let i=this.particles.length-1;i>=0;i--){
      const p=this.particles[i];
      p.life-=dt*1.8;
      p.mesh.position.addScaledVector(p.vel, dt*12);
      p.mesh.scale.multiplyScalar(0.985);
      p.mesh.material.opacity = p.life*0.7;
      if(p.life<=0){
        this.scene.remove(p.mesh);
        this.particles.splice(i,1);
      }
    }
  }
}

// ── main ──
(() => {
  const canvas = document.getElementById('c');
  const overlay = document.getElementById('overlay');
  const playBtn = document.getElementById('playBtn');
  const scoreEl = document.getElementById('score');
  const distEl = document.getElementById('dist');
  const maxSpeedEl = document.getElementById('maxSpeed');
  const speedNumEl = document.getElementById('speedNum');
  const multiEl = document.getElementById('multiplier');
  const energyFill = document.getElementById('energyFill');
  const energyGlow = document.getElementById('energyGlow');
  const turboFill = document.getElementById('turboFill');
  const speedFill = document.getElementById('speedFill');
  const nearMissEl = document.getElementById('nearMiss');
  const nearValEl = document.getElementById('nearVal');
  const statsEnd = document.getElementById('statsEnd');

  const scene = new THREE.Scene();
  scene.fog = new THREE.Fog(PAL.fog, 60, 260);
  scene.background = new THREE.Color(0x170a2e);

  const renderer = new THREE.WebGLRenderer({canvas, antialias:true, powerPreference:'high-performance'});
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.8));
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.outputColorSpace = THREE.SRGBColorSpace;

  const camera = new THREE.PerspectiveCamera(CFG.FOV_BASE, window.innerWidth/window.innerHeight, 0.1, 1000);
  camera.position.set(0, 4.8, 16);
  camera.lookAt(0,1.5,0);

  const ambient = new THREE.AmbientLight(0x8a6eff, 0.55);
  scene.add(ambient);
  const sun = new THREE.DirectionalLight(0xff9a3d, 1.35);
  sun.position.set(-30, 60, 10);
  scene.add(sun);
  const rim = new THREE.DirectionalLight(0x7bf8ff, 0.6);
  rim.position.set(20,10,-40);
  scene.add(rim);

  const worldGen = new WorldGenerator(scene);
  worldGen.generateInitial();

  const ship = makeShipGroup();
  scene.add(ship);
  ship.position.set(0, 1.5, 0);

  const dust = new DustSystem(scene, 380);
  const trail = new TrailSystem(scene);

  const input = new InputSys();
  input.attach(canvas);
  const audio = new AudioEngine();

  let worldOffset = 0;

  let state = 'menu';
  let vx=0, vy=0;
  let px=0, py=1.5;
  let roll=0, pitch=0;
  let speed = CFG.BASE_SPEED;
  let baseSpeed = CFG.BASE_SPEED;
  let turboMeter = 35;
  let turboActive=false;
  let turboTime=0;
  let energy=100;
  let score=0, distance=0;
  let multiplier=1.0;
  let maxMultiplier=1.0;
  let maxSpeed=0;
  let orbCollected=0;
  let nearTimer=0;
  let nearAccum=0;
  let shake=0;
  let straightTime=0;

  function resetGame(){
    worldOffset=0;
    worldGen.chunks.forEach(c=>{ scene.remove(c.group); c.group.traverse(o=>{ if(o.geometry) o.geometry.dispose(); }); });
    worldGen.chunks=[]; worldGen.nextZ=0; worldGen.nextIndex=0;
    worldGen.generateInitial();
    px=0; py=1.5; vx=0; vy=0; roll=0; pitch=0;
    baseSpeed=CFG.BASE_SPEED; speed=baseSpeed;
    turboMeter=35; turboActive=false; turboTime=0;
    energy=100; score=0; distance=0; multiplier=1.0; maxMultiplier=1.0; maxSpeed=0; orbCollected=0;
    nearTimer=0; nearAccum=0; shake=0; straightTime=0;
    statsEnd.classList.add('hidden');
    playBtn.textContent='IGNITE ENGINES [ ENTER ]';
  }

  function startGame(){
    if(state==='playing') return;
    audio.init();
    if(state==='over') resetGame();
    state='playing';
    overlay.classList.add('hidden');
  }

  function gameOver(){
    if(state!=='playing') return;
    state='over';
    audio.blip('hit');
    document.getElementById('finalScore').textContent = Math.floor(score).toString();
    document.getElementById('finalMax').textContent = Math.floor(maxSpeed)+' km/h';
    document.getElementById('finalMulti').textContent = 'x'+maxMultiplier.toFixed(1);
    document.getElementById('finalOrbs').textContent = orbCollected.toString();
    statsEnd.classList.remove('hidden');
    playBtn.textContent='RETRY RUN [ ENTER ]';
    overlay.classList.remove('hidden');
  }

  playBtn.addEventListener('click', startGame);
  window.addEventListener('keydown', (e)=>{
    if(e.code==='Enter') { if(state!=='playing') startGame(); }
    if(e.code==='KeyR' && state==='over') { resetGame(); startGame(); }
  });

  let lastT = performance.now();
  let nearDisplayT=0;

  function loop(){
    const now = performance.now();
    let dt = (now-lastT)/1000;
    dt = Math.min(dt, 0.033);
    lastT = now;

    input.update(dt);

    if(state==='playing'){
      baseSpeed = CFG.BASE_SPEED + distance*0.12 + Math.min(70, score*0.06);
      baseSpeed = clamp(baseSpeed, CFG.BASE_SPEED, CFG.MAX_SPEED);

      const wantsTurbo = input.turboHeld && turboMeter > 2;
      if(wantsTurbo && !turboActive){
        turboActive=true; turboTime=1.6; audio.blip('turbo');
      }
      if(turboActive){
        turboTime-=dt;
        turboMeter = Math.max(0, turboMeter - dt*34);
        speed = baseSpeed + CFG.TURBO_SPEED;
        if(turboTime<=0 || turboMeter<=0 || !input.turboHeld){
          turboActive=false;
        }
      }else{
        speed = lerp(speed, baseSpeed, dt*2.5);
        turboMeter = clamp(turboMeter + dt*2.2, 0, 100);
      }

      const targetVX = input.x * 19;
      const targetVY = input.y * 12;
      const accel = 26 + speed*0.03;
      vx = lerp(vx, targetVX, clamp(accel*dt*0.18,0,1));
      vx += -vx*0.8*dt;
      vy = lerp(vy, targetVY, clamp(accel*dt*0.18,0,1));
      vy += -vy*0.9*dt;

      px += vx*dt;
      py += vy*dt;
      px = clamp(px, -CFG.PLAYER_BOUNDS_X, CFG.PLAYER_BOUNDS_X);
      py = clamp(py, -6, CFG.PLAYER_BOUNDS_Y);
      if(Math.abs(px)>=CFG.PLAYER_BOUNDS_X-0.01) vx*= -0.2;
      if(py<=-5.8 || py>=CFG.PLAYER_BOUNDS_Y-0.1) vy*= -0.25;

      const targetRoll = -vx*0.11 - input.x*0.42;
      const targetPitch = vy*0.11 + input.y*0.25 - speed*0.002;
      roll = lerp(roll, targetRoll, dt*7);
      pitch = lerp(pitch, targetPitch, dt*6);
      ship.position.set(px, py, 0);
      ship.rotation.set(pitch*0.6, -roll*0.25, roll);

      if(speed>40 && (Math.abs(vx)>1 || turboActive)){
        if(Math.random()<0.45) trail.spawn(new THREE.Vector3(px, py-0.25, -3.2), speed);
      }

      if(Math.abs(input.x)<0.15 && Math.abs(vx)<1.2){
        straightTime+=dt;
        if(straightTime>2.2){
          turboMeter = clamp(turboMeter + dt*6,0,100);
        }
      }else straightTime=0;

      const travel = speed * dt;
      distance += travel*0.08;
      worldOffset += travel;
      maxSpeed = Math.max(maxSpeed, speed*5.2);

      for(const ch of worldGen.chunks){
        ch.group.position.z += travel;
      }
      while(worldGen.chunks.length && worldGen.chunks[0].group.position.z > 70){
        const old = worldGen.chunks.shift();
        old.group.traverse(o=>{ if(o.geometry) o.geometry.dispose(); });
        scene.remove(old.group);
        worldGen.pushChunk();
        const newest = worldGen.chunks[worldGen.chunks.length-1];
        newest.group.position.z = newest.zStart + worldOffset;
      }

      let closestDist = 1000;
      let collided=false;
      const playerSpherePos = new THREE.Vector3(px, py, 0);
      const playerRadius = 1.05;

      for(const ch of worldGen.chunks){
        const gz = ch.group.position.z;
        if(gz < -220 || gz > 70) continue;
        for(const col of ch.colliders){
          const wp = new THREE.Vector3();
          col.mesh.getWorldPosition(wp);
          const half = col.half;
          const cx = clamp(playerSpherePos.x, wp.x-half.x, wp.x+half.x);
          const cy = clamp(playerSpherePos.y, wp.y-half.y, wp.y+half.y);
          const cz = clamp(playerSpherePos.z, wp.z-half.z, wp.z+half.z);
          const dx = playerSpherePos.x - cx;
          const dy = playerSpherePos.y - cy;
          const dz = playerSpherePos.z - cz;
          const dist = Math.sqrt(dx*dx+dy*dy+dz*dz);
          if(dist < closestDist) closestDist = dist;
          if(dist < playerRadius + 0.15){
            collided=true; break;
          }
        }
        if(collided) break;
        for(const orb of ch.orbs){
          if(orb.collected) continue;
          const wp = new THREE.Vector3();
          orb.mesh.getWorldPosition(wp);
          const d = wp.distanceTo(playerSpherePos);
          if(d < 2.2){
            orb.collected=true;
            orb.mesh.visible=false;
            energy = clamp(energy + CFG.ENERGY_ORB, 0, 100);
            turboMeter = clamp(turboMeter + CFG.TURBO_GAIN_ORB, 0, 100);
            score += 120 * multiplier;
            orbCollected++;
            audio.blip('orb');
            for(let b=0;b<8;b++) trail.spawn(wp, speed);
          }else{
            orb.mesh.rotation.y += dt*1.4;
            orb.mesh.position.y = orb.mesh.userData.baseY + Math.sin(now*0.002 + orb.mesh.userData.phase)*0.6;
            if(orb.mesh.userData.glowMat){
              orb.mesh.userData.glowMat.opacity = 0.25 + Math.sin(now*0.005 + orb.mesh.userData.phase)*0.15;
            }
          }
        }
      }

      if(!collided && closestDist < 4.8){
        const prox = 1 - (closestDist/4.8);
        nearAccum += prox*dt*4.5;
        nearTimer += dt;
        shake = Math.max(shake, prox*0.6);
        if(nearTimer>0.18 && prox>0.35){
          multiplier = clamp(multiplier + prox*0.14, 1, 9.9);
          maxMultiplier = Math.max(maxMultiplier, multiplier);
          turboMeter = clamp(turboMeter + prox*CFG.TURBO_GAIN_NEAR*dt*2.5,0,100);
          if(nearTimer>0.45){
            audio.blip('near');
            nearTimer=0;
            nearValEl.textContent = (prox*100).toFixed(0);
            nearMissEl.classList.add('show');
            nearDisplayT=0.55;
            score += 35*multiplier*prox;
            multiEl.classList.remove('pulse'); void multiEl.offsetWidth; multiEl.classList.add('pulse');
          }
        }
      }else{
        nearTimer = Math.max(0, nearTimer - dt*2);
        multiplier = lerp(multiplier, 1.0, dt*0.22);
      }

      const decayMult = turboActive ? 1.75 : 1.0;
      energy -= CFG.ENERGY_DECAY * decayMult * dt * (0.6 + speed/200);
      if(energy<=0){ energy=0; gameOver(); }

      if(collided){
        energy -= 34;
        score = Math.max(0, score - 80);
        audio.blip('hit');
        shake=1.2;
        multiplier = Math.max(1, multiplier*0.6);
        speed *= 0.65;
        if(energy<=0) gameOver();
      }

      score += speed * dt * 0.22 * multiplier;

      scoreEl.textContent = Math.floor(score).toString().padStart(5,'0');
      distEl.textContent = Math.floor(distance)+'m';
      maxSpeedEl.textContent = Math.floor(maxSpeed)+' km/h';
      speedNumEl.textContent = Math.floor(speed*5.2).toString();
      multiEl.textContent = 'x'+multiplier.toFixed(1);
      energyFill.style.width = energy+'%';
      energyGlow.style.left = energy+'%';
      turboFill.style.width = turboMeter+'%';
      speedFill.style.width = clamp((speed/CFG.MAX_SPEED)*100, 0,100)+'%';
      if(energy<28) energyFill.style.filter='hue-rotate(300deg) brightness(1.2)';
      else energyFill.style.filter='';

      nearDisplayT -= dt;
      if(nearDisplayT<=0) nearMissEl.classList.remove('show');
      shake = lerp(shake, 0, dt*4.5);
    }else{
      ship.position.y = 1.5 + Math.sin(now*0.001)*0.2;
      ship.rotation.y = Math.sin(now*0.0006)*0.15;
      ship.rotation.z = Math.sin(now*0.0008)*0.12;
    }

    dust.update(dt, speed, worldOffset);
    trail.update(dt);

    const fovTarget = turboActive ? CFG.FOV_TURBO : CFG.FOV_BASE + speed*0.06;
    camera.fov = lerp(camera.fov, fovTarget, dt*3);
    camera.updateProjectionMatrix();
    const camX = lerp(camera.position.x, px*0.26, dt*2.2) + (shake*(Math.random()-0.5))*0.9;
    const camY = lerp(camera.position.y, py*0.32 + 3.8, dt*2.2) + (shake*(Math.random()-0.5))*0.6;
    camera.position.x = camX;
    camera.position.y = camY;
    camera.lookAt(px*0.45, py*0.45+0.6, -14);

    const glow = 0.4 + speed*0.006 + (turboActive?0.8:0);
    if(ship.userData.engMat){
      ship.userData.engMat.emissiveIntensity = glow*2.2;
      ship.userData.trailMat.opacity = turboActive?0.85 : 0.15 + speed*0.0015;
    }
    if(state==='playing'){
      audio.setEngine(speed/CFG.MAX_SPEED, turboActive?1:0);
    }

    renderer.render(scene, camera);
    requestAnimationFrame(loop);
  }

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

  resetGame();
  loop();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.1","provider":"openrouter","persona":"composer","sessionId":"1218bdf4-6643-40d8-a01e-daf2abc4746b","tokensIn":352440,"tokensOut":25444,"tokensTotal":377884,"turns":19,"toolCalls":18,"failedToolCalls":1,"timestamp":"2026-07-17T04:16:37.304Z"} -->