← Canyon Run results

Canyon Run

Deepseek v4.1 Flash · full

◈Preview unavailable — try playing the result

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

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 57,244 bytes · SHA-256 936d709027a4
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>CANYON RUN — Procedural Canyon Racer</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{width:100%;height:100%;overflow:hidden;background:#0a0614}
  body{
    font-family:"Helvetica Neue",Helvetica,Arial,system-ui,sans-serif;
    color:#f6ecff;user-select:none;-webkit-user-select:none;-webkit-font-smoothing:antialiased;
    background:
      radial-gradient(70% 42% at 62% 63%, rgba(255,214,150,.55) 0%, rgba(255,140,80,.22) 34%, rgba(255,90,60,0) 68%),
      linear-gradient(to bottom,
        #07030f 0%, #150a28 16%, #361141 32%, #6b2250 46%, #ab3f4e 55%,
        #dd6a44 61%, #f2a463 65%, #b35640 71%, #55163a 82%, #1d0a24 93%, #0a0614 100%);
    background-attachment:fixed;
  }
  canvas{display:block;position:fixed;inset:0;width:100%;height:100%}

  /* ---------- full screen fx layers ---------- */
  #fx{position:fixed;inset:0;pointer-events:none;z-index:5}
  #vignette{position:absolute;inset:0;background:radial-gradient(120% 95% at 50% 50%, rgba(0,0,0,0) 42%, rgba(10,2,20,.55) 78%, rgba(6,1,14,.92) 100%)}
  #danger{position:absolute;inset:0;opacity:0;background:radial-gradient(130% 100% at 50% 50%, rgba(255,0,60,0) 45%, rgba(255,20,50,.35) 82%, rgba(180,0,40,.6) 100%);transition:opacity .25s}
  #flash{position:absolute;inset:0;background:#fff;opacity:0;transition:opacity .35s}

  /* ---------- HUD ---------- */
  #hud{position:fixed;inset:0;z-index:10;pointer-events:none;opacity:0;transition:opacity .8s}
  #hud.on{opacity:1}
  .lbl{font-size:9.5px;letter-spacing:.34em;text-transform:uppercase;opacity:.5;font-weight:600}
  #tl{position:absolute;top:24px;left:30px}
  #score{font-size:52px;font-weight:200;letter-spacing:.02em;line-height:.95;font-variant-numeric:tabular-nums;
         text-shadow:0 0 26px rgba(255,180,120,.55),0 0 60px rgba(255,120,80,.25)}
  #sub{display:flex;gap:16px;margin-top:8px}
  #sub div{font-size:10px;letter-spacing:.18em;opacity:.55;font-variant-numeric:tabular-nums}
  #sub b{font-weight:600;opacity:1;color:#ffd9ac}

  #tr{position:absolute;top:20px;right:32px;text-align:right}
  #mult{font-size:70px;font-weight:700;line-height:.9;letter-spacing:-.02em;color:#fff;
        text-shadow:0 0 18px rgba(255,170,110,.9),0 0 54px rgba(255,110,60,.55);
        transform-origin:100% 60%;transition:transform .18s cubic-bezier(.2,1.6,.4,1),color .2s}
  #mult.hot{color:#fff3d6;transform:scale(1.28)}
  #mbar{margin-top:8px;width:130px;height:2px;margin-left:auto;background:rgba(255,255,255,.12);overflow:hidden}
  #mbarFill{height:100%;width:10%;background:linear-gradient(90deg,#ffb26b,#ff6a3d)}

  #bottom{position:absolute;left:0;right:0;bottom:26px;display:flex;align-items:flex-end;
          justify-content:space-between;padding:0 34px;gap:28px}
  .gauge{width:230px}
  .gtrack{position:relative;height:9px;border-radius:5px;background:rgba(255,255,255,.09);
          box-shadow:inset 0 0 0 1px rgba(255,255,255,.08);overflow:hidden;margin-top:7px}
  .gfill{position:absolute;inset:0;width:100%;transform-origin:0 50%;border-radius:5px}
  #energyFill{background:linear-gradient(90deg,#59f7d4,#a6ffdf);box-shadow:0 0 16px rgba(90,250,215,.85)}
  #turboFill{background:linear-gradient(90deg,#ff9d5c,#ffe6a8);box-shadow:0 0 16px rgba(255,180,110,.8)}
  #gaugeR{text-align:right}
  #turboFill{transform-origin:100% 50%}
  .gauge .val{font-size:11px;letter-spacing:.2em;opacity:.75;margin-top:7px;font-variant-numeric:tabular-nums}
  .gauge.armed .lbl{color:#ffe1ae;opacity:1;text-shadow:0 0 12px rgba(255,180,110,.9)}

  #dial{flex:0 0 auto;width:400px;text-align:center}
  #dial .num{display:flex;align-items:baseline;justify-content:center;gap:8px}
  #spdNum{font-size:44px;font-weight:200;line-height:1;font-variant-numeric:tabular-nums;
          text-shadow:0 0 22px rgba(255,190,140,.5)}
  #dial .unit{font-size:9.5px;letter-spacing:.3em;opacity:.5}
  #track{position:relative;height:3px;margin-top:12px;background:rgba(255,255,255,.1);border-radius:2px}
  #spdFill{height:100%;width:0%;border-radius:2px;background:linear-gradient(90deg,#8ffff0,#ffd7a0 55%,#ff6a3d);
           box-shadow:0 0 14px rgba(255,170,110,.9)}
  #spdKnob{position:absolute;top:-3px;left:0;width:9px;height:9px;border-radius:50%;background:#fff;
           box-shadow:0 0 16px rgba(255,220,180,1);transform:translateX(-50%)}
  #ticks{display:flex;justify-content:space-between;margin-top:6px}
  #ticks span{width:1px;height:5px;background:rgba(255,255,255,.25)}

  #nm{position:absolute;top:48%;left:50%;transform:translate(-50%,-50%) scale(.9);
      font-size:15px;letter-spacing:.5em;font-weight:600;color:#fff2d8;opacity:0;text-transform:uppercase;
      text-shadow:0 0 22px rgba(255,150,90,.95),0 0 60px rgba(255,90,50,.6)}
  #nm.show{animation:nmpop .5s ease-out}
  @keyframes nmpop{0%{opacity:0;transform:translate(-50%,-50%) scale(.75)}
                   18%{opacity:1;transform:translate(-50%,-50%) scale(1.05)}
                   100%{opacity:0;transform:translate(-50%,-55%) scale(1.1)}}
  #boost{position:absolute;bottom:78px;left:50%;transform:translateX(-50%);font-size:11px;letter-spacing:.6em;
         opacity:0;color:#ffe3b8;text-shadow:0 0 18px rgba(255,170,110,.9)}
  #boost.on{opacity:1;animation:bl .55s infinite alternate}
  @keyframes bl{from{opacity:.45}to{opacity:1}}

  /* ---------- Overlays ---------- */
  #overlay{position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;
           background:radial-gradient(70% 70% at 50% 45%, rgba(20,6,26,.35) 0%, rgba(8,2,14,.72) 70%, rgba(6,1,12,.88) 100%);
           opacity:0;pointer-events:none;transition:opacity .5s;backdrop-filter:blur(3px)}
  #overlay.show{opacity:1;pointer-events:auto}
  .panel{width:min(620px,88vw);text-align:center;padding:44px 46px 34px;border:1px solid rgba(255,190,140,.18);
         border-radius:4px;background:linear-gradient(180deg,rgba(30,12,36,.62),rgba(14,4,22,.72));
         box-shadow:0 0 80px rgba(255,120,60,.12),inset 0 1px 0 rgba(255,220,180,.09)}
  .kicker{font-size:10px;letter-spacing:.55em;opacity:.55;text-transform:uppercase}
  h1{font-size:min(76px,13vw);font-weight:200;letter-spacing:.06em;margin:12px 0 6px;line-height:1;
     background:linear-gradient(180deg,#fff6e6,#ffb26b 55%,#ff6a3d);
     -webkit-background-clip:text;background-clip:text;color:transparent;
     filter:drop-shadow(0 0 32px rgba(255,130,70,.45))}
  #ovtitle.dead{background:linear-gradient(180deg,#fff,#ff8ea0 60%,#c33a5c);-webkit-background-clip:text;background-clip:text}
  .body-text{font-size:12px;letter-spacing:.13em;line-height:2;opacity:.7;margin-top:14px;text-transform:uppercase}
  .stats{display:grid;grid-template-columns:1fr 1fr;gap:14px 26px;margin:26px 0 6px;text-align:left}
  .stats .s{border-left:1px solid rgba(255,170,120,.3);padding-left:12px}
  .stats .s i{display:block;font-size:9px;letter-spacing:.28em;opacity:.5;font-style:normal;text-transform:uppercase}
  .stats .s b{font-size:26px;font-weight:300;font-variant-numeric:tabular-nums;color:#ffe0b8}
  #startBtn{margin-top:24px;cursor:pointer;pointer-events:auto;font-family:inherit;font-size:12px;letter-spacing:.34em;
            text-transform:uppercase;color:#1b0812;background:linear-gradient(180deg,#ffe6bb,#ffab6b 60%,#ff7a45);
            border:0;border-radius:2px;padding:15px 44px;font-weight:700;
            box-shadow:0 0 34px rgba(255,150,90,.45),0 8px 26px rgba(0,0,0,.4);transition:transform .16s,box-shadow .16s}
  #startBtn:hover{transform:translateY(-2px);box-shadow:0 0 46px rgba(255,170,110,.7),0 10px 30px rgba(0,0,0,.45)}
  .hint{margin-top:18px;font-size:9.5px;letter-spacing:.3em;opacity:.42;text-transform:uppercase}
  .keys{display:flex;justify-content:center;gap:22px;margin-top:18px;flex-wrap:wrap}
  .key{font-size:9.5px;letter-spacing:.22em;opacity:.62;text-transform:uppercase}
  .key b{display:inline-block;min-width:22px;padding:3px 7px;margin-right:6px;border:1px solid rgba(255,200,150,.3);
         border-radius:3px;font-weight:600;opacity:1;color:#ffd9ac}
  #loading{position:fixed;inset:0;z-index:30;display:flex;align-items:center;justify-content:center;
           background:#0a0614;font-size:11px;letter-spacing:.4em;text-transform:uppercase;opacity:.7}
</style>
</head>
<body>
<div id="loading">initialising canyon…</div>
<canvas id="gl"></canvas>

<div id="fx">
  <div id="danger"></div>
  <div id="vignette"></div>
  <div id="flash"></div>
</div>

<div id="hud">
  <div id="tl">
    <div class="lbl">Score</div>
    <div id="score">0</div>
    <div id="sub">
      <div>DIST <b id="dist">0</b>m</div>
      <div>MAX <b id="maxspd">0</b></div>
      <div>ORBS <b id="orbs">0</b></div>
    </div>
  </div>
  <div id="tr">
    <div class="lbl">Multiplier</div>
    <div id="mult">×1.0</div>
    <div id="mbar"><div id="mbarFill"></div></div>
  </div>
  <div id="bottom">
    <div class="gauge" id="energyGauge">
      <div class="lbl">Energy</div>
      <div class="gtrack"><div class="gfill" id="energyFill"></div></div>
    </div>
    <div id="dial">
      <div class="num"><b id="spdNum">0</b><span class="unit">KM/H</span></div>
      <div id="track"><div id="spdFill"></div><div id="spdKnob"></div></div>
      <div id="ticks"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
    </div>
    <div class="gauge" id="gaugeR">
      <div class="lbl">Turbo</div>
      <div class="gtrack"><div class="gfill" id="turboFill"></div></div>
    </div>
  </div>
  <div id="nm">Near Miss</div>
  <div id="boost">TURBO ENGAGED</div>
</div>

<div id="overlay" class="show">
  <div class="panel">
    <div class="kicker" id="ovkicker">Endless procedural racer</div>
    <h1 id="ovtitle">Canyon Run</h1>
    <div class="body-text" id="ovbody">Fly the canyon. Skim the walls. Feed on light.</div>
    <div class="stats" id="ovstats" style="display:none"></div>
    <button id="startBtn">Launch</button>
    <div class="keys">
      <div class="key"><b>A</b><b>D</b> bank left / right</div>
      <div class="key"><b>W</b><b>S</b> climb / dive</div>
      <div class="key"><b>⇧</b> turbo</div>
      <div class="key"><b>R</b> restart</div>
    </div>
    <div class="hint">Collect orbs to stay charged · hug the rock to multiply your score</div>
  </div>
</div>

<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js"}}</script>
<script type="module">
import * as THREE from 'three';

/* ============================================================
   CANYON RUN — high-speed procedural canyon racer
   ============================================================ */

const CFG = {
  CHUNK: 62,
  SPAWN_AHEAD: 13,
  KILL_BEHIND: 2,
  PLAYER_R: 2.3,
  NEAR_DIST: 7.0,
  MAX_VX: 84,
  LAT_ACC: 175,
  VERT_ACC: 155,
  CEIL: 56,
  FLOOR: -78,
  BASE_SPEED: 66,
  SPEED_GAIN: 66,
  ATTRACT_SPEED: 58,
  TURBO_MULT: 1.8,
  DRAIN: 5.6,
  DRAIN_SPD: 0.035,
  ORB_ENERGY: 22,
  MAX_MULT: 10
};

/* ---------------- utils ---------------- */
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp=(a,b,t)=>a+(b-a)*t;
const ss=t=>t*t*(3-2*t);
function hash(n){ const s=Math.sin(n*127.1+n*311.7+n*n*0.0731+1.3)*43758.5453; return s-Math.floor(s); }
const rnd=(s)=>hash(s);

/* ---------------- canyon path ---------------- */
function pathX(z){
  return Math.sin(z*0.0042)*38 + Math.sin(z*0.0113+1.7)*13 + Math.sin(z*0.0191+0.6)*4;
}
const nodeWCache = new Map();
let diffGlobal = 0;
function nodeW(i){
  let v = nodeWCache.get(i);
  if(v===undefined){
    const h=hash(i*7.13+2.3), h2=hash(i*3.71+9.1), h3=hash(i*11.77+5.5);
    let base = 15 + h*21;                                  // 15 .. 36
    base *= (1 - 0.32*clamp(diffGlobal,0,1));              // narrows as you improve
    if(h2 > 0.70) base *= 0.55;                            // tight squeeze region
    else if(h2 < 0.16) base *= 1.18;                       // wide basin
    if(h3 > 0.93) base *= 0.8;
    v = Math.max(8.0, base);
    nodeWCache.set(i,v);
  }
  return v;
}
function widthAt(z){
  const t=-z/CFG.CHUNK, i=Math.floor(t), f=t-i;
  return lerp(nodeW(i), nodeW(i+1), ss(f));
}

/* ---------------- palette ---------------- */
const C = (h)=>new THREE.Color(h);
const PAL = {
  deep:  C('#241a35'), mid: C('#4a3457'), top: C('#9c5560'), crest: C('#c9846f'),
  monoA: C('#2b2340'), monoB: C('#3d2b4d')
};

/* ============================================================
   RENDERER / SCENE
   ============================================================ */
const canvas = document.getElementById('gl');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, alpha:true, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio||1, 1.6));
renderer.setSize(innerWidth, innerHeight);
renderer.setClearAlpha(0);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(C('#9e4a3c'), 0.0042);

const camera = new THREE.PerspectiveCamera(74, innerWidth/innerHeight, 0.3, 2600);
camera.position.set(0,6,14);

/* lights — low golden sun, cool ambient bounce */
const sun = new THREE.DirectionalLight(C('#ffc089'), 2.35);
sun.position.set(-1, 0.42, -0.5);
scene.add(sun); scene.add(sun.target);
sun.castShadow = true;
sun.shadow.mapSize.set(1024,1024);
const sc = sun.shadow.camera;
sc.left=-140; sc.right=140; sc.top=140; sc.bottom=-140; sc.near=1; sc.far=900;
sun.shadow.bias = -0.0009;
sun.shadow.normalBias = 1.2;

const hemi = new THREE.HemisphereLight(C('#ff9a6a'), C('#180a2a'), 0.85);
scene.add(hemi);
const fill = new THREE.DirectionalLight(C('#9a7bff'), 0.45);
fill.position.set(1,0.6,0.7); scene.add(fill);

/* ============================================================
   SHARED GEOMETRY / MATERIALS
   ============================================================ */
const wallMat = new THREE.MeshLambertMaterial({
  vertexColors:true, flatShading:true, side:THREE.DoubleSide,
  emissive:C('#2a0f2e'), emissiveIntensity:0.35
});
const boxGeo = new THREE.BoxGeometry(1,1,1);
const monoMats = [
  new THREE.MeshLambertMaterial({color:C('#332a4a'), flatShading:true, emissive:C('#180c26'), emissiveIntensity:.5}),
  new THREE.MeshLambertMaterial({color:C('#453055'), flatShading:true, emissive:C('#1c0e2c'), emissiveIntensity:.5}),
  new THREE.MeshLambertMaterial({color:C('#5b3a52'), flatShading:true, emissive:C('#241028'), emissiveIntensity:.5})
];
const orbGeo = new THREE.IcosahedronGeometry(1.15, 0);
const orbMat = new THREE.MeshBasicMaterial({color:C('#c9fff6')});
const orbHaloMat = new THREE.MeshBasicMaterial({color:C('#3ef0d8'), transparent:true, opacity:0.28,
                                                blending:THREE.AdditiveBlending, depthWrite:false});
const haloGeo = new THREE.IcosahedronGeometry(3.1, 0);

/* ============================================================
   CANYON WALL GEOMETRY
   ============================================================ */
const PROF = [
  {y:-108, d:0.0},
  {y:-56,  d:1.4},
  {y:-16,  d:3.0},
  {y: 18,  d:6.0},
  {y: 44,  d:10.5}
];
function flareD(y){
  if(y<=PROF[0].y) return PROF[0].d;
  for(let i=0;i<PROF.length-1;i++){
    const a=PROF[i], b=PROF[i+1];
    if(y>=a.y && y<=b.y) return lerp(a.d, b.d, (y-a.y)/(b.y-a.y));
  }
  return PROF[PROF.length-1].d;
}
function wallRadius(z,y){ return widthAt(z) + flareD(y); }

const _c = new THREE.Color();
function faceColor(yc, h, out){
  const t = clamp((yc+80)/160, 0, 1);
  if(t<0.5) out.copy(PAL.deep).lerp(PAL.mid, t*2*1.05 + h*0.15);
  else out.copy(PAL.mid).lerp(PAL.crest, (t-0.5)*2*(0.55+h*0.75));
  if(h>0.88) out.lerp(PAL.top, 0.45);
  out.multiplyScalar(0.88 + h*0.26);
  return out;
}
function buildWallGeo(idx){
  const z0 = -idx*CFG.CHUNK, NS = 6;
  const pos=[], col=[], nor=[];
  const cols = PROF.length + 3;
  const grid = [];
  for(let side=-1; side<=1; side+=2){
    const g = [];
    for(let i=0;i<=NS;i++){
      const z = z0 - (i/NS)*CFG.CHUNK;
      const cx = pathX(z), w = widthAt(z);
      const row = [];
      for(let j=0;j<PROF.length;j++){
        const pr = PROF[j];
        const jit = (hash(i*3.13+j*7.7+side*17.3+idx*0.37)-0.5) * (2.0 + j*1.9);
        const yr  = (hash(i*5.11+j*2.93+side*3.1+idx*0.91)-0.5) * (j>1?5:1.5);
        row.push(new THREE.Vector3(cx + side*(w + pr.d + jit), pr.y + yr, z));
      }
      const pk  = 14 + hash(i*9.41+side*4.4+idx*0.113)*46;
      const off = w + 11 + hash(i*2.23+side*1.7+idx*0.31)*12;
      row.push(new THREE.Vector3(cx + side*(off+7),  44 + pk*0.45 + (hash(i*4.2+side)-0.5)*8, z));
      row.push(new THREE.Vector3(cx + side*(off+30), 44 + pk*1.0, z));
      row.push(new THREE.Vector3(cx + side*(off+86), -108, z));
      g.push(row);
    }
    grid.push(g);
  }
  const tri = (a,b,c,i,j)=>{}; // placeholder (unused)
  function pushTri(p1,p2,p3,i,j,side){
    pos.push(p1.x,p1.y,p1.z, p2.x,p2.y,p2.z, p3.x,p3.y,p3.z);
    const yc=(p1.y+p2.y+p3.y)/3;
    const h = hash(i*13.11+j*3.73+side*5.53+idx*2.17);
    faceColor(yc, h, _c);
    for(let k=0;k<3;k++) col.push(_c.r,_c.g,_c.b);
  }
  for(let s=0;s<2;s++){
    const g = grid[s], side = s===0?-1:1;
    for(let i=0;i<NS;i++){
      const a=g[i], b=g[i+1];
      for(let j=0;j<cols-1;j++){
        pushTri(a[j], a[j+1], b[j+1], i, j, side);
        pushTri(a[j], b[j+1], b[j],   i, j, side);
      }
    }
  }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(pos,3));
  geo.setAttribute('color', new THREE.Float32BufferAttribute(col,3));
  geo.computeVertexNormals();
  geo.computeBoundingSphere();
  return geo;
}

/* ============================================================
   PLAYER CRAFT
   ============================================================ */
const craft = new THREE.Group();
(function buildCraft(){
  const bodyMat  = new THREE.MeshLambertMaterial({color:C('#3a3a52'), flatShading:true,
                                                  emissive:C('#1a1030'), emissiveIntensity:.6});
  const plateMat = new THREE.MeshLambertMaterial({color:C('#c9743e'), flatShading:true,
                                                  emissive:C('#5a1e08'), emissiveIntensity:.9});
  const glowMat  = new THREE.MeshBasicMaterial({color:C('#ffe0b0'), transparent:true, opacity:.95,
                                                blending:THREE.AdditiveBlending, depthWrite:false});
  const hull = new THREE.Mesh(new THREE.ConeGeometry(1.5, 6.2, 4), bodyMat);
  hull.rotation.x = -Math.PI/2; hull.rotation.y = Math.PI/4;
  hull.castShadow = true; craft.add(hull);

  const spine = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.7, 3.2), plateMat);
  spine.position.set(0,0.5,-1.1); craft.add(spine);

  const wing = new THREE.BoxGeometry(4.4, 0.22, 1.7);
  const wl = new THREE.Mesh(wing, bodyMat); wl.position.set(-2.4,0,0.5); wl.rotation.z = 0.30;
  wl.rotation.y = 0.22; wl.castShadow = true; craft.add(wl);
  const wr = new THREE.Mesh(wing, bodyMat); wr.position.set( 2.4,0,0.5); wr.rotation.z = -0.30;
  wr.rotation.y = -0.22; wr.castShadow = true; craft.add(wr);

  const tipL = new THREE.Mesh(new THREE.BoxGeometry(0.28,1.1,0.8), plateMat);
  tipL.position.set(-4.35,0.35,0.72); tipL.rotation.z=0.30; craft.add(tipL);
  const tipR = tipL.clone(); tipR.position.x = 4.35; tipR.rotation.z=-0.30; craft.add(tipR);

  const fin = new THREE.Mesh(new THREE.ConeGeometry(0.5,2.2,3), plateMat);
  fin.rotation.x = -Math.PI/2; fin.position.set(0,0.9,1.4); craft.add(fin);

  const engine = new THREE.Mesh(new THREE.SphereGeometry(1.05,8,6), glowMat);
  engine.position.set(0,0,2.5); engine.scale.set(1,1,1.7); craft.add(engine);
  craft.userData.engine = engine;
  craft.userData.wings = [wl,wr,tipL,tipR];
})();
scene.add(craft);

/* ============================================================
   CHUNK WORLD
   ============================================================ */
const world = {
  chunks: new Map(),
  nextIdx: 0,
  colliders: [],
  spawn(idx){
    const group = new THREE.Group();
    const walls = new THREE.Mesh(buildWallGeo(idx), wallMat);
    walls.receiveShadow = true; walls.castShadow = true;
    group.add(walls);

    const colliders = [];
    const r0 = hash(idx*3.77+1.1);
    const count = r0<0.32 ? 1 : (r0<0.78 ? 2 : 3);
    for(let k=0;k<count;k++){
      const h1=hash(idx*11.3+k*5.7), h2=hash(idx*7.9+k*3.1), h3=hash(idx*17.1+k*9.3);
      const nz = -((idx + (k+0.5)/count + (h1-0.5)*0.34) * CFG.CHUNK);
      const w  = widthAt(nz), cx = pathX(nz);
      const mw = Math.min(2.6 + h2*5.2, Math.max(1.8, w-6.2), 7.0);
      const mh = 16 + h3*52;
      const md = 5 + h2*9;
      const maxOff = Math.max(0, w - mw - 6.0);
      const x = cx + (h3-0.5)*2*maxOff;
      const y = -18 + h1*44;
      const m = new THREE.Mesh(boxGeo, monoMats[(idx+k)%3]);
      m.position.set(x,y,nz);
      m.scale.set(mw*2, mh, md);
      m.rotation.set((h1-0.5)*0.10, (h2-0.5)*0.30, (h3-0.5)*0.22);
      m.castShadow = true; m.receiveShadow = true;
      group.add(m);
      colliders.push({x, y, z:nz, hx:mw+1.4, hy:mh/2+1.0, hz:md/2+1.4});
    }

    const orbs = [];
    const oc = 2 + Math.floor(hash(idx*5.51+2.2)*3);
    for(let k=0;k<oc;k++){
      const h1=hash(idx*9.11+k*7.3), h2=hash(idx*4.19+k*3.9), h3=hash(idx*13.7+k*1.7);
      const nz = -((idx + (k+0.35+h2*0.4)/oc) * CFG.CHUNK);
      const w  = widthAt(nz), cx = pathX(nz);
      let x = cx + (h1-0.5)*2*(w-4.0);
      const y = clamp((h3-0.5)*46, -26, 24);
      let ok = true;
      for(const cd of colliders){
        if(Math.abs(nz-cd.z) < cd.hz+3 && Math.abs(x-cd.x) < cd.hx+3 && Math.abs(y-cd.y) < cd.hy+3){ ok=false; break; }
      }
      if(!ok) x += (x>cx?1:-1) * (w*0.55);
      const mesh = new THREE.Mesh(orbGeo, orbMat);
      const halo = new THREE.Mesh(haloGeo, orbHaloMat);
      const base = new THREE.Vector3(x,y,nz);
      mesh.position.copy(base); halo.position.copy(base);
      group.add(mesh); group.add(halo);
      orbs.push({mesh, halo, base, phase: hash(idx*2.7+k)*6.283, taken:false});
    }

    scene.add(group);
    this.chunks.set(idx, {idx, group, colliders, orbs});
    for(const c of colliders) this.colliders.push({...c, chunk:idx});
  },
  clear(){
    for(const [,ch] of this.chunks){
      ch.group.traverse(o=>{ if(o.isMesh && o.geometry && o.geometry !== boxGeo && o.geometry !== orbGeo && o.geometry !== haloGeo) o.geometry.dispose(); });
      scene.remove(ch.group);
    }
    this.chunks.clear(); this.colliders.length = 0; this.nextIdx = 0;
    nodeWCache.clear();
  },
  update(pz, spawnAll){
    const ci = Math.floor(-pz/CFG.CHUNK);
    let guard = 0;
    while(this.nextIdx <= ci + CFG.SPAWN_AHEAD && guard++ < 40) this.spawn(this.nextIdx++);
    for(const [idx,ch] of this.chunks){
      if(idx < ci - CFG.KILL_BEHIND){
        ch.group.traverse(o=>{ if(o.isMesh && o.geometry && o.geometry !== boxGeo && o.geometry !== orbGeo && o.geometry !== haloGeo) o.geometry.dispose(); });
        scene.remove(ch.group);
        this.chunks.delete(idx);
      }
    }
    for(let i=this.colliders.length-1;i>=0;i--) if(this.colliders[i].chunk < ci-3) this.colliders.splice(i,1);
    if(nodeWCache.size > 60){ for(const k of nodeWCache.keys()) if(k < ci-10) nodeWCache.delete(k); }
  }
};

/* ============================================================
   SKY DOME (golden-hour gradient + sun disc)
   ============================================================ */
const SUN_DIR = new THREE.Vector3(-1, 0.30, -0.55).normalize();
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide, depthWrite:false, fog:false,
  uniforms:{
    uTop:{value:C('#150a2b')}, uHor:{value:C('#e8734a')}, uBot:{value:C('#200c26')},
    uSun:{value:C('#ffd9a4')}, uSunDir:{value:SUN_DIR.clone()}
  },
  vertexShader:`
    varying vec3 vDir;
    void main(){ vDir = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader:`
    uniform vec3 uTop,uHor,uBot,uSun,uSunDir; varying vec3 vDir;
    void main(){
      vec3 d = normalize(vDir);
      float h = d.y;
      vec3 c = h>0.0 ? mix(uHor,uTop,pow(clamp(h,0.0,1.0),0.42)) : mix(uHor,uBot,pow(clamp(-h,0.0,1.0),0.5));
      c = mix(c, uHor, smoothstep(0.30,0.0,abs(h-0.02))*0.55);
      float sd = max(dot(d, uSunDir), 0.0);
      c += uSun * smoothstep(0.99930,0.99985,sd) * 9.0;
      c += uSun * pow(sd, 9.0) * 0.30;
      c += uSun * pow(sd, 2.0) * 0.07;
      gl_FragColor = vec4(c,1.0);
    }`
});
const sky = new THREE.Mesh(new THREE.SphereGeometry(1500, 32, 20), skyMat);
sky.frustumCulled = false;
scene.add(sky);

/* ============================================================
   PARTICLES
   ============================================================ */
const P_VERT = `
  attribute float aSize; attribute float aAlpha; attribute vec3 aColor;
  varying float vA; varying vec3 vC;
  void main(){
    vA = aAlpha; vC = aColor;
    vec4 mv = modelViewMatrix*vec4(position,1.0);
    gl_PointSize = aSize*(430.0/max(1.0,-mv.z));
    gl_Position = projectionMatrix*mv;
  }`;
const P_FRAG = `
  varying float vA; varying vec3 vC;
  void main(){
    vec2 d = gl_PointCoord-0.5;
    float r = length(d);
    float a = smoothstep(0.5,0.02,r);
    if(a*vA < 0.004) discard;
    gl_FragColor = vec4(vC, a*vA);
  }`;
function pMaterial(){ return new THREE.ShaderMaterial({
  vertexShader:P_VERT, fragmentShader:P_FRAG, transparent:true, depthWrite:false,
  blending:THREE.AdditiveBlending
}); }

class Burst {
  constructor(max){
    this.max=max; this.cursor=0;
    this.pos=new Float32Array(max*3); this.col=new Float32Array(max*3);
    this.size=new Float32Array(max); this.alpha=new Float32Array(max);
    this.vel=new Float32Array(max*3); this.life=new Float32Array(max);
    this.maxLife=new Float32Array(max); this.drag=new Float32Array(max); this.grow=new Float32Array(max);
    const g=new THREE.BufferGeometry();
    g.setAttribute('position', new THREE.BufferAttribute(this.pos,3));
    g.setAttribute('aColor', new THREE.BufferAttribute(this.col,3));
    g.setAttribute('aSize', new THREE.BufferAttribute(this.size,1));
    g.setAttribute('aAlpha', new THREE.BufferAttribute(this.alpha,1));
    this.geo=g;
    this.points=new THREE.Points(g, pMaterial());
    this.points.frustumCulled=false;
    scene.add(this.points);
    this._c=new THREE.Color();
  }
  emit(x,y,z,vx,vy,vz,size,life,color,drag,grow){
    const i=this.cursor; this.cursor=(this.cursor+1)%this.max;
    const i3=i*3;
    this.pos[i3]=x; this.pos[i3+1]=y; this.pos[i3+2]=z;
    this.vel[i3]=vx; this.vel[i3+1]=vy; this.vel[i3+2]=vz;
    this._c.set(color);
    this.col[i3]=this._c.r; this.col[i3+1]=this._c.g; this.col[i3+2]=this._c.b;
    this.size[i]=size; this.alpha[i]=1; this.life[i]=life; this.maxLife[i]=life;
    this.drag[i]=drag||1.6; this.grow[i]=grow||0;
  }
  update(dt){
    const {pos,vel,life,maxLife,size,alpha,drag,grow,max}=this;
    for(let i=0;i<max;i++){
      if(life[i]<=0){ if(alpha[i]!==0){alpha[i]=0; size[i]=0;} continue; }
      life[i]-=dt;
      const i3=i*3;
      const k=Math.exp(-drag[i]*dt);
      const t=life[i]/maxLife[i];
      if(life[i]<=0){ alpha[i]=0; size[i]=0; continue; }
      vel[i3]*=k; vel[i3+1]*=k; vel[i3+2]*=k;
      pos[i3]+=vel[i3]*dt; pos[i3+1]+=vel[i3+1]*dt; pos[i3+2]+=vel[i3+2]*dt;
      alpha[i]=t*t*0.95+0.05*t;
      size[i]*=(1+grow[i]*dt);
    }
    this.geo.attributes.position.needsUpdate=true;
    this.geo.attributes.aAlpha.needsUpdate=true;
    this.geo.attributes.aSize.needsUpdate=true;
    this.geo.attributes.aColor.needsUpdate=true;
  }
  clear(){
    this.life.fill(0); this.alpha.fill(0); this.size.fill(0);
    this.geo.attributes.aAlpha.needsUpdate=true;
    this.geo.attributes.aSize.needsUpdate=true;
  }
}

class Streaks {
  constructor(n){
    this.n=n;
    this.pos=new Float32Array(n*3); this.col=new Float32Array(n*3);
    this.size=new Float32Array(n); this.alpha=new Float32Array(n);
    this.tw=new Float32Array(n);
    const c=new THREE.Color();
    for(let i=0;i<n;i++){
      this.pos[i*3]=0; this.pos[i*3+1]=0; this.pos[i*3+2]=0;
      const warm = Math.random()<0.72;
      c.set(warm ? '#ffd2a0' : '#a8ecff');
      this.col[i*3]=c.r; this.col[i*3+1]=c.g; this.col[i*3+2]=c.b;
      this.size[i]=0.9+Math.random()*2.0;
      this.alpha[i]=0.20+Math.random()*0.45;
      this.tw[i]=Math.random()*6.28;
    }
    const g=new THREE.BufferGeometry();
    g.setAttribute('position', new THREE.BufferAttribute(this.pos,3));
    g.setAttribute('aColor', new THREE.BufferAttribute(this.col,3));
    g.setAttribute('aSize', new THREE.BufferAttribute(this.size,1));
    g.setAttribute('aAlpha', new THREE.BufferAttribute(this.alpha,1));
    this.geo=g;
    this.points=new THREE.Points(g, pMaterial());
    this.points.frustumCulled=false;
    scene.add(this.points);
  }
  reset(px,py,pz){
    for(let i=0;i<this.n;i++) this.place(i,px,py,pz,true);
  }
  place(i,px,py,pz,anywhere){
    const i3=i*3;
    const far = anywhere ? Math.random()*520 : 380+Math.random()*320;
    this.pos[i3]   = px + (Math.random()-0.5)*2*95;
    this.pos[i3+1] = py + (Math.random()-0.5)*2*70;
    this.pos[i3+2] = pz - far;
  }
  update(dt,px,py,pz,t){
    for(let i=0;i<this.n;i++){
      const i3=i*3;
      if(this.pos[i3+2] > pz + 40 || this.alpha[i]<=0) this.place(i,px,py,pz,false);
      this.alpha[i]=0.16+0.30*(0.5+0.5*Math.sin(t*3.0+this.tw[i]));
    }
    this.geo.attributes.position.needsUpdate=true;
    this.geo.attributes.aAlpha.needsUpdate=true;
  }
}

const burst = new Burst(760);
const streaks = new Streaks(300);

/* ============================================================
   AUDIO (procedural, Web Audio API)
   ============================================================ */
const Sfx = {
  ready:false, muted:false,
  init(){
    if(this.ready) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    if(!AC) return;
    const ctx = new AC(); this.ctx=ctx;
    const master=ctx.createGain(); master.gain.value=0.9;
    const comp=ctx.createDynamicsCompressor();
    comp.threshold.value=-14; comp.knee.value=20; comp.ratio.value=7;
    master.connect(comp); comp.connect(ctx.destination);
    this.master=master;
    const len=Math.floor(ctx.sampleRate*2);
    const buf=ctx.createBuffer(1,len,ctx.sampleRate);
    const d=buf.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=Math.random()*2-1;
    this.noise=buf;
    const o1=ctx.createOscillator(); o1.type='sawtooth'; o1.frequency.value=44;
    const o2=ctx.createOscillator(); o2.type='square';   o2.frequency.value=66;
    const eg=ctx.createGain(); eg.gain.value=0.0;
    const lp=ctx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=340; lp.Q.value=8;
    o1.connect(eg); o2.connect(eg); eg.connect(lp); lp.connect(master);
    o1.start(); o2.start();
    const wind=ctx.createBufferSource(); wind.buffer=buf; wind.loop=true;
    const wf=ctx.createBiquadFilter(); wf.type='bandpass'; wf.frequency.value=520; wf.Q.value=0.8;
    const wg=ctx.createGain(); wg.gain.value=0.0;
    wind.connect(wf); wf.connect(wg); wg.connect(master); wind.start();
    this.eng={o1,o2,eg,lp,wind,wf,wg};
    this.ready=true;
  },
  resume(){ if(this.ctx && this.ctx.state==='suspended') this.ctx.resume(); },
  engine(speed,turbo,active){
    if(!this.ready || this.muted) return;
    const e=this.eng, t=this.ctx.currentTime;
    const base=34+speed*0.40;
    e.o1.frequency.setTargetAtTime(base, t, 0.07);
    e.o2.frequency.setTargetAtTime(base*1.503, t, 0.07);
    e.lp.frequency.setTargetAtTime(240+speed*7.5+(turbo?1100:0), t, 0.12);
    e.eg.gain.setTargetAtTime(active?0.055+Math.min(speed/230,1)*0.06:0.0, t, 0.18);
    e.wg.gain.setTargetAtTime(active?0.010+Math.min(speed/260,1)*0.05:0.0, t, 0.25);
    e.wf.frequency.setTargetAtTime(380+speed*6.5, t, 0.25);
  },
  ping(){
    if(!this.ready) return;
    const ctx=this.ctx, t=ctx.currentTime;
    const o=ctx.createOscillator(); o.type='sine';
    const o2=ctx.createOscillator(); o2.type='triangle';
    const g=ctx.createGain();
    o.frequency.setValueAtTime(1180,t); o.frequency.exponentialRampToValueAtTime(2050,t+0.09);
    o2.frequency.setValueAtTime(1760,t); o2.frequency.exponentialRampToValueAtTime(2980,t+0.09);
    g.gain.setValueAtTime(0.001,t); g.gain.exponentialRampToValueAtTime(0.28,t+0.012);
    g.gain.exponentialRampToValueAtTime(0.0008,t+0.30);
    o.connect(g); o2.connect(g); g.connect(this.master);
    o.start(t); o2.start(t); o.stop(t+0.34); o2.stop(t+0.34);
  },
  whoosh(){
    if(!this.ready) return;
    const ctx=this.ctx, t=ctx.currentTime;
    const s=ctx.createBufferSource(); s.buffer=this.noise;
    const bp=ctx.createBiquadFilter(); bp.type='bandpass'; bp.Q.value=1.6;
    bp.frequency.setValueAtTime(300,t); bp.frequency.exponentialRampToValueAtTime(2600,t+0.30);
    const g=ctx.createGain();
    g.gain.setValueAtTime(0.0008,t); g.gain.exponentialRampToValueAtTime(0.30,t+0.10);
    g.gain.exponentialRampToValueAtTime(0.0008,t+0.42);
    s.connect(bp); bp.connect(g); g.connect(this.master);
    s.start(t); s.stop(t+0.45);
  },
  boost(){
    if(!this.ready) return;
    const ctx=this.ctx, t=ctx.currentTime;
    const o=ctx.createOscillator(); o.type='sawtooth';
    const f=ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=1800;
    o.frequency.setValueAtTime(150,t); o.frequency.exponentialRampToValueAtTime(880,t+0.45);
    const g=ctx.createGain();
    g.gain.setValueAtTime(0.0008,t); g.gain.exponentialRampToValueAtTime(0.16,t+0.06);
    g.gain.exponentialRampToValueAtTime(0.0008,t+0.55);
    o.connect(f); f.connect(g); g.connect(this.master);
    o.start(t); o.stop(t+0.6);
  },
  beep(){
    if(!this.ready) return;
    const ctx=this.ctx, t=ctx.currentTime;
    const o=ctx.createOscillator(); o.type='square'; o.frequency.value=760;
    const g=ctx.createGain();
    g.gain.setValueAtTime(0.0008,t); g.gain.exponentialRampToValueAtTime(0.075,t+0.01);
    g.gain.exponentialRampToValueAtTime(0.0008,t+0.13);
    o.connect(g); g.connect(this.master);
    o.start(t); o.stop(t+0.15);
  },
  crash(){
    if(!this.ready) return;
    const ctx=this.ctx, t=ctx.currentTime;
    const s=ctx.createBufferSource(); s.buffer=this.noise;
    const lp=ctx.createBiquadFilter(); lp.type='lowpass';
    lp.frequency.setValueAtTime(1600,t); lp.frequency.exponentialRampToValueAtTime(90,t+1.1);
    const g=ctx.createGain();
    g.gain.setValueAtTime(0.55,t); g.gain.exponentialRampToValueAtTime(0.0008,t+1.3);
    s.connect(lp); lp.connect(g); g.connect(this.master);
    s.start(t); s.stop(t+1.35);
    const o=ctx.createOscillator(); o.type='sine';
    o.frequency.setValueAtTime(120,t); o.frequency.exponentialRampToValueAtTime(34,t+0.9);
    const g2=ctx.createGain();
    g2.gain.setValueAtTime(0.5,t); g2.gain.exponentialRampToValueAtTime(0.0008,t+1.0);
    o.connect(g2); g2.connect(this.master);
    o.start(t); o.stop(t+1.05);
  }
};

/* ============================================================
   POST PROCESSING  (bright-pass → blur → composite/trail → blit)
   ============================================================ */
const PostFX = (function(){
  const quadGeo = new THREE.PlaneGeometry(2,2);
  const fsScene = new THREE.Scene();
  const fsCam = new THREE.OrthographicCamera(-1,1,1,-1,0,1);
  const fsMesh = new THREE.Mesh(quadGeo, null);
  fsMesh.frustumCulled = false;
  fsScene.add(fsMesh);

  const VERT = `varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position.xy,0.0,1.0); }`;

  const brightMat = new THREE.ShaderMaterial({
    uniforms:{ tDiffuse:{value:null}, uThresh:{value:0.52} },
    vertexShader:VERT,
    fragmentShader:`
      uniform sampler2D tDiffuse; uniform float uThresh; varying vec2 vUv;
      void main(){
        vec3 c=texture2D(tDiffuse,vUv).rgb;
        float l=dot(c,vec3(0.2126,0.7152,0.0722));
        float k=max(0.0,l-uThresh)/max(l,1e-4);
        gl_FragColor=vec4(c*k,1.0);
      }`
  });

  const blurMat = new THREE.ShaderMaterial({
    uniforms:{ tDiffuse:{value:null}, uDir:{value:new THREE.Vector2()} },
    vertexShader:VERT,
    fragmentShader:`
      uniform sampler2D tDiffuse; uniform vec2 uDir; varying vec2 vUv;
      void main(){
        vec3 s=texture2D(tDiffuse,vUv).rgb*0.227;
        s+=(texture2D(tDiffuse,vUv+uDir*1.0).rgb+texture2D(tDiffuse,vUv-uDir*1.0).rgb)*0.194;
        s+=(texture2D(tDiffuse,vUv+uDir*2.0).rgb+texture2D(tDiffuse,vUv-uDir*2.0).rgb)*0.121;
        s+=(texture2D(tDiffuse,vUv+uDir*3.0).rgb+texture2D(tDiffuse,vUv-uDir*3.0).rgb)*0.054;
        s+=(texture2D(tDiffuse,vUv+uDir*4.0).rgb+texture2D(tDiffuse,vUv-uDir*4.0).rgb)*0.016;
        gl_FragColor=vec4(s,1.0);
      }`
  });

  const compMat = new THREE.ShaderMaterial({
    uniforms:{
      tScene:{value:null}, tBloom:{value:null}, tPrev:{value:null},
      uBloom:{value:0.9}, uTrail:{value:0.45}, uBlur:{value:0.0}, uTime:{value:0}
    },
    vertexShader:VERT,
    fragmentShader:`
      uniform sampler2D tScene,tBloom,tPrev;
      uniform float uBloom,uTrail,uBlur,uTime;
      varying vec2 vUv;
      void main(){
        vec2 dir=vUv-0.5;
        float amt=uBlur*dot(dir,dir)*4.0;
        vec3 s=texture2D(tScene,vUv).rgb*0.34;
        s+=texture2D(tScene,vUv-dir*amt*0.25).rgb*0.24;
        s+=texture2D(tScene,vUv-dir*amt*0.50).rgb*0.18;
        s+=texture2D(tScene,vUv-dir*amt*0.75).rgb*0.14;
        s+=texture2D(tScene,vUv-dir*amt*1.00).rgb*0.10;
        vec3 c=s+texture2D(tBloom,vUv).rgb*uBloom;
        vec3 p=texture2D(tPrev,vUv).rgb;
        c=max(c, p*uTrail);
        gl_FragColor=vec4(c,1.0);
      }`
  });

  const blitMat = new THREE.ShaderMaterial({
    uniforms:{ tDiffuse:{value:null}, uExp:{value:1.25}, uVig:{value:0.85},
               uRed:{value:0.0}, uTime:{value:0.0}, uHurt:{value:0.0} },
    vertexShader:VERT,
    fragmentShader:`
      uniform sampler2D tDiffuse; uniform float uExp,uVig,uRed,uTime,uHurt;
      varying vec2 vUv;
      float rand(vec2 c){ return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453); }
      void main(){
        vec2 uv=vUv;
        vec2 d=uv-0.5;
        float r2=dot(d,d);
        vec2 ca=d*r2*(0.014+uHurt*0.05);
        vec3 c;
        c.r=texture2D(tDiffuse,uv+ca).r;
        c.g=texture2D(tDiffuse,uv).g;
        c.b=texture2D(tDiffuse,uv-ca).b;
        c=vec3(1.0)-exp(-max(c,0.0)*uExp);
        float v=smoothstep(1.05,0.28,length(d*vec2(1.06,1.0))*1.42);
        c*=mix(1.0,v,uVig);
        c+=vec3(0.65,0.05,0.12)*uRed*(1.0-v);
        float g=rand(uv*vec2(1024.0,760.0)+fract(uTime)*37.0)-0.5;
        c+=g*0.035;
        c=pow(max(c,0.0),vec3(1.0/2.2));
        gl_FragColor=vec4(c,1.0);
      }`
  });

  let sceneRT=null, brightRT=null, blurRT=null, compA=null, compB=null;
  let halfW=2, halfH=2;
  const _size = new THREE.Vector2();

  function makeRT(w,h,depth,samples){
    return new THREE.WebGLRenderTarget(Math.max(2,w|0), Math.max(2,h|0), {
      minFilter:THREE.LinearFilter, magFilter:THREE.LinearFilter,
      type:THREE.HalfFloatType, depthBuffer:!!depth, stencilBuffer:false,
      samples:samples||0
    });
  }
  function disposeAll(){
    [sceneRT,brightRT,blurRT,compA,compB].forEach(rt=>{ if(rt) rt.dispose(); });
  }
  function resize(){
    renderer.getDrawingBufferSize(_size);
    const w=Math.max(2,_size.x|0), h=Math.max(2,_size.y|0);
    const hw=Math.max(2,(w/2)|0), hh=Math.max(2,(h/2)|0);
    disposeAll();
    sceneRT = makeRT(w,h,true,4);
    brightRT = makeRT(hw,hh,false,0);
    blurRT   = makeRT(hw,hh,false,0);
    compA    = makeRT(w,h,false,0);
    compB    = makeRT(w,h,false,0);
    compA = makeRT(w,h,false,0); compB = makeRT(w,h,false,0);
    [compA,compB].forEach(rt=>{
      renderer.setRenderTarget(rt); renderer.setClearColor(0x000000,1); renderer.clear();
    });
    renderer.setRenderTarget(null);
    blurMat.uniforms.uDir.value.set(1/hw, 0);
    halfW=hw; halfH=hh;
  }
  function pass(mat,target){
    fsMesh.material=mat;
    renderer.setRenderTarget(target||null);
    renderer.render(fsScene, fsCam);
  }

  return {
    resize,
    render(dt, p){
      if(!sceneRT) resize();
      renderer.setClearColor(0x000000,1);
      renderer.setRenderTarget(sceneRT);
      renderer.clear();
      renderer.render(scene, camera);

      brightMat.uniforms.tDiffuse.value = sceneRT.texture;
      pass(brightMat, brightRT);

      blurMat.uniforms.tDiffuse.value = brightRT.texture;
      blurMat.uniforms.uDir.value.set(1/this._hw, 0);
      pass(blurMat, blurRT);
      blurMat.uniforms.tDiffuse.value = blurRT.texture;
      blurMat.uniforms.uDir.value.set(0, 1/this._hh);
      pass(blurMat, brightRT);

      compMat.uniforms.tScene.value = sceneRT.texture;
      compMat.uniforms.tBloom.value = brightRT.texture;
      compMat.uniforms.tPrev.value = compA.texture;
      compMat.uniforms.uBloom.value = 0.85 + (p.turbo?0.5:0);
      compMat.uniforms.uTrail.value = p.trail;
      compMat.uniforms.uBlur.value = p.blur;
      pass(compMat, compB);
      const t=compA; compA=compB; compB=t;

      blitMat.uniforms.tDiffuse.value = compA.texture;
      blitMat.uniforms.uRed.value = p.red;
      blitMat.uniforms.uHurt.value = p.hurt;
      blitMat.uniforms.uTime.value = p.time;
      pass(blitMat, null);
      renderer.setRenderTarget(null);
    }
  };
})();

/* ============================================================
   STATE / DOM
   ============================================================ */
const player = {x:0,y:6,z:0,vx:0,vy:0,roll:0,yaw:0,pitch:0};
const G = {
  state:'menu', score:0, best:0, distance:0, speed:CFG.BASE_SPEED, maxSpeed:0,
  mult:1, energy:100, turbo:35, turboActive:false, orbCount:0,
  t:0, nearCd:0, shake:0, deathT:0, reason:'', hudT:0, beepT:0
};
const $=id=>document.getElementById(id);
const hud = {
  score:$('score'), dist:$('dist'), maxspd:$('maxspd'), orbs:$('orbs'),
  mult:$('mult'), mb:$('mbarFill'), energy:$('energyFill'), turbo:$('turboFill'),
  spdNum:$('spdNum'), spdFill:$('spdFill'), spdKnob:$('spdKnob'),
  nm:$('nm'), boost:$('boost'), danger:$('danger'), flash:$('flash'),
  hud:$('hud'), overlay:$('overlay'), ovkicker:$('ovkicker'), ovtitle:$('ovtitle'),
  ovbody:$('ovbody'), ovstats:$('ovstats'), startBtn:$('startBtn'),
  energyGauge:$('energyGauge'), gaugeR:$('gaugeR'), loading:$('loading')
};

/* ============================================================
   INPUT
   ============================================================ */
const keys={left:false,right:false,up:false,down:false,turbo:false};
const KMAP={ArrowLeft:'left',KeyA:'left',ArrowRight:'right',KeyD:'right',
            ArrowUp:'up',KeyW:'up',ArrowDown:'down',KeyS:'down',
            ShiftLeft:'turbo',ShiftRight:'turbo',Space:'turbo'};
addEventListener('keydown', e=>{
  const k=KMAP[e.code];
  if(k){ keys[k]=true; e.preventDefault(); }
  if(e.code==='KeyR' && G.state!=='menu'){ start(); e.preventDefault(); }
  if(G.state==='menu' && (k || e.code==='Enter')) start();
}, {passive:false});
addEventListener('keyup', e=>{ const k=KMAP[e.code]; if(k){ keys[k]=false; e.preventDefault(); } }, {passive:false});
addEventListener('blur', ()=>{ for(const k in keys) keys[k]=false; ptrOn=false; });

let ptrOn=false, ptrX=0, ptrY=0;
canvas.addEventListener('pointerdown', e=>{ ptrOn=true; ptrX=e.clientX; ptrY=e.clientY; });
addEventListener('pointerup', ()=>{ ptrOn=false; });
addEventListener('pointermove', e=>{ if(ptrOn){ ptrX=e.clientX; ptrY=e.clientY; } });

/* ============================================================
   COLLISION HELPERS
   ============================================================ */
function wallDistAndX(){
  const rad = wallRadius(player.z, player.y);
  const cx = pathX(player.z);
  const dL = player.x - (cx - rad);
  const dR = (cx + rad) - player.x;
  return dL<dR ? {d:dL, x:cx-rad} : {d:dR, x:cx+rad};
}
function boxDist(c){
  const dx=Math.abs(player.x-c.x)-c.hx;
  const dy=Math.abs(player.y-c.y)-c.hy;
  const dz=Math.abs(player.z-c.z)-c.hz;
  const ox=dx>0?dx:0, oy=dy>0?dy:0, oz=dz>0?dz:0;
  const outside=Math.sqrt(ox*ox+oy*oy+oz*oz);
  const inside=Math.min(Math.max(dx,dy,dz),0);
  return outside+inside;
}

/* ============================================================
   EVENTS
   ============================================================ */
function nearMiss(x,y){
  G.mult = Math.min(CFG.MAX_MULT, G.mult+0.16);
  G.score += 30*G.mult;
  G.turbo = Math.min(100, G.turbo+7);
  G.shake = Math.min(1.0, G.shake+0.42);
  Sfx.whoosh();
  hud.nm.classList.remove('show'); void hud.nm.offsetWidth; hud.nm.classList.add('show');
  hud.mult.classList.add('hot');
  clearTimeout(nearMiss._t);
  nearMiss._t = setTimeout(()=>hud.mult.classList.remove('hot'), 180);
  for(let i=0;i<12;i++){
    burst.emit(x+(Math.random()-0.5)*3, y+(Math.random()-0.5)*9, player.z-4-Math.random()*16,
      (Math.random()-0.5)*22, (Math.random()-0.5)*24, 26+Math.random()*34,
      0.7+Math.random()*1.4, 0.32+Math.random()*0.5, '#ffd6ac', 1.5, 0.7);
  }
}

function die(reason){
  if(G.state!=='play') return;
  G.state='dying'; G.deathT=0; G.reason=reason; G.shake=1.15; G.turboActive=false;
  G.best = Math.max(G.best, G.score);
  craft.visible=false;
  Sfx.crash(); Sfx.engine(0,false,false);
  hud.flash.style.transition='opacity .05s'; hud.flash.style.opacity='0.9';
  setTimeout(()=>{ hud.flash.style.transition='opacity .9s'; hud.flash.style.opacity='0'; }, 70);
  const col=new THREE.Color();
  for(let i=0;i<180;i++){
    const a=Math.random()*6.283, b=Math.acos(2*Math.random()-1), sp=10+Math.random()*75;
    col.setHSL(0.03+Math.random()*0.10, 1, 0.5+Math.random()*0.4);
    burst.emit(player.x, player.y, player.z,
      Math.sin(b)*Math.cos(a)*sp, Math.cos(b)*sp*0.7+10, Math.sin(b)*Math.sin(a)*sp+34,
      1.0+Math.random()*2.8, 0.7+Math.random()*1.5, '#'+col.getHexString(), 0.75, -0.25);
  }
}

function statBlock(label, val){
  return '<div class="s"><i>'+label+'</i><b>'+val+'</b></div>';
}
function showDead(){
  G.state='dead';
  hud.ovkicker.textContent = G.reason || 'RUN TERMINATED';
  hud.ovtitle.textContent = 'RUN OVER';
  hud.ovtitle.classList.add('dead');
  hud.ovbody.textContent = 'The canyon swallowed you. Fly tighter next time.';
  hud.ovstats.innerHTML =
    statBlock('Final Score', Math.floor(G.score).toLocaleString('en-US')) +
    statBlock('Max Speed', Math.round(G.maxSpeed*3.6)+' km/h') +
    statBlock('Distance', Math.floor(G.distance).toLocaleString('en-US')+' m') +
    statBlock('Orbs Collected', G.orbCount);
  hud.ovstats.style.display='grid';
  hud.startBtn.textContent='Retry';
  hud.overlay.classList.add('show');
}

function resetWorld(){
  world.clear();
  diffGlobal=0;
  player.x=pathX(0); player.y=8; player.z=0; player.vx=0; player.vy=0;
  player.roll=0; player.yaw=0; player.pitch=0;
  G.score=0; G.distance=0; G.speed=CFG.BASE_SPEED; G.maxSpeed=0; G.mult=1;
  G.energy=100; G.turbo=35; G.turboActive=false; G.orbCount=0;
  G.shake=0; G.nearCd=0; G.deathT=0; G.reason=''; G.beepT=0;
  world.nextIdx=-2;
  world.update(player.z);
  burst.clear();
  streaks.reset(player.x, player.y, player.z);
  craft.visible=true;
  craft.position.set(player.x,player.y,player.z);
  camera.position.set(player.x, player.y+5, player.z+15);
  camera.fov=74; camera.updateProjectionMatrix();
  sun.position.set(player.x-190, player.y+80, player.z-105);
  sun.target.position.set(player.x, player.y-6, player.z);
  sun.target.updateMatrixWorld();
}

function start(){
  Sfx.init(); Sfx.resume();
  resetWorld();
  hud.overlay.classList.remove('show');
  hud.hud.classList.add('on');
  hud.ovtitle.classList.remove('dead');
  G.state='play';
}
hud.startBtn.addEventListener('click', start);

/* ============================================================
   UPDATE
   ============================================================ */
const _look=new THREE.Vector3(), _des=new THREE.Vector3();
function update(dt){
  const playing = G.state==='play';
  const dying = G.state==='dying';
  let inX=0, inY=0;

  if(playing){
    inX = (keys.left?-1:0)+(keys.right?1:0);
    inY = (keys.up?1:0)+(keys.down?-1:0);
    if(ptrOn && inX===0 && inY===0){
      inX = clamp((ptrX/innerWidth-0.5)*3.2,-1,1);
      inY = clamp(-(ptrY/innerHeight-0.5)*3.2,-1,1);
    }
  } else if(!dying){
    const cx=pathX(player.z), w=widthAt(player.z);
    const tx = cx + Math.sin(G.t*0.33)*w*0.30;
    const ty = 8 + Math.sin(G.t*0.23)*14;
    inX = clamp((tx-player.x)*0.10 - player.vx*0.055, -1,1);
    inY = clamp((ty-player.y)*0.10 - player.vy*0.050, -1,1);
  }

  const diff = clamp(G.distance/15000,0,1);
  diffGlobal = diff;

  if(dying){
    G.speed *= Math.exp(-dt*2.6);
    G.deathT += dt;
    player.z -= G.speed*dt;
    if(G.deathT>0.85) showDead();
  } else {
    const pitchFactor = 1 - 0.26*inY;
    const baseSpd = (CFG.BASE_SPEED + CFG.SPEED_GAIN*diff) * (playing?1:0.9);
    const target = baseSpd * pitchFactor * (G.turboActive?CFG.TURBO_MULT:1);
    G.speed += (target-G.speed)*(1-Math.exp(-dt*1.7));
    G.speed = clamp(G.speed, 16, 430);
    G.distance += G.speed*dt;
    player.z -= G.speed*dt;
    if(G.speed>G.maxSpeed) G.maxSpeed=G.speed;
  }

  /* --- lateral flight (momentum + drag) --- */
  player.vx += inX*CFG.LAT_ACC*dt;
  player.vx *= Math.exp(-dt*(playing?2.0:3.0));
  player.vx = clamp(player.vx,-CFG.MAX_VX,CFG.MAX_VX);
  player.x += player.vx*dt;

  /* --- vertical flight --- */
  player.vy += inY*CFG.VERT_ACC*dt;
  player.vy *= Math.exp(-dt*2.7);
  player.vy = clamp(player.vy,-72,72);
  player.y += player.vy*dt;

  if(!dying){
    /* --- attitude: bank into the turn, pitch with climb/dive --- */
    const tRoll = clamp(-player.vx*0.0088,-0.85,0.85);
    const tYaw  = clamp(-player.vx*0.0060,-0.55,0.55);
    const tPit  = clamp(-player.vy*0.0055,-0.40,0.40);
    const kr=1-Math.exp(-dt*6.5), ky=1-Math.exp(-dt*7.5), kp=1-Math.exp(-dt*6.0);
    player.roll += (tRoll-player.roll)*kr;
    player.yaw  += (tYaw -player.yaw )*ky;
    player.pitch+= (tPit -player.pitch)*kp;
    craft.position.set(player.x, player.y, player.z);
    craft.rotation.set(player.pitch, player.yaw, player.roll);
  } else {
    player.roll += dt*1.6; player.pitch += dt*0.9;
    craft.rotation.set(player.pitch, player.yaw, player.roll);
  }

  const eng = craft.userData.engine;
  const puls = 1 + Math.sin(G.t*40)*0.06;
  const es = (G.turboActive?2.7:1.7) + G.speed*0.006;
  eng.scale.set(puls, puls, es*puls);
  eng.material.opacity = 0.75 + (G.turboActive?0.25:0);

  /* --- collisions --- */
  let dMin = 9999, wallX = 0;
  if(playing){
    const wd = wallDistAndX();
    dMin = wd.d; wallX = wd.x;
    if(wd.d < CFG.PLAYER_R+1.6) die('WALL IMPACT');
    if(player.y > CFG.CEIL) die('LOST TO THE SKY');
    if(player.y < CFG.FLOOR) die('LOST TO THE VOID');
    for(const c of world.colliders){
      if(Math.abs(c.z-player.z) > 80) continue;
      const d = boxDist(c);
      if(d<dMin){ dMin=d; wallX=c.x; }
      if(d < CFG.PLAYER_R) die('MONOLITH IMPACT');
    }
  }

  /* --- near miss / multiplier --- */
  G.nearCd -= dt;
  if(G.state==='play'){
    if(dMin < CFG.NEAR_DIST && dMin > CFG.PLAYER_R){
      if(G.nearCd<=0){ nearMiss(wallX, player.y); G.nearCd = 0.40; }
    }
    G.mult = Math.max(1, G.mult - dt*0.10);
    G.score += G.speed*dt*0.09*G.mult;
  }

  /* --- energy / turbo --- */
  const wantBoost = G.state==='play' && keys.turbo && G.turbo>0.5;
  G.turboActive = wantBoost;
  if(G.state==='play'){
    G.energy -= (CFG.DRAIN + G.speed*CFG.DRAIN_SPD + (G.turboActive?7.5:0))*dt;
    if(G.energy<=0){ G.energy=0; die('ENERGY DEPLETED'); }
  }
  if(G.turboActive) G.turbo = Math.max(0, G.turbo-24*dt);
  else if(G.state==='play' && G.speed>115) G.turbo = Math.min(100, G.turbo+3.6*dt);

  /* --- low energy warning --- */
  if(G.state==='play' && G.energy<28){
    G.beepT-=dt;
    if(G.beepT<=0){ Sfx.beep(); G.beepT = 0.35 + (G.energy/28)*0.7; }
  }

  /* --- orbs --- */
  for(const ch of world.chunks.values()){
    for(const o of ch.orbs){
      if(o.taken) continue;
      const bob = Math.sin(G.t*2.2+o.phase)*1.7;
      o.mesh.position.y = o.base.y + bob;
      o.halo.position.copy(o.mesh.position);
      o.mesh.rotation.y += dt*1.6; o.mesh.rotation.x += dt*0.7;
      const hs = 1 + Math.sin(G.t*3.4+o.phase)*0.10;
      o.halo.scale.setScalar(hs);
      o.halo.rotation.y -= dt*0.6;
      if(!playing) continue;
      const dx=o.mesh.position.x-player.x, dy=o.mesh.position.y-player.y, dz=o.mesh.position.z-player.z;
      if(dx*dx+dy*dy+dz*dz < 20){
        o.taken=true; o.mesh.visible=false; o.halo.visible=false;
        G.energy = Math.min(100, G.energy+CFG.ORB_ENERGY);
        G.turbo  = Math.min(100, G.turbo+6);
        G.orbCount++; G.score += 25*G.mult;
        Sfx.ping();
        for(let i=0;i<14;i++){
          const a=Math.random()*6.283, b=Math.acos(2*Math.random()-1), sp=8+Math.random()*26;
          burst.emit(o.mesh.position.x, o.mesh.position.y, o.mesh.position.z,
            Math.sin(b)*Math.cos(a)*sp, Math.cos(b)*sp, Math.sin(b)*Math.sin(a)*sp+18,
            0.6+Math.random()*1.2, 0.35+Math.random()*0.5, '#8ffbe4', 1.8, 0.3);
        }
      }
    }
  }

  /* --- world streaming --- */
  world.update(player.z);

  /* --- engine trail --- */
  if(G.state==='play' || G.state==='menu'){
    const tn = G.turboActive?4:2;
    for(let i=0;i<tn;i++){
      burst.emit(
        player.x - Math.sin(player.yaw)*2.6 + (Math.random()-0.5)*1.1,
        player.y + (Math.random()-0.5)*1.1,
        player.z + 2.6 + (Math.random()-0.5)*0.6,
        -player.vx*0.15, -player.vy*0.1, 26+Math.random()*24,
        1.6+Math.random()*1.4, 0.30+Math.random()*0.35,
        G.turboActive?'#ffd39a':'#ff9f5e', 2.4, 1.4);
    }
  }

  burst.update(dt);
  streaks.update(dt, player.x, player.y, player.z, G.t);

  /* --- camera --- */
  const camLag = 1-Math.exp(-dt*(playing?8:3));
  const back = 13.2 + G.speed*0.011 + (G.turboActive?3.4:0) + (dying?18:0);
  _des.set(player.x + player.vx*0.05, player.y + 4.5 - player.vy*0.02, player.z + back);
  camera.position.lerp(_des, camLag);
  _look.set(player.x + player.vx*0.28, player.y + player.vy*0.06, player.z - 36);
  camera.lookAt(_look);
  camera.rotateZ(-player.roll*0.40);
  if(G.shake>0.002){
    const s=G.shake;
    camera.position.x += (Math.random()-0.5)*s*2.4;
    camera.position.y += (Math.random()-0.5)*s*2.4;
    camera.position.z += (Math.random()-0.5)*s*2.0;
    camera.rotateZ((Math.random()-0.5)*s*0.05);
    G.shake *= Math.exp(-dt*5.0);
  } else G.shake=0;
  const tFov = 72 + clamp((G.speed-60)/170,0,1)*11 + (G.turboActive?15:0);
  camera.fov += (tFov-camera.fov)*(1-Math.exp(-dt*4.5));
  camera.updateProjectionMatrix();

  /* --- sun shadows follow the player --- */
  sun.position.set(player.x-190, player.y+80, player.z-105);
  sun.target.position.set(player.x, player.y-6, player.z);
  sun.target.updateMatrixWorld();
  sky.position.copy(camera.position);

  Sfx.engine(G.speed, G.turboActive, G.state==='play'||G.state==='menu');

  /* --- HUD (throttled) --- */
  G.hudT -= dt;
  if(G.hudT<=0){ G.hudT=0.06; updateHUD(); }
}

function updateHUD(){
  const kmh = G.speed*3.6;
  hud.score.textContent = Math.floor(G.score).toLocaleString('en-US');
  hud.dist.textContent  = Math.floor(G.distance).toLocaleString('en-US');
  hud.maxspd.textContent= Math.round(G.maxSpeed*3.6);
  hud.orbs.textContent  = G.orbCount;
  hud.mult.textContent  = '×'+G.mult.toFixed(1);
  hud.mb.style.width    = clamp(G.mult/CFG.MAX_MULT,0,1)*100+'%';
  hud.energy.style.width= clamp(G.energy/100,0,1)*100+'%';
  hud.turbo.style.width = clamp(G.turbo/100,0,1)*100+'%';
  hud.spdNum.textContent= Math.round(kmh);
  const f = clamp(kmh/950,0,1);
  hud.spdFill.style.width = (f*100)+'%';
  hud.spdKnob.style.left  = (f*100)+'%';
  hud.danger.style.opacity = G.energy<32 ? ((32-G.energy)/32)*0.7 : 0;
  hud.boost.classList.toggle('on', G.turboActive);
  hud.gaugeR.classList.toggle('armed', G.turbo>18);
  hud.energyGauge.classList.toggle('armed', G.energy<30);
}

/* ============================================================
   RESIZE + LOOP
   ============================================================ */
function onResize(){
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
  PostFX.resize();
}
addEventListener('resize', onResize);

let last = performance.now();
let booted = false;
function frame(now){
  requestAnimationFrame(frame);
  let dt = (now-last)/1000; last = now;
  if(!(dt>0)) dt = 0.016;
  dt = Math.min(dt, 0.05);
  G.t += dt;
  update(dt);

  const spdF = clamp((G.speed-70)/210, 0, 1);
  const trail = 0.30 + spdF*0.34 + (G.turboActive?0.10:0);
  const blur  = 0.22 + spdF*0.55 + (G.turboActive?0.30:0);
  const red   = (G.state==='play' && G.energy<32) ? (1-G.energy/32)*0.85 : 0;
  const hurt  = Math.min(1.4, G.shake*0.9 + (G.state==='dying'?0.7:0));
  PostFX.render(dt, {trail, blur, red, hurt, turbo:G.turboActive, time:G.t});

  if(!booted){
    booted = true;
    hud.loading.style.display='none';
  }
}

/* ============================================================
   BOOT
   ============================================================ */
try{
  onResize();
  resetWorld();
  updateHUD();
  requestAnimationFrame(frame);
}catch(err){
  hud.loading.textContent = 'ERROR: ' + err.message;
  console.error(err);
}
</script>
</body>
</html>