← Canyon Run results

Canyon Run

Tiel Coder 35B A3B GGUF MTP · full

Initial view of Canyon Run

Browser check: Artifact runtime error. 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 49,117 bytes · SHA-256 bae7ffc22a7e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>CANYON // HOURLINE — Procedural Canyon Racer</title>
<style>
  :root{
    --gold:#ffd166; --orange:#ff7b3d; --magenta:#ff4d8d;
    --purple:#6a3db0; --deep:#1a0f2e; --silver:#c9bfe0;
  }
  *{ margin:0; padding:0; box-sizing:border-box; }
  html,body{ width:100%; height:100%; overflow:hidden; background:#0d0718; font-family:"Segoe UI",Helvetica,Arial,sans-serif; }
  #game{ position:fixed; inset:0; }
  canvas{ display:block; }

  /* ---------- HUD ---------- */
  #hud{ position:fixed; inset:0; pointer-events:none; z-index:10; color:var(--silver);
    text-shadow:0 2px 12px rgba(0,0,0,.6); }
  .hud-row{ position:absolute; top:22px; display:flex; align-items:flex-start; gap:26px; }
  #scoreWrap{ }
  #scoreLabel{ font-size:11px; letter-spacing:4px; text-transform:uppercase; opacity:.65; }
  #score{ font-size:34px; font-weight:700; letter-spacing:1px; color:#fff; }
  #best{ font-size:12px; opacity:.55; letter-spacing:2px; margin-top:2px; }

  #multiWrap{ text-align:right; transform:translateX(0); will-change:transform; }
  #multiLabel{ font-size:11px; letter-spacing:4px; text-transform:uppercase; opacity:.6; text-align:right; }
  #multiplier{ font-size:64px; font-weight:800; line-height:.9;
    background:linear-gradient(180deg,#fff,var(--gold)); -webkit-background-clip:text; background-clip:text;
    -webkit-text-fill-color:transparent; filter:drop-shadow(0 0 14px rgba(255,209,102,.5));
    transition:transform .08s ease-out; }
  #multi.pulse{ transform:scale(1.28); }

  /* speedometer */
  #speedo{ position:absolute; left:22px; bottom:22px; width:240px; }
  #speedoTop{ display:flex; justify-content:space-between; font-size:11px; letter-spacing:3px; opacity:.7; margin-bottom:6px; }
  #speedVal{ font-size:22px; font-weight:700; color:#fff; }
  #speedTrack{ height:3px; width:100%; background:rgba(255,255,255,.14); border-radius:2px; overflow:hidden; }
  #speedFill{ height:100%; width:0%; background:linear-gradient(90deg,var(--purple),var(--magenta),var(--gold));
    box-shadow:0 0 12px rgba(255,123,61,.7); transition:width .1s linear; }

  /* energy */
  #energy{ position:absolute; right:22px; bottom:22px; width:240px; text-align:right; }
  #energyTop{ display:flex; align-items:center; justify-content:flex-end; gap:10px; font-size:11px;
    letter-spacing:3px; text-transform:uppercase; opacity:.7; margin-bottom:6px; }
  #energyIcon{ width:12px; height:12px; border-radius:50%; background:var(--gold);
    box-shadow:0 0 12px var(--gold); animation:pulseE 1.4s infinite; }
  @keyframes pulseE{ 0%,100%{opacity:.5} 50%{opacity:1} }
  #energyTrack{ height:8px; width:100%; background:rgba(255,255,255,.12); border-radius:4px; overflow:hidden; }
  #energyFill{ height:100%; width:100%; background:linear-gradient(90deg,#ff3b6b,var(--gold));
    box-shadow:0 0 16px rgba(255,209,102,.6); transition:width .12s linear; }
  #energyFill.low{ background:linear-gradient(90deg,#ff1e3d,#ff6b3d); }

  /* turbo */
  #turbo{ position:absolute; left:50%; bottom:22px; transform:translateX(-50%); width:260px; text-align:center; }
  #turboLabel{ font-size:10px; letter-spacing:5px; text-transform:uppercase; opacity:.6; margin-bottom:5px; }
  #turboTrack{ height:5px; width:100%; background:rgba(255,255,255,.12); border-radius:3px; overflow:hidden; }
  #turboFill{ height:100%; width:0%; background:linear-gradient(90deg,#4de3ff,var(--magenta)); transition:width .1s linear; }
  #turbo.active #turboLabel{ color:var(--gold); opacity:1; animation:tbPulse .35s infinite; }
  @keyframes tbPulse{ 0%,100%{opacity:.6} 50%{opacity:1} }

  #boostHint{ position:absolute; left:50%; bottom:56px; transform:translateX(-50%);
    font-size:11px; letter-spacing:3px; text-transform:uppercase; opacity:0; transition:opacity .3s; color:var(--silver); }
  #boostHint.show{ opacity:.85; }

  /* overlays */
  .overlay{ position:fixed; inset:0; z-index:20; display:flex; flex-direction:column;
    align-items:center; justify-content:center; text-align:center;
    background:radial-gradient(ellipse at 50% 40%, rgba(40,18,64,.72), rgba(10,5,20,.94));
    backdrop-filter:blur(3px); }
  .hidden{ display:none !important; }
  #title{ font-size:64px; font-weight:900; letter-spacing:8px; line-height:.95;
    background:linear-gradient(180deg,#fff,var(--gold),var(--orange)); -webkit-background-clip:text; background-clip:text;
    -webkit-text-fill-color:transparent; filter:drop-shadow(0 4px 30px rgba(255,123,61,.4)); }
  #subtitle{ font-size:14px; letter-spacing:8px; text-transform:uppercase; opacity:.6; margin-top:8px; }
  .card{ margin-top:34px; padding:26px 40px; border:1px solid rgba(255,255,255,.12); border-radius:14px;
    background:rgba(20,10,34,.5); min-width:300px; }
  .stat{ display:flex; justify-content:space-between; margin:10px 0; font-size:14px; letter-spacing:2px; }
  .stat b{ color:#fff; font-size:20px; }
  .stat span{ opacity:.6; text-transform:uppercase; letter-spacing:3px; font-size:12px; }
  .btn{ margin-top:26px; pointer-events:auto; cursor:pointer; font-size:15px; letter-spacing:4px;
    text-transform:uppercase; padding:16px 40px; border:none; border-radius:40px; color:#1a0f2e; font-weight:800;
    background:linear-gradient(90deg,var(--gold),var(--orange)); box-shadow:0 8px 30px rgba(255,123,61,.5);
    transition:transform .15s ease, box-shadow .15s ease; }
  .btn:hover{ transform:translateY(-2px) scale(1.03); box-shadow:0 12px 40px rgba(255,123,61,.7); }
  .btn:active{ transform:scale(.97); }
  .controls{ margin-top:26px; font-size:12px; letter-spacing:2px; opacity:.55; line-height:2; }
  .controls b{ color:var(--silver); font-weight:600; }
  #crashFlash{ position:fixed; inset:0; z-index:15; background:radial-gradient(ellipse at center, rgba(255,80,40,.5), transparent 70%);
    opacity:0; pointer-events:none; transition:opacity .12s; }
  #vignette{ position:fixed; inset:0; z-index:9; pointer-events:none;
    box-shadow:inset 0 0 180px 40px rgba(10,4,20,.65); }
</style>
</head>
<body>
  <div id="game"></div>
  <div id="vignette"></div>
  <div id="crashFlash"></div>

  <div id="hud" class="hidden">
    <div class="hud-row">
      <div id="scoreWrap">
        <div id="scoreLabel">Score</div>
        <div id="score">0</div>
        <div id="best">BEST — 0</div>
      </div>
      <div id="multiWrap">
        <div id="multiLabel">Multiplier</div>
        <div id="multiplier">x1</div>
      </div>
    </div>

    <div id="speedo">
      <div id="speedoTop"><span>SPEED</span><span id="speedVal">0</span></div>
      <div id="speedTrack"><div id="speedFill"></div></div>
    </div>

    <div id="energy">
      <div id="energyTop"><span>Energy</span><div id="energyIcon"></div></div>
      <div id="energyTrack"><div id="energyFill"></div></div>
    </div>

    <div id="turbo"><div id="turboLabel">Turbo</div><div id="turboTrack"><div id="turboFill"></div></div></div>
    <div id="boostHint">Hold <b>Space</b> / Right-Click to Boost</div>
  </div>

  <div id="startScreen" class="overlay">
    <div id="title">CANYON<br>// HOURLINE</div>
    <div id="subtitle">Procedural Canyon Racer</div>
    <div class="card">
      <div class="stat"><span>Goal</span><b>Go far. Go fast.</b></div>
      <div class="stat"><span>Mechanic</span><b>Near-miss → boost</b></div>
    </div>
    <button class="btn" id="startBtn">Launch</button>
    <div class="controls">
      <b>← → / A D</b> &nbsp;bank &amp; steer<br>
      <b>↑ ↓ / W S</b> &nbsp;climb &amp; dive<br>
      <b>SPACE</b> &nbsp;boost (needs Turbo)<br>
      Collect <b>glowing orbs</b> to stay fueled · Clip walls to build multiplier
    </div>
  </div>

  <div id="gameOverScreen" class="overlay hidden">
    <div id="title" style="font-size:52px;">SIGNAL LOSS</div>
    <div id="subtitle">Craft destroyed</div>
    <div class="card">
      <div class="stat"><span>Final Score</span><b id="finalScore">0</b></div>
      <div class="stat"><span>Distance</span><b id="finalDist">0</b></div>
      <div class="stat"><span>Max Speed</span><b id="finalSpeed">0</b></div>
      <div class="stat"><span>Best</span><b id="finalBest">0</b></div>
    </div>
    <button class="btn" id="restartBtn">Re-launch</button>
  </div>

  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
    }
  }
  </script>
  <script type="module">
  // ===================================================================
  //  CANYON // HOURLINE  —  single-file procedural canyon racer
  // ===================================================================
  import * as THREE from 'three';
  import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
  import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
  import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
  import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

  const container = document.getElementById('game');

  // -------------------------------------------------------------------
  //  Renderer / Scene / Camera
  // -------------------------------------------------------------------
  const renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  container.appendChild(renderer.domElement);

  const scene = new THREE.Scene();
  scene.fog = new THREE.Fog(0x3a1c4a, 120, 900);

  const camera = new THREE.PerspectiveCamera(72, window.innerWidth/window.innerHeight, 0.1, 3000);
  camera.position.set(0, 8, -16);

  // ---- Lighting (golden-hour sun) ----
  const hemi = new THREE.HemisphereLight(0xffd9a0, 0x2a1440, 0.75);
  scene.add(hemi);
  const sun = new THREE.DirectionalLight(0xffb066, 2.1);
  sun.position.set(-60, 90, 80);
  sun.castShadow = true;
  sun.shadow.camera.left = -120;
  sun.shadow.camera.right = 120;
  sun.shadow.camera.top = 120;
  sun.shadow.camera.bottom = -120;
  sun.shadow.camera.near = 10;
  sun.shadow.camera.far = 400;
  sun.shadow.mapSize.set(1024,1024);
  sun.shadow.bias = -0.0004;
  scene.add(sun);
  scene.add(sun.target);
  const rim = new THREE.DirectionalLight(0xff4d8d, 0.5);
  rim.position.set(60, 30, -80);
  scene.add(rim);

  // ---- Sun disc on the horizon ----
  const sunDiscGeo = new THREE.CircleGeometry(70, 32);
  const sunDiscMat = new THREE.MeshBasicMaterial({
    color:0xffd166, side:THREE.DoubleSide, transparent:true, opacity:0.95
  });
  const sunDisc = new THREE.Mesh(sunDiscGeo, sunDiscMat);
  sunDisc.position.set(-140, 40, -1200);
  sunDisc.lookAt(0,40,0);
  scene.add(sunDisc);
  const sunGlowGeo = new THREE.CircleGeometry(120, 32);
  const sunGlowMat = new THREE.MeshBasicMaterial({ color:0xff7b3d, transparent:true, opacity:0.28, side:THREE.DoubleSide });
  const sunGlow = new THREE.Mesh(sunGlowGeo, sunGlowMat);
  sunGlow.copy(sunDisc); sunGlow.position.z -= 2; scene.add(sunGlow);

  // ---- Gradient sky dome ----
  const skyGeo = new THREE.SphereGeometry(2000, 32, 16);
  const skyMat = new THREE.ShaderMaterial({
    side: THREE.BackSide, depthWrite:false,
    uniforms:{
      topColor:    {value:new THREE.Color(0x2a1050)},
      midColor:    {value:new THREE.Color(0x8a2f5e)},
      bottomColor: {value:new THREE.Color(0xff8a3d)},
    },
    vertexShader:`varying vec3 vP; void main(){ vP=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
    fragmentShader:`
      varying vec3 vP; uniform vec3 topColor; uniform vec3 midColor; uniform vec3 bottomColor;
      void main(){
        float h = normalize(vP).y*0.5+0.5;
        vec3 c = h<0.5 ? mix(bottomColor,midColor,h/0.5) : mix(midColor,topColor,(h-0.5)/0.5);
        gl_FragColor=vec4(c,1.0);
      }`
  });
  scene.add(new THREE.Mesh(skyGeo, skyMat));

  // -------------------------------------------------------------------
  //  Post-processing: Render -> radial motion blur -> bloom
  // -------------------------------------------------------------------
  const composer = new EffectComposer(renderer);
  composer.addPass(new RenderPass(scene, camera));

  const blurMat = new THREE.ShaderMaterial({
    uniforms:{
      tDiffuse:{value:null},
      resolution:{value:new THREE.Vector2(window.innerWidth, window.innerHeight)},
      strength:{value:0.0}
    },
    vertexShader:`varying vec2 vUv; void main(){ vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
    fragmentShader:`
      varying vec2 vUv; uniform sampler2D tDiffuse; uniform float strength; uniform vec2 resolution;
      void main(){
        vec2 uv = vUv;
        vec2 dir = uv - 0.5;
        float d = length(dir);
        vec4 col = vec4(0.0); float total = 0.0;
        int N = 9;
        for(int i=1;i<=N;i++){
          float t = float(i)/float(N);
          vec2 off = dir * d * strength * 0.012 * t;
          vec4 s = texture2D(tDiffuse, uv - off);
          float w = 1.0 - t*0.55;
          col += s*w; total += w;
        }
        gl_FragColor = col/total;
      }`
  });
  const blurPass = new ShaderPass(blurMat);
  blurPass.material.uniforms.strength.value = 0.0;
  composer.addPass(blurPass);

  const bloom = new UnrealBloomPass(
    new THREE.Vector2(window.innerWidth, window.innerHeight), 0.75, 0.55, 0.55
  );
  composer.addPass(bloom);

  // -------------------------------------------------------------------
  //  Palette / materials
  // -------------------------------------------------------------------
  const COL = {
    base:new THREE.Color(0x4a2034),
    mid:new THREE.Color(0xa8543b),
    top:new THREE.Color(0xb79fce),
    wallTop:new THREE.Color(0xd3c6ee),
    monolith:new THREE.Color(0x7a4fb0),
    orb:new THREE.Color(0xffd166),
    craft:new THREE.Color(0xe8e2f2),
    craftDark:new THREE.Color(0x3a2d55),
    engine:new THREE.Color(0xff7b3d),
  };
  function wallColorAt(y, floorY, wallH){
    const t = THREE.MathUtils.clamp((y-floorY)/wallH, 0, 1);
    let c;
    if(t<0.5) c = COL.base.clone().lerp(COL.mid, t/0.5);
    else c = COL.mid.clone().lerp(COL.wallTop, (t-0.5)/0.5);
    return c;
  }

  const wallMat = new THREE.MeshStandardMaterial({ vertexColors:true, flatShading:true, roughness:0.85, metalness:0.05 });
  const floorMat = new THREE.MeshStandardMaterial({ vertexColors:true, flatShading:true, roughness:0.9, metalness:0.02 });
  const monolithMat = new THREE.MeshStandardMaterial({ color:COL.monolith, flatShading:true, roughness:0.4, metalness:0.2, emissive:0x2a1545, emissiveIntensity:0.4 });

  // -------------------------------------------------------------------
  //  Player craft (low-poly)
  // -------------------------------------------------------------------
  const craft = new THREE.Group();
  let engineL, engineR;
  (function buildCraft(){
    const body = new THREE.Mesh(new THREE.BoxGeometry(2.0, 1.1, 6.0),
      new THREE.MeshStandardMaterial({ color:COL.craft, flatShading:true, roughness:0.35, metalness:0.4 }));
    body.castShadow = true; craft.add(body);
    const nose = new THREE.Mesh(new THREE.ConeGeometry(1.0, 3.4, 4),
      new THREE.MeshStandardMaterial({ color:COL.craft, flatShading:true, roughness:0.35, metalness:0.4 }));
    nose.rotation.x = Math.PI/2; nose.rotation.y = Math.PI/4;
    nose.position.z = 4.4; nose.castShadow=true; craft.add(nose);
    const wingMat = new THREE.MeshStandardMaterial({ color:COL.craftDark, flatShading:true, roughness:0.5, metalness:0.3 });
    const wingL = new THREE.Mesh(new THREE.BoxGeometry(5.2, 0.25, 2.6), wingMat);
    wingL.position.set(-2.6, -0.1, 0.4); wingL.rotation.z = 0.05; wingL.castShadow=true; craft.add(wingL);
    const wingR = wingL.clone(); wingR.position.x = 2.6; wingR.rotation.z = -0.05; craft.add(wingR);
    const tailL = new THREE.Mesh(new THREE.BoxGeometry(2.2, 1.4, 0.3), wingMat);
    tailL.position.set(-1.3, 0.5, -2.4); tailL.rotation.z = 0.25; craft.add(tailL);
    const tailR = tailL.clone(); tailR.position.x = 1.3; tailR.rotation.z = -0.25; craft.add(tailR);
    // engine pods (emissive)
    const engMat = new THREE.MeshStandardMaterial({ color:0x222222, emissive:COL.engine, emissiveIntensity:2.2, flatShading:true });
    const engL = new THREE.Mesh(new THREE.CylinderGeometry(0.5,0.7,1.6,8), engMat);
    engL.rotation.x = Math.PI/2; engL.position.set(-1.0, 0, -3.4); craft.add(engL);
    const engR = engL.clone(); engR.position.x = 1.0; craft.add(engR);
    engineL = engL; engineR = engR;
    // engine glow light
    const glow = new THREE.PointLight(0xff7b3d, 2.0, 26, 2);
    glow.position.set(0,0,-4.2); craft.add(glow);
  })();
  scene.add(craft);

  // -------------------------------------------------------------------
  //  Particle systems: trail + speed streaks
  // -------------------------------------------------------------------
  const TRAIL_N = 260, STREAK_N = 340;
  const trailGeo = new THREE.BufferGeometry();
  const trailPos = new Float32Array(TRAIL_N*3);
  const trailLife = new Float32Array(TRAIL_N);
  trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
  const trailTex = makeSoftTex();
  const trailMat = new THREE.PointsMaterial({
    size:2.6, map:trailTex, transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
    color:0xff9a4d, opacity:0.85, sizeAttenuation:true
  });
  const trailPts = new THREE.Points(trailGeo, trailMat);
  trailPts.frustumCulled = false; scene.add(trailPts);
  const trailState = [];
  for(let i=0;i<TRAIL_N;i++) trailState.push({x:0,y:-999,z:0,vx:0,vy:0,vz:0,life:0,max:1});

  const streakGeo = new THREE.BufferGeometry();
  const streakPos = new Float32Array(STREAK_N*3);
  streakGeo.setAttribute('position', new THREE.BufferAttribute(streakPos, 3));
  const streakMat = new THREE.PointsMaterial({
    size:0.9, map:trailTex, transparent:true, depthWrite:false, blending:THREE.AdditiveBlending,
    color:0xbfa8ff, opacity:0.5, sizeAttenuation:true
  });
  const streakPts = new THREE.Points(streakGeo, streakMat);
  streakPts.frustumCulled = false; scene.add(streakPts);
  const streakState = [];
  for(let i=0;i<STREAK_N;i++) streakState.push({});

  function makeSoftTex(){
    const c = document.createElement('canvas'); c.width=c.height=64;
    const g = c.getContext('2d');
    const grd = g.createRadialGradient(32,32,0,32,32,32);
    grd.addColorStop(0,'rgba(255,255,255,1)');
    grd.addColorStop(0.4,'rgba(255,255,255,0.6)');
    grd.addColorStop(1,'rgba(255,255,255,0)');
    g.fillStyle=grd; g.fillRect(0,0,64,64);
    const t = new THREE.CanvasTexture(c); return t;
  }

  // -------------------------------------------------------------------
  //  Procedural generation (ring-based canyon)
  // -------------------------------------------------------------------
  const RING_SPACING = 22;
  const CHUNK_RINGS = 12;
  const FAR_X = 150;
  const NEAR_MISS = 9.0;          // world units from a wall = near miss
  const PLAYER_R = 2.0;

  let difficulty = 0;
  const ringParams = {};          // index -> {cx,hw,floorY,wallH,z}
  const chunks = [];              // active chunk managers
  const obstacles = [];           // {ring,type,x,y,z,hx,hy,hz,radius,collected,nearTriggered,glow}
  let nextRing = 0;

  function ringNoise(n, s){
    return Math.sin(n*0.037+s*1.0)*0.45 + Math.sin(n*0.0213+s*2.3)*0.35 + Math.sin(n*0.011+s*0.7)*0.20;
  }
  function genRingParams(n){
    const d = difficulty;
    const cxAmp = 16 + Math.min(46, d*7);
    const cx = ringNoise(n,1.0)*cxAmp;
    const wv = ringNoise(n,2.0);
    const minHw = Math.max(14, 30 - d*2.2);
    const maxHw = Math.max(minHw+10, 64 - d*5);
    const hw = maxHw - (maxHw-minHw)*0.5*(1.0+wv);
    const floorVar = ringNoise(n,3.0);
    const floorY = -18 + floorVar*16;
    const heightVar = ringNoise(n,4.0);
    const wallH = 150 + heightVar*46;
    return { cx, hw, floorY, wallH, z: n*RING_SPACING };
  }

  // accumulate wall geometry across a chunk for merging
  function chunkGeoArrays(){
    return {
      left:{p:[], n:[], c:[]},
      right:{p:[], n:[], c:[]},
      floor:{p:[], n:[], c:[]}
    };
  }
  function pushQuad(arr, a,b,c,d, normal){
    // emit a quad using the given winding; normal is the (already-computed) geometric normal
    const faces = [[0,1,2],[0,2,3]];
    for(const f of faces){
      for(const idx of f){
        const p = (idx===0?a:idx===1?b:idx===2?c:d);
        arr.p.push(p.x,p.y,p.z);
        arr.n.push(normal.x,normal.y,normal.z);
        const col = wallColorAt(p.y, arr._floorY, arr._wallH);
        arr.c.push(col.r,col.g,col.b);
      }
    }
  }
  // helper to grab floorY/wallH of the ring a quad belongs to (same for both verts of a quad's near/far)
  function floorYFor(arr,p){ return arr._floorY; }
  function wallHFor(arr,p){ return arr._wallH; }

  function addSegment(geo, which, ri, rn){
    const g = geo[which];
    if(which==='floor'){
      const far = FAR_X;
      const a = new THREE.Vector3(-far, ri.floorY, ri.z);
      const b = new THREE.Vector3( far, ri.floorY, ri.z);
      const c = new THREE.Vector3( far, rn.floorY, rn.z);
      const d = new THREE.Vector3(-far, rn.floorY, rn.z);
      const n = new THREE.Vector3().subVectors(b,a).cross(new THREE.Vector3().subVectors(c,a)).normalize();
      g._floorY = ri.floorY; g._wallH = 1;
      pushQuad(g,a,b,c,d,n);
    } else {
      // canyon wall quad: vertical slab at the passage edge, spanning near->far rings.
      const innerX = which==='left'? ri.cx-ri.hw : ri.cx+ri.hw;
      const innerXn = which==='left'? rn.cx-rn.hw : rn.cx+rn.hw;
      const ty = ri.floorY+ri.wallH, ty2 = rn.floorY+rn.wallH;
      const inward = which==='left'? 1 : -1;   // inner face points toward the passage
      const a = new THREE.Vector3(innerX, ri.floorY, ri.z);
      const b = new THREE.Vector3(innerX, ty, ri.z);
      const c = new THREE.Vector3(innerXn, ty2, rn.z);
      const d = new THREE.Vector3(innerXn, rn.floorY, rn.z);
      const n = new THREE.Vector3().subVectors(b,a).cross(new THREE.Vector3().subVectors(c,a)).normalize();
      if (n.x * inward < 0){ const t2=c; c=d; d=t2; }  // ensure inner face points inward (x always nonzero)
      g._floorY = ri.floorY; g._wallH = ri.wallH;
      pushQuad(g,a,b,c,d,n);
    }
  }

  function makeChunk(startRing){
    const group = new THREE.Group();
    const geo = chunkGeoArrays();
    const rings = [];
    for(let k=0;k<=CHUNK_RINGS;k++){
      const i = startRing+k;
      rings.push(genRingParams(i));
      ringParams[i] = rings[k];
    }
    for(let k=0;k<CHUNK_RINGS;k++){
      const ri = rings[k];
      const rn = rings[k+1];
      addSegment(geo,'left', ri, rn);
      addSegment(geo,'right', ri, rn);
      addSegment(geo,'floor', ri, rn);
      genObstaclesForRing(startRing+k, ri);
    }
    function buildMesh(geoArr){
      const g = new THREE.BufferGeometry();
      g.setAttribute('position', new THREE.BufferAttribute(new Float32Array(geoArr.p),3));
      g.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(geoArr.n),3));
      g.setAttribute('color', new THREE.BufferAttribute(new Float32Array(geoArr.c),3));
      return g;
    }
    const leftMesh = new THREE.Mesh(buildMesh(geo.left), wallMat);
    const rightMesh = new THREE.Mesh(buildMesh(geo.right), wallMat);
    const floorMesh = new THREE.Mesh(buildMesh(geo.floor), floorMat);
    for(const m of [leftMesh,rightMesh,floorMesh]){ m.castShadow=false; m.receiveShadow=true; group.add(m); }
    scene.add(group);
    return { startRing, endRing:startRing+CHUNK_RINGS-1, group, leftMesh, rightMesh, floorMesh };
  }
  function genObstaclesForRing(i, ri){
    if(i < 4) return; // safe start corridor
    // orbs
    const orbChance = 0.34 + Math.min(0.3, difficulty*0.03);
    if(Math.random() < orbChance){
      const off = (Math.random()-0.5)* Math.max(4, ri.hw*0.7);
      const oy = ri.floorY + 12 + Math.random()*34;
      addOrb(i, ri.cx+off, oy, ri.z);
      // occasional line of orbs
      if(Math.random()<0.4){
        const dir = Math.random()<0.5?1:-1;
        for(let s=1;s<=2;s++){
          const ox = ri.cx + off + dir*s*10;
          if(Math.abs(ox-ri.cx) < ri.hw-4){
            addOrb(i, ox, oy + dir*s*3, ri.z + s*0);
          }
        }
      }
    }
    // monoliths (floating pillars)
    const monChance = 0.14 + Math.min(0.4, difficulty*0.05);
    let count = 0;
    if(Math.random() < monChance) count = 1 + (Math.random()<Math.min(0.7,difficulty*0.12)?1:0);
    for(let c=0;c<count;c++){
      // place offset to one side, leaving a gap to fly through
      const side = Math.random()<0.5?-1:1;
      const hx = Math.max(4, ri.hw* (0.22+Math.random()*0.16));
      const gapCenter = ri.cx - side*(ri.hw*0.35);
      const mx = gapCenter + side*(hx*0.6);
      const my = ri.floorY + 14 + Math.random()*26;
      const hy = 12 + Math.random()*16;
      const hz = 4 + Math.random()*3;
      if( ri.cx - (gapCenter) * side < ri.hw - hx) continue; // keep a gap
      addMonolith(i, mx, my, ri.z, hx, hy, hz);
    }
  }

  function addOrb(i,x,y,z){
    obstacles.push({ ring:i, type:'orb', x,y,z, radius:3.2, collected:false, nearTriggered:false, glow:null });
  }
  function addMonolith(i,x,y,z,hx,hy,hz){
    const m = new THREE.Mesh(new THREE.BoxGeometry(hx*2,hy*2,hz*2), monolithMat);
    m.position.set(x,y,z); m.castShadow=true; m.receiveShadow=true;
    m.rotation.y = Math.random()*Math.PI;
    scene.add(m);
    obstacles.push({ ring:i, type:'monolith', x,y,z, hx,hy,hz, mesh:m, glow:null });
  }

  // -------------------------------------------------------------------
  //  Game state
  // -------------------------------------------------------------------
  const state = {
    pos:new THREE.Vector3(0,2,0),
    vel:new THREE.Vector3(0,0,0),
    speed:0, baseSpeed:0, maxSpeed:64,
    roll:0, pitch:0, targetRoll:0, targetPitch:0,
    energy:1.0, turbo:0.0, boosting:false,
    score:0, multiplier:1, multTimer:0,
    maxSpeedSeen:0, distance:0,
    running:false, over:false,
    shake:0, multPulse:0, fov:72
  };
  const input = { x:0, y:0, boost:false };
  let prevWallClear = 1e9;
  let bestScore = Number(localStorage.getItem('canyon_best')||0);

  // -------------------------------------------------------------------
  //  Audio (Web Audio API procedural)
  // -------------------------------------------------------------------
  let AC=null, engineOsc,engineGain,engineFilter,engineSub;
  let audioReady=false;
  function initAudio(){
    if(audioReady) return;
    try{
      AC = new (window.AudioContext||window.webkitAudioContext)();
      const master = AC.createGain(); master.gain.value=0.9; master.connect(AC.destination);
      // engine: saw + sub, filtered
      engineOsc = AC.createOscillator(); engineOsc.type='sawtooth'; engineOsc.frequency.value=55;
      engineSub = AC.createOscillator(); engineSub.type='sine'; engineSub.frequency.value=28;
      engineFilter = AC.createBiquadFilter(); engineFilter.type='lowpass'; engineFilter.frequency.value=500; engineFilter.Q.value=1.5;
      engineGain = AC.createGain(); engineGain.gain.value=0.0;
      engineOsc.connect(engineFilter); engineSub.connect(engineFilter);
      engineFilter.connect(engineGain); engineGain.connect(master);
      engineOsc.start(); engineSub.start();
      audioReady=true;
    }catch(e){ audioReady=false; }
  }
  function sfxPing(){
    if(!AC) return;
    const o=AC.createOscillator(), g=AC.createGain();
    o.type='triangle'; o.frequency.setValueAtTime(880,AC.currentTime);
    o.frequency.exponentialRampToValueAtTime(1760,AC.currentTime+0.08);
    g.gain.setValueAtTime(0.25,AC.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+0.25);
    o.connect(g); g.connect(AC.destination); o.start(); o.stop(AC.currentTime+0.26);
  }
  function sfxWhoosh(intensity){
    if(!AC) return;
    const b=AC.createBufferSource();
    const len=AC.sampleRate*0.35; const buf=AC.createBuffer(1,len,AC.sampleRate);
    const d=buf.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=(Math.random()*2-1)*(1-i/len);
    b.buffer=buf;
    const f=AC.createBiquadFilter(); f.type='bandpass'; f.frequency.value=600+intensity*900; f.Q.value=1.2;
    const g=AC.createGain(); g.gain.setValueAtTime(0.18*intensity,AC.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+0.32);
    b.connect(f); f.connect(g); g.connect(AC.destination); b.start();
  }
  function sfxCrash(){
    if(!AC) return;
    const b=AC.createBufferSource();
    const len=AC.sampleRate*0.7; const buf=AC.createBuffer(1,len,AC.sampleRate);
    const d=buf.getChannelData(0);
    for(let i=0;i<len;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/len,2);
    b.buffer=buf;
    const f=AC.createBiquadFilter(); f.type='lowpass'; f.frequency.setValueAtTime(1400,AC.currentTime);
    f.frequency.exponentialRampToValueAtTime(120,AC.currentTime+0.6);
    const g=AC.createGain(); g.gain.setValueAtTime(0.6,AC.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+0.7);
    b.connect(f); f.connect(g); g.connect(AC.destination); b.start();
    const o=AC.createOscillator(); o.type='sine'; o.frequency.setValueAtTime(120,AC.currentTime);
    o.frequency.exponentialRampToValueAtTime(40,AC.currentTime+0.6);
    const og=AC.createGain(); og.gain.setValueAtTime(0.5,AC.currentTime);
    og.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+0.6);
    o.connect(og); og.connect(AC.destination); o.start(); o.stop(AC.currentTime+0.7);
  }
  function sfxBoost(){
    if(!AC) return;
    const o=AC.createOscillator(); o.type='sawtooth'; o.frequency.setValueAtTime(80,AC.currentTime);
    o.frequency.exponentialRampToValueAtTime(220,AC.currentTime+0.3);
    const g=AC.createGain(); g.gain.setValueAtTime(0.2,AC.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001,AC.currentTime+0.5);
    const f=AC.createBiquadFilter(); f.type='lowpass'; f.frequency.value=900;
    o.connect(f); f.connect(g); g.connect(AC.destination); o.start(); o.stop(AC.currentTime+0.5);
  }

  // -------------------------------------------------------------------
  //  Game flow
  // -------------------------------------------------------------------
  const hud = document.getElementById('hud');
  const startScreen = document.getElementById('startScreen');
  const gameOverScreen = document.getElementById('gameOverScreen');
  const el = {
    score:document.getElementById('score'), best:document.getElementById('best'),
    mult:document.getElementById('multiplier'), multiWrap:document.getElementById('multiWrap'),
    speedVal:document.getElementById('speedVal'), speedFill:document.getElementById('speedFill'),
    energyFill:document.getElementById('energyFill'), turboFill:document.getElementById('turboFill'),
    turbo:document.getElementById('turbo'), boostHint:document.getElementById('boostHint'),
    finalScore:document.getElementById('finalScore'), finalDist:document.getElementById('finalDist'),
    finalSpeed:document.getElementById('finalSpeed'), finalBest:document.getElementById('finalBest'),
    crashFlash:document.getElementById('crashFlash'),
  };
  el.best.textContent = 'BEST — '+Math.round(bestScore);

  function resetGame(){
    // clear old geometry
    for(const ch of chunks){ scene.remove(ch.group); ch.leftMesh.geometry.dispose(); ch.rightMesh.geometry.dispose(); ch.floorMesh.geometry.dispose(); }
    for(const o of obstacles){ if(o.mesh) scene.remove(o.mesh); }
    chunks.length=0; obstacles.length=0;
    for(const k in ringParams) delete ringParams[k];
    nextRing=0;
    state.pos.set(0,2,0); state.vel.set(0,0,0);
    state.speed=0; state.baseSpeed=48; state.maxSpeed=64;
    state.roll=0; state.pitch=0; state.targetRoll=0; state.targetPitch=0;
    state.energy=1.0; state.turbo=0.0; state.boosting=false;
    state.score=0; state.multiplier=1; state.multTimer=0;
    state.maxSpeedSeen=0; state.distance=0;
    state.shake=0; state.multPulse=0; state.fov=72;
    difficulty=0; prevWallClear=1e9;
    input.x=0; input.y=0; input.boost=false;
    // clear particles
    for(const p of trailState){ p.y=-999; p.life=0; }
    for(let c=0;c<6;c++) makeChunk(nextRing);
    nextRing = CHUNK_RINGS*6; // 72 — next chunk will start here
    state.pos.z = 0;
    craft.visible=true;
  }

  function startGame(){
    initAudio(); if(AC&&AC.state==='suspended') AC.resume();
    resetGame();
    state.running=true; state.over=false;
    startScreen.classList.add('hidden');
    gameOverScreen.classList.add('hidden');
    hud.classList.remove('hidden');
  }

  function gameOver(){
    state.running=false; state.over=true; craft.visible=false;
    sfxCrash();
    if(state.score>bestScore){ bestScore=Math.round(state.score); localStorage.setItem('canyon_best',bestScore); }
    el.finalScore.textContent = Math.round(state.score).toLocaleString();
    el.finalDist.textContent = Math.round(state.distance)+' m';
    el.finalSpeed.textContent = Math.round(state.maxSpeedSeen);
    el.finalBest.textContent = Math.round(bestScore).toLocaleString();
    el.best.textContent = 'BEST — '+Math.round(bestScore);
    el.crashFlash.style.opacity='1';
    setTimeout(()=>{ el.crashFlash.style.opacity='0'; }, 160);
    setTimeout(()=>{ gameOverScreen.classList.remove('hidden'); }, 500);
  }

  document.getElementById('startBtn').addEventListener('click', startGame);
  document.getElementById('restartBtn').addEventListener('click', startGame);

  // -------------------------------------------------------------------
  //  Input
  // -------------------------------------------------------------------
  const keyMap = {
    ArrowLeft:'left', ArrowRight:'right', ArrowUp:'up', ArrowDown:'down',
    KeyA:'left', KeyD:'right', KeyW:'up', KeyS:'down'
  };
  const held = {};
  window.addEventListener('keydown', e=>{
    if(keyMap[e.code]) held[keyMap[e.code]]=true;
    if(e.code==='Space'){ input.boost=true; e.preventDefault(); }
    if((e.code==='Enter'||e.code==='Space') && !state.running){ startGame(); }
  });
  window.addEventListener('keyup', e=>{
    if(keyMap[e.code]) held[keyMap[e.code]]=false;
    if(e.code==='Space') input.boost=false;
  });
  renderer.domElement.addEventListener('contextmenu', e=>{ e.preventDefault(); input.boost=true; });
  renderer.domElement.addEventListener('mouseup', e=>{ if(e.button===2) input.boost=false; });
  // mouse steering
  let mouseActive=false;
  renderer.domElement.addEventListener('mousemove', e=>{
    if(mouseActive){ input.x = THREE.MathUtils.clamp((e.clientX/window.innerWidth)*2-1,-1,1); input.y = THREE.MathUtils.clamp(-((e.clientY/window.innerHeight)*2-1),-1,1); }
  });
  renderer.domElement.addEventListener('mousedown', e=>{ if(e.button===0){ mouseActive=true; } });
  window.addEventListener('mouseup', e=>{ if(e.button===0){ mouseActive=false; input.x=0; input.y=0; } });

  function readInput(){
    let ix=0, iy=0;
    if(held.left) ix-=1; if(held.right) ix+=1;
    if(held.up) iy+=1; if(held.down) iy-=1;
    input.x = THREE.MathUtils.clamp(ix,-1,1);
    input.y = THREE.MathUtils.clamp(iy,-1,1);
  }

  // -------------------------------------------------------------------
  //  Collision helpers
  // -------------------------------------------------------------------
  function passageAt(z){
    const i = Math.floor(z/RING_SPACING);
    const r0 = ringParams[i], r1 = ringParams[i+1];
    if(!r0||!r1) return null;
    const t = (z - r0.z)/RING_SPACING;
    const cx = r0.cx + (r1.cx-r0.cx)*t;
    const hw = r0.hw + (r1.hw-r0.hw)*t;
    const floorY = r0.floorY + (r1.floorY-r0.floorY)*t;
    return { cx, hw, floorY, left:cx-hw, right:cx+hw };
  }

  // -------------------------------------------------------------------
  //  Update loop
  // -------------------------------------------------------------------
  const clock = new THREE.Clock();
  const _camTarget = new THREE.Vector3();
  const _camPos = new THREE.Vector3();

  function update(dt){
    if(!state.running) return;
    readInput();
    difficulty = state.score/4000;

    // ---- forward speed ramps with difficulty ----
    state.maxSpeed = 66 + difficulty*52;
    if(state.boosting){
      state.speed = THREE.MathUtils.lerp(state.speed, state.maxSpeed*1.9, dt*2.2);
      state.turbo = THREE.MathUtils.clamp(state.turbo - dt*0.55, 0, 1);
      if(state.turbo<=0) state.boosting=false;
    } else {
      const target = state.maxSpeed;
      state.baseSpeed = THREE.MathUtils.lerp(state.baseSpeed, target, dt*0.5);
      state.speed = THREE.MathUtils.lerp(state.speed, state.baseSpeed, dt*2.5);
      // sustain turbo when fast straightaways
      if(state.speed > state.maxSpeed*0.86) state.turbo = THREE.MathUtils.clamp(state.turbo+dt*0.06, 0, 1);
    }
    // boost activation
    if(input.boost && (state.turbo>=0.15 || state.turbo>=1.0)) state.boosting=true;

    // ---- lateral + vertical dynamics (weighty) ----
    const lateralAcc = 62, vertAcc = 46, damp = 3.2, grav = 6.0;
    state.vel.x += input.x * lateralAcc * dt;
    state.vel.y += (input.y * vertAcc - grav) * dt;
    state.vel.x *= Math.exp(-damp*dt);
    state.vel.y *= Math.exp(-damp*dt);
    state.pos.x += state.vel.x * dt;
    state.pos.y += state.vel.y * dt;

    // ---- forward progress ----
    state.pos.z += state.speed * dt;
    state.distance = state.pos.z;

    // ---- banking & pitch visuals ----
    state.targetRoll = - THREE.MathUtils.clamp(state.vel.x, -1,1) * 1.05 - input.x*0.3;
    state.targetPitch = THREE.MathUtils.clamp(state.vel.y, -1,1)*0.5 + (input.y*0.3);
    state.roll += (state.targetRoll - state.roll) * Math.exp(-8*dt);
    state.pitch += (state.targetPitch - state.pitch) * Math.exp(-8*dt);

    // ---- canyon collision ----
    const p = passageAt(state.pos.z);
    let crash = false;
    let minWall = 1e9;
    if(p){
      const clearLeft = state.pos.x - p.left;
      const clearRight = p.right - state.pos.x;
      const clearFloor = state.pos.y - p.floorY;
      minWall = Math.min(clearLeft, clearRight, clearFloor);
      if(clearLeft <= PLAYER_R || clearRight <= PLAYER_R || clearFloor <= PLAYER_R) crash = true;
    } else crash = true;

    // ---- obstacles ----
    for(const o of obstacles){
      if(o.collected) continue;
      if(o.z < state.pos.z-30 || o.z > state.pos.z+70) continue;
      const dx=state.pos.x-o.x, dy=state.pos.y-o.y, dz=state.pos.z-o.z;
      const dist = Math.sqrt(dx*dx+dy*dy+dz*dz);
      if(o.type==='orb'){
        if(dist < o.radius+PLAYER_R){
          o.collected=true;
          state.energy = THREE.MathUtils.clamp(state.energy+0.26, 0, 1);
          state.score += 60*state.multiplier;
          state.turbo = THREE.MathUtils.clamp(state.turbo+0.18,0,1);
          sfxPing();
          spawnBurst(o.x,o.y,o.z,0xffd166);
        } else if(!o.nearTriggered && dist < NEAR_MISS+PLAYER_R){
          o.nearTriggered=true; triggerNearMiss(NEAR_MISS/dist);
        }
      } else { // monolith
        const hx=o.hx-PLAYER_R, hy=o.hy-PLAYER_R, hz=o.hz-PLAYER_R;
        const px=Math.abs(dx), py=Math.abs(dy), pz=Math.abs(dz);
        let closest;
        if(px<hx&&py<hy&&pz<hz){ closest=0; }
        else {
          const ex=Math.max(px-hx,0), ey=Math.max(py-hy,0), ez=Math.max(pz-hz,0);
          closest=Math.sqrt(ex*ex+ey*ey+ez*ez);
        }
        if(closest<=0) crash=true;
        else if(!o.nearTriggered && closest<=NEAR_MISS){ o.nearTriggered=true; triggerNearMiss(NEAR_MISS/closest); }
        else if(closest>NEAR_MISS*1.6) o.nearTriggered=false;
      }
    }

    // ---- near miss (walls) ----
    if(!crash && minWall>0 && prevWallClear>NEAR_MISS && minWall<=NEAR_MISS){
      triggerNearMiss(NEAR_MISS/minWall);
    }
    prevWallClear = minWall;

    // ---- multiplier decay ----
    if(state.multTimer>0){ state.multTimer-=dt; if(state.multTimer<=0) state.multiplier=1; }

    // ---- energy decay ----
    const drain = 0.055 + (state.speed/state.maxSpeed)*0.05 + (state.boosting?0.09:0);
    state.energy -= drain*dt;
    if(state.energy<=0){ state.energy=0; crash=true; }

    // ---- scoring (distance) ----
    state.score += state.speed*dt*0.15*state.multiplier;
    if(state.speed>state.maxSpeedSeen) state.maxSpeedSeen=state.speed;

    // ---- crash ----
    if(crash){ gameOver(); return; }

    // ---- mult pulse decay ----
    if(state.multPulse>0) state.multPulse=Math.max(0,state.multPulse-dt*3);

    // ---- camera FOV ----
    const targetFov = state.boosting? 96 : 72 + (state.speed/state.maxSpeed)*6;
    state.fov += (targetFov-state.fov)*Math.exp(-4*dt);
    camera.fov = state.fov; camera.updateProjectionMatrix();

    // ---- shake decay ----
    if(state.shake>0) state.shake=Math.max(0,state.shake-dt*3.5);

    updateParticles(dt);
    updateCraftVisuals(dt);
    updateHUD();
    updateCamera(dt);
    manageChunks();
  }

  let burstPool=[];
  function spawnBurst(x,y,z,color){
    // simple: flash a small orb mesh that expands & fades
    if(burstPool.length>40) return;
    const g=new THREE.Mesh(new THREE.SphereGeometry(1,8,8),
      new THREE.MeshBasicMaterial({color, transparent:true, opacity:0.9, blending:THREE.AdditiveBlending, depthWrite:false}));
    g.position.set(x,y,z); scene.add(g);
    burstPool.push({mesh:g, life:0, max:0.5});
  }
  function updateBursts(dt){
    for(let i=burstPool.length-1;i>=0;i--){
      const b=burstPool[i]; b.life+=dt; const t=b.life/b.max;
      b.mesh.scale.setScalar(1+t*5); b.mesh.material.opacity=0.9*(1-t);
      if(b.life>=b.max){ scene.remove(b.mesh); b.mesh.geometry.dispose(); b.mesh.material.dispose(); burstPool.splice(i,1); }
    }
  }

  function triggerNearMiss(intensity){
    state.multiplier++;
    state.multTimer = 2.4;
    state.score += 80*state.multiplier;
    state.turbo = THREE.MathUtils.clamp(state.turbo + 0.09*intensity, 0, 1);
    state.shake = Math.min(1.2, state.shake + 0.28*intensity);
    state.multPulse = 1;
    sfxWhoosh(Math.min(1.6,intensity));
    el.mult.classList.add('pulse');
    setTimeout(()=>el.mult.classList.remove('pulse'), 90);
  }

  // -------------------------------------------------------------------
  //  Particles
  // -------------------------------------------------------------------
  function updateParticles(dt){
    // trail from engines
    const engineWorldL=new THREE.Vector3(), engineWorldR=new THREE.Vector3();
    engineL.getWorldPosition(engineWorldL); engineR.getWorldPosition(engineWorldR);
    for(let i=0;i<TRAIL_N;i++){
      const p=trailState[i];
      if(p.life<=0){
        // respawn at an engine
        const e = (i%2===0)?engineWorldL:engineWorldR;
        p.x=e.x+(Math.random()-0.5)*0.6; p.y=e.y+(Math.random()-0.5)*0.6; p.z=e.z;
        p.vx=(Math.random()-0.5)*2 - state.speed*0.0; p.vy=(Math.random()-0.5)*2; p.vz=-6-Math.random()*8;
        p.max=0.6+Math.random()*0.6; p.life=p.max;
      }
      p.life-=dt;
      p.x+=p.vx*dt; p.y+=p.vy*dt; p.z+=p.vz*dt;
      trailPos[i*3]=p.x; trailPos[i*3+1]=p.y; trailPos[i*3+2]=p.z;
    }
    trailGeo.attributes.position.needsUpdate=true;
    trailMat.opacity = 0.55 + (state.boosting?0.4:0);

    // speed streaks — stay around player, respawn ahead as we pass
    for(let i=0;i<STREAK_N;i++){
      const s=streakState[i];
      if(s.z < state.pos.z-8){
        s.z = state.pos.z + 90 + Math.random()*620;
        s.x = state.pos.x + (Math.random()*2-1)* (40 + Math.random()*60);
        s.y = 8 + Math.random()*70;
      }
      streakPos[i*3]=s.x; streakPos[i*3+1]=s.y; streakPos[i*3+2]=s.z;
    }
    streakGeo.attributes.position.needsUpdate=true;
    streakMat.opacity = 0.35 + (state.speed/state.maxSpeed)*0.5 + (state.boosting?0.3:0);
  }

  function updateCraftVisuals(dt){
    craft.position.copy(state.pos);
    craft.rotation.z = state.roll;
    craft.rotation.x = state.pitch;
    // engine pulse
    const e = 1.6 + Math.sin(performance.now()*0.02)*0.4 + (state.boosting?1.6:0);
    engineL.material.emissiveIntensity=e; engineR.material.emissiveIntensity=e;
  }

  function updateCamera(dt){
    const speedFrac = state.speed/state.maxSpeed;
    const behind = 11 + speedFrac*8 + (state.boosting?6:0);
    const up = 6 + speedFrac*3;
    _camTarget.set(state.pos.x*0.6, state.pos.y+2, state.pos.z+14);
    const desired = new THREE.Vector3(state.pos.x*0.7, state.pos.y+up, state.pos.z-behind);
    _camPos.lerp(desired, Math.min(1, 4*dt));
    // shake
    const sh = state.shake;
    const sx = (Math.random()-0.5)*sh*1.6;
    const sy = (Math.random()-0.5)*sh*1.6;
    const sz = (Math.random()-0.5)*sh*1.0;
    camera.position.set(_camPos.x+sx, _camPos.y+sy, _camPos.z+sz);
    camera.lookAt(_camTarget.x, _camTarget.y, _camTarget.z+30);
    // sun follows player in X to keep shadows sweeping the canyon
    sun.position.set(state.pos.x-60, state.pos.y+110, state.pos.z+80);
    sun.target.position.copy(state.pos);
    sunDisc.position.set(state.pos.x-160, 44, state.pos.z-1200);
    sunGlow.position.set(state.pos.x-160, 44, state.pos.z-1202);
  }

  // -------------------------------------------------------------------
  //  Chunk management (infinite generation)
  // -------------------------------------------------------------------
  function manageChunks(){
    // ensure chunks ahead
    const needRing = Math.floor((state.pos.z+900)/RING_SPACING);
    while(nextRing*0 + (chunks.length? chunks[chunks.length-1].endRing:0) < needRing){
      const first = chunks.length? chunks[chunks.length-1].endRing+1 : 0;
      if(nextRing>0 && first>0 && first<=chunks[chunks.length-1].endRing+1){
        // continue from where we left off
      }
      const startRing = chunks.length? chunks[chunks.length-1].endRing+1 : 0;
      chunks.push(makeChunk(startRing));
      nextRing = startRing+CHUNK_RINGS;
    }
    // remove chunks behind player
    while(chunks.length && chunks[0].endRing*RING_SPACING < state.pos.z-220){
      const ch=chunks.shift();
      scene.remove(ch.group);
      ch.leftMesh.geometry.dispose(); ch.rightMesh.geometry.dispose(); ch.floorMesh.geometry.dispose();
    }
    // cull passed obstacles
    for(const o of obstacles){
      if(o.collected && o.type==='orb' && o.z < state.pos.z-60){
        if(o.glow){ scene.remove(o.glow); o.glow=null; }
      }
      if(o.type==='monolith' && o.z < state.pos.z-120){
        scene.remove(o.mesh); o.mesh=null;
      }
    }
    // trim old obstacles occasionally
    while(obstacles.length && obstacles[0].z < state.pos.z-200) obstacles.shift();
  }

  // -------------------------------------------------------------------
  //  HUD
  // -------------------------------------------------------------------
  function updateHUD(){
    el.score.textContent = Math.round(state.score).toLocaleString();
    el.mult.textContent = 'x'+state.multiplier;
    const pulse = state.multPulse>0.05 ? (1+state.multPulse*0.3) : 1;
    el.mult.style.transform = 'scale('+pulse+')';
    el.speedVal.textContent = Math.round(state.speed);
    el.speedFill.style.width = Math.min(100,(state.speed/state.maxSpeed)*100)+'%';
    el.energyFill.style.width = (state.energy*100)+'%';
    el.energyFill.classList.toggle('low', state.energy<0.25);
    el.turboFill.style.width = (state.turbo*100)+'%';
    el.turbo.classList.toggle('active', state.boosting);
    el.boostHint.classList.toggle('show', state.turbo>=0.15 && !state.boosting);
  }

  // -------------------------------------------------------------------
  //  Main loop
  // -------------------------------------------------------------------
  function animate(){
    requestAnimationFrame(animate);
    const dt = Math.min(clock.getDelta(), 0.05);
    update(dt);
    updateBursts(dt);
    blurPass.material.uniforms.strength.value = (state.boosting?0.9: state.speed/state.maxSpeed*0.42);
    bloom.strength = state.boosting? 1.05 : 0.72;
    composer.render();
  }

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

  // intro idle camera
  (function idle(){
    if(state.running) return;
    craft.visible=true;
    craft.position.set(0,2,0); craft.rotation.set(0,0,0);
    const t=performance.now()*0.0003;
    camera.position.set(Math.sin(t)*8, 10+Math.sin(t*1.3)*3, -16+Math.cos(t)*4);
    camera.lookAt(0,4,40);
    blurPass.material.uniforms.strength.value=0;
    composer.render();
    requestAnimationFrame(idle);
  })();
  animate();
  </script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"45c72804-dcfa-46a9-a509-13a6fe54da6b","tokensIn":7836601,"tokensOut":134305,"tokensTotal":7970906,"cost":0,"turns":77,"toolCalls":76,"failedToolCalls":5,"timestamp":"2026-09-04T20:22:49.023Z"} -->