← Canyon Run results

Canyon Run

ox alpha · 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 30,393 bytes · SHA-256 30edb930b779
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DUNE RUNNER — Canyon Racer</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; overflow:hidden; background:#1a0f2e; font-family:'Segoe UI',system-ui,sans-serif; }
  #c { position:fixed; inset:0; display:block; }
  #hud { position:fixed; inset:0; pointer-events:none; color:#ffe8d0; user-select:none; }

  /* Speedometer: thin line top center */
  #speedWrap { position:absolute; top:26px; left:50%; transform:translateX(-50%); width:340px; text-align:center; }
  #speedTrack { height:2px; background:rgba(255,232,208,.15); border-radius:2px; overflow:hidden; }
  #speedFill { height:100%; width:0%; background:linear-gradient(90deg,#ff8c42,#ffd166); box-shadow:0 0 8px #ff8c42; transition:width .08s linear; }
  #speedNum { margin-top:6px; font-size:13px; letter-spacing:4px; opacity:.85; font-weight:300; }

  /* Multiplier */
  #mult { position:absolute; top:70px; left:50%; transform:translateX(-50%); font-size:64px; font-weight:200;
    color:#ffd166; text-shadow:0 0 24px rgba(255,209,102,.7); transition:transform .1s; opacity:.9; }
  #mult.pulse { animation:pulse .35s ease-out; }
  @keyframes pulse { 0%{transform:translateX(-50%) scale(1.6);} 100%{transform:translateX(-50%) scale(1);} }
  #score { position:absolute; top:24px; left:30px; font-size:22px; font-weight:300; letter-spacing:2px; }
  #score small { display:block; font-size:10px; letter-spacing:4px; opacity:.5; }

  /* Energy + Turbo bars bottom */
  #bars { position:absolute; bottom:34px; left:50%; transform:translateX(-50%); width:420px; }
  .barLabel { font-size:9px; letter-spacing:4px; opacity:.55; margin-bottom:4px; }
  .bar { height:5px; background:rgba(255,255,255,.08); border-radius:3px; overflow:hidden; margin-bottom:14px; }
  #energyFill { height:100%; width:100%; background:#4de3c1; box-shadow:0 0 12px #4de3c1; border-radius:3px; }
  #energyFill.low { background:#ff5c5c; box-shadow:0 0 12px #ff5c5c; }
  #turboFill { height:100%; width:0%; background:#b388ff; box-shadow:0 0 12px #b388ff; border-radius:3px; }
  #turboWrap { opacity:.45; transition:opacity .3s; }
  #turboWrap.ready { opacity:1; }

  #boostHint { position:absolute; bottom:12px; left:50%; transform:translateX(-50%); font-size:9px; letter-spacing:3px; opacity:.35; }

  /* Overlays */
  .overlay { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
    background:rgba(20,10,35,.82); backdrop-filter:blur(6px); color:#ffe8d0; z-index:10; }
  .overlay h1 { font-size:56px; font-weight:200; letter-spacing:14px; color:#ff8c42; text-shadow:0 0 40px rgba(255,140,66,.6); margin-bottom:8px; }
  .overlay h2 { font-size:15px; font-weight:300; letter-spacing:6px; opacity:.7; margin-bottom:40px; }
  .stats { display:flex; gap:60px; margin-bottom:44px; }
  .stat { text-align:center; }
  .stat .v { font-size:40px; font-weight:200; color:#ffd166; }
  .stat .k { font-size:10px; letter-spacing:4px; opacity:.5; margin-top:4px; }
  .btn { pointer-events:auto; cursor:pointer; background:none; border:1px solid #ff8c42; color:#ff8c42;
    font-size:14px; letter-spacing:6px; padding:14px 46px; transition:all .2s; }
  .btn:hover { background:#ff8c42; color:#1a0f2e; box-shadow:0 0 30px rgba(255,140,66,.6); }
  .keys { margin-top:36px; font-size:10px; letter-spacing:3px; opacity:.4; line-height:2; text-align:center; }
  .hidden { display:none !important; }
</style>
</head>
<body>
<canvas id="c"></canvas>

<div id="hud">
  <div id="score">0<small>SCORE</small></div>
  <div id="speedWrap">
    <div id="speedTrack"><div id="speedFill"></div></div>
    <div id="speedNum">0 KM/H</div>
  </div>
  <div id="mult">×1</div>
  <div id="bars">
    <div class="barLabel">ENERGY</div>
    <div class="bar"><div id="energyFill"></div></div>
    <div id="turboWrap">
      <div class="barLabel">TURBO</div>
      <div class="bar"><div id="turboFill"></div></div>
    </div>
  </div>
  <div id="boostHint">SHIFT — BOOST</div>
</div>

<div id="startScreen" class="overlay">
  <h1>DUNE RUNNER</h1>
  <h2>PROCEDURAL CANYON RACER</h2>
  <div class="keys">
    ◀ ▶ / A D — BANK &nbsp;&nbsp;·&nbsp;&nbsp; ▲ ▼ / W S — PITCH (NOSE DOWN = SPEED)<br>
    SHIFT — TURBO &nbsp;&nbsp;·&nbsp;&nbsp; FLY CLOSE TO WALLS FOR MULTIPLIER &nbsp;&nbsp;·&nbsp;&nbsp; COLLECT ORBS FOR ENERGY
  </div>
  <div style="height:40px"></div>
  <button class="btn" id="startBtn">LAUNCH</button>
</div>

<div id="gameOver" class="overlay hidden">
  <h1 id="goTitle">SIGNAL LOST</h1>
  <h2 id="goReason">HULL INTEGRITY FAILURE</h2>
  <div class="stats">
    <div class="stat"><div class="v" id="finalScore">0</div><div class="k">FINAL SCORE</div></div>
    <div class="stat"><div class="v" id="finalSpeed">0</div><div class="k">MAX SPEED</div></div>
    <div class="stat"><div class="v" id="finalDist">0</div><div class="k">DISTANCE</div></div>
  </div>
  <button class="btn" id="restartBtn">RESTART</button>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/AfterimagePass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/AfterimageShader.js"></script>
<script>
'use strict';
/* ============================================================
   DUNE RUNNER — high-speed procedural canyon racer
   ============================================================ */

// ---------- constants ----------
const CHUNK_LEN = 220;
const CHUNKS_AHEAD = 7;
const CHUNKS_BEHIND = 2;
const WALL_H = 90;
const PLAYER_R = 1.6;
const FLOOR_Y = 0;

// ---------- renderer / scene ----------
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, powerPreference:'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.75));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x2a1440);
scene.fog = new THREE.Fog(0x2a1440, 120, 950);

const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 2000);
const BASE_FOV = 75, BOOST_FOV = 96;

// ---------- lighting: golden hour ----------
const hemi = new THREE.HemisphereLight(0xffb37a, 0x3a1f5c, 0.75);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffa050, 1.5);
sun.position.set(-160, 60, 60);   // low sun = long dramatic shadows
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -180; sun.shadow.camera.right = 180;
sun.shadow.camera.top = 200;  sun.shadow.camera.bottom = -200;
sun.shadow.camera.near = 10;  sun.shadow.camera.far = 700;
sun.shadow.bias = -0.0006;
scene.add(sun);
scene.add(sun.target);

// sun disc + gradient sky handled by big backdrop sphere
const skyGeo = new THREE.SphereGeometry(1500, 16, 12);
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide, depthWrite:false,
  uniforms: { },
  vertexShader: `varying vec3 vP; void main(){ vP=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader: `
    varying vec3 vP;
    void main(){
      float h = normalize(vP).y;
      vec3 top = vec3(0.13,0.06,0.25);
      vec3 mid = vec3(0.55,0.18,0.42);
      vec3 hor = vec3(1.0,0.45,0.18);
      vec3 c = h>0.0 ? mix(mid,top,pow(h,0.6)) : mix(hor,mid*0.6,pow(-h,0.5));
      // sun glow toward -x horizon
      vec3 d = normalize(vP);
      float s = pow(max(dot(d, normalize(vec3(-0.9,0.12,0.3))),0.0), 40.0);
      c += vec3(1.0,0.6,0.25)*s*1.4;
      gl_FragColor = vec4(c,1.0);
    }`
});
const sky = new THREE.Mesh(skyGeo, skyMat);
scene.add(sky);

// ---------- materials (flat shaded low-poly) ----------
const matWall = new THREE.MeshStandardMaterial({ color:0x6b4a8c, flatShading:true, roughness:0.95, metalness:0.05 });
const matWallDark = new THREE.MeshStandardMaterial({ color:0x4a3268, flatShading:true, roughness:0.95, metalness:0.05 });
const matMono = new THREE.MeshStandardMaterial({ color:0x2e1f4d, flatShading:true, roughness:0.7, metalness:0.3, emissive:0x1a0f33, emissiveIntensity:0.4 });
const matOrb = new THREE.MeshBasicMaterial({ color:0x4de3c1 });
const matFloor = new THREE.MeshStandardMaterial({ color:0x3a2456, flatShading:true, roughness:1 });

// floor
const floor = new THREE.Mesh(new THREE.PlaneGeometry(1200, 6000, 24, 120), matFloor);
floor.rotation.x = -Math.PI/2;
floor.position.z = -2000;
floor.receiveShadow = true;
scene.add(floor);

// ---------- post-processing ----------
const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));
const afterimage = new THREE.AfterimagePass(0.55);
composer.addPass(afterimage);
const bloom = new THREE.UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.9, 0.5, 0.72);
composer.addPass(bloom);

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

// ---------- RNG / noise ----------
let seed = 1337;
function rnd(){ seed = (seed*16807) % 2147483647; return (seed-1)/2147483646; }
function jitterGeo(geo, amt){
  const p = geo.attributes.position;
  for (let i=0;i<p.count;i++){
    p.setXYZ(i, p.getX(i)+(rnd()-0.5)*amt, p.getY(i)+(rnd()-0.5)*amt, p.getZ(i)+(rnd()-0.5)*amt);
  }
  geo.computeVertexNormals();
  return geo;
}

// ---------- game state ----------
const G = {
  running:false, over:false,
  score:0, mult:1, multTimer:0,
  speed:42, baseSpeed:42, maxSpeed:0, dist:0,
  energy:100, turbo:0, boosting:false,
  shake:0, time:0,
  px:0, pvx:0, py:8, pvy:0, pitch:0, roll:0,
  bestGap: 60,
};

// ---------- player craft ----------
const player = new THREE.Group();
{
  const bodyGeo = jitterGeo(new THREE.ConeGeometry(1.5, 6, 4), 0.15);
  bodyGeo.rotateX(-Math.PI/2);
  const body = new THREE.Mesh(bodyGeo, new THREE.MeshStandardMaterial({ color:0xd8d8e8, flatShading:true, roughness:0.35, metalness:0.7 }));
  body.castShadow = true;
  player.add(body);
  // wings
  const wingGeo = new THREE.BoxGeometry(7, 0.18, 1.8);
  const wing = new THREE.Mesh(wingGeo, new THREE.MeshStandardMaterial({ color:0xff8c42, flatShading:true, roughness:0.4, metalness:0.6 }));
  wing.position.z = 1.2;
  wing.castShadow = true;
  player.add(wing);
  // fin
  const fin = new THREE.Mesh(new THREE.BoxGeometry(0.16, 1.6, 1.6), wing.material);
  fin.position.set(0, 0.8, 1.4);
  player.add(fin);
  // engine glow
  const engMat = new THREE.SpriteMaterial({ color:0x55ccee, blending:THREE.AdditiveBlending, transparent:true, opacity:0.45, depthWrite:false });
  const eng = new THREE.Sprite(engMat);
  eng.scale.set(1.4,1.4,1); eng.position.z = 3.2;
  player.add(eng);
  player.userData.engine = eng;
}
scene.add(player);

// ---------- trail particles ----------
const TRAIL_N = 220;
const trailGeo = new THREE.BufferGeometry();
const trailPos = new Float32Array(TRAIL_N*3);
const trailLife = new Float32Array(TRAIL_N);
for (let i=0;i<TRAIL_N;i++){ trailPos[i*3+1] = -999; }
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
const trail = new THREE.Points(trailGeo, new THREE.PointsMaterial({
  color:0x66ccff, size:1.4, transparent:true, opacity:0.8,
  blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true
}));
scene.add(trail);
let trailHead = 0;

// ---------- dust / speed particles ----------
const DUST_N = 500;
const dustGeo = new THREE.BufferGeometry();
const dustPos = new Float32Array(DUST_N*3);
for (let i=0;i<DUST_N;i++){
  dustPos[i*3]   = (rnd()-0.5)*260;
  dustPos[i*3+1] = rnd()*70;
  dustPos[i*3+2] = -rnd()*500;
}
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3));
const dust = new THREE.Points(dustGeo, new THREE.PointsMaterial({
  color:0xffc8a0, size:0.55, transparent:true, opacity:0.55, depthWrite:false
}));
scene.add(dust);

// ---------- chunk system ----------
const chunks = new Map(); // index -> {group, colliders, orbs, nearMissed:Set, gapCenter, gapWidth}
let nextChunkIdx = 0;

function chunkIndexAt(z){ return Math.floor(-z / CHUNK_LEN); }

function makeWallSlab(cx, cz, w, h, d, mat){
  const geo = jitterGeo(new THREE.BoxGeometry(w, h, d), Math.min(3, w*0.12));
  const m = new THREE.Mesh(geo, mat);
  m.position.set(cx, h/2 - 2, cz);
  m.castShadow = true; m.receiveShadow = true;
  return m;
}

function addOrb(chunk, x, y, z){
  const orb = new THREE.Mesh(new THREE.IcosahedronGeometry(1.4, 0), matOrb);
  orb.position.set(x, y, z);
  const halo = new THREE.Sprite(new THREE.SpriteMaterial({ color:0x4de3c1, blending:THREE.AdditiveBlending, transparent:true, opacity:0.5 }));
  halo.scale.set(7,7,1);
  orb.add(halo);
  chunk.group.add(orb);
  chunk.orbs.push(orb);
}

function addCollider(chunk, mesh, pad=0){
  const b = new THREE.Box3().setFromObject(mesh);
  b.expandByScalar(pad);
  chunk.colliders.push({ box:b, mesh, nm:false });
}

function genChunk(idx){
  const diff = Math.min(1, G.dist / 9000);           // 0..1 difficulty
  const z0 = -idx * CHUNK_LEN;
  const group = new THREE.Group();
  const chunk = { group, colliders:[], orbs:[], nm:new Set(), gapCenter:0, gapWidth:60 };

  // wandering canyon path
  const prev = chunks.get(idx-1);
  const prevC = prev ? prev.gapCenter : 0;
  const prevW = prev ? prev.gapWidth : 60;
  const targetW = 62 - 26*diff + rnd()*18;
  chunk.gapWidth = prevW + (targetW - prevW)*0.35;
  chunk.gapCenter = THREE.MathUtils.clamp(prevC + (rnd()-0.5)*70*(0.5+diff), -90, 90);

  const gc = chunk.gapCenter, gw = chunk.gapWidth;
  const SUBS = 4, SL = CHUNK_LEN/SUBS;

  // canyon walls: stacked jagged slabs on each side
  for (let s=0; s<SUBS; s++){
    const cz = z0 - s*SL - SL/2;
    const sideJit = (rnd()-0.5)*14;
    for (const side of [-1, 1]){
      const inner = gc + side*(gw/2 + sideJit*0.5);
      const w = 60 + rnd()*40;
      const cx = inner + side*w/2;
      // 2-3 stacked slabs for geological look
      const nSlabs = 2 + (rnd()>0.5?1:0);
      let hAcc = 0;
      for (let k=0;k<nSlabs;k++){
        const h = (WALL_H - hAcc)/(nSlabs-k) * (0.7+rnd()*0.6);
        const slab = makeWallSlab(cx + (rnd()-0.5)*6, cz + (rnd()-0.5)*8, w*(0.8+rnd()*0.4), h, SL*(0.9+rnd()*0.25), k%2?matWallDark:matWall);
        slab.position.y = hAcc + h/2 - 2;
        hAcc += h*0.85;
        group.add(slab);
        if (k===0) addCollider(chunk, slab, 0.5);
      }
    }
  }

  // tight squeeze: pinch walls narrowing the gap mid-chunk (more often with difficulty)
  if (rnd() < 0.35 + diff*0.45){
    const pinch = gw * (0.62 - diff*0.18);
    const px_ = gc + (rnd()-0.5)*(gw-pinch)*0.8;
    const cz = z0 - CHUNK_LEN*(0.35 + rnd()*0.3);
    for (const side of [-1,1]){
      const w = (gw-pinch)/2 + 8;
      const slab = makeWallSlab(px_ + side*(pinch/2 + w/2), cz, w, 34, 26, matWallDark);
      group.add(slab);
      addCollider(chunk, slab, 0.5);
    }
    // reward orb in the squeeze
    addOrb(chunk, px_, 7, cz);
  }

  // floating monoliths
  const nMono = Math.floor(1 + diff*3 + rnd()*1.5);
  for (let i=0;i<nMono;i++){
    const h = 18 + rnd()*40;
    const w = 4 + rnd()*7;
    const mx = gc + (rnd()-0.5)*(gw+30);
    const mz = z0 - rnd()*CHUNK_LEN;
    const geo = jitterGeo(new THREE.BoxGeometry(w, h, w*(0.6+rnd()*0.8)), 0.8);
    const m = new THREE.Mesh(geo, matMono);
    m.position.set(mx, 4 + h/2 + rnd()*10, mz);
    m.rotation.y = rnd()*Math.PI;
    m.castShadow = true;
    group.add(m);
    addCollider(chunk, m, 0.4);
  }

  // energy orbs: line of 2-4, sometimes risky near a wall
  const nOrbs = 2 + Math.floor(rnd()*3);
  let ox = gc, oz = z0 - 20;
  for (let i=0;i<nOrbs;i++){
    oz -= CHUNK_LEN/(nOrbs+1);
    ox += (gc - ox)*0.5 + (rnd()-0.5)*20;
    const risky = rnd() < 0.35;
    const x = risky ? gc + (rnd()>0.5?1:-1)*(gw/2 - 5) : ox;
    addOrb(chunk, THREE.MathUtils.clamp(x, gc-gw/2+3, gc+gw/2-3), 5 + rnd()*8, oz);
  }

  scene.add(group);
  return chunk;
}

function updateChunks(){
  const cur = chunkIndexAt(player.position.z);
  for (let i = cur - CHUNKS_BEHIND; i <= cur + CHUNKS_AHEAD; i++){
    if (i >= 0 && !chunks.has(i)) chunks.set(i, genChunk(i));
  }
  for (const [idx, ch] of chunks){
    if (idx < cur - CHUNKS_BEHIND - 1){
      scene.remove(ch.group);
      ch.group.traverse(o => { if (o.geometry) o.geometry.dispose(); });
      chunks.delete(idx);
    }
  }
}

// ---------- audio (Web Audio synth) ----------
let AC = null, engineOsc, engineOsc2, engineGain, engineFilter, noiseBuf;
function initAudio(){
  if (AC) return;
  AC = new (window.AudioContext||window.webkitAudioContext)();
  engineGain = AC.createGain(); engineGain.gain.value = 0;
  engineFilter = AC.createBiquadFilter(); engineFilter.type='lowpass'; engineFilter.frequency.value = 400;
  engineOsc = AC.createOscillator(); engineOsc.type='sawtooth'; engineOsc.frequency.value = 55;
  engineOsc2 = AC.createOscillator(); engineOsc2.type='sawtooth'; engineOsc2.frequency.value = 55.7;
  engineOsc.connect(engineFilter); engineOsc2.connect(engineFilter);
  engineFilter.connect(engineGain); engineGain.connect(AC.destination);
  engineOsc.start(); engineOsc2.start();
  // noise buffer for whoosh
  noiseBuf = AC.createBuffer(1, AC.sampleRate*1, AC.sampleRate);
  const d = noiseBuf.getChannelData(0);
  for (let i=0;i<d.length;i++) d[i] = Math.random()*2-1;
}
function sfxEngine(speedN, boost){
  if (!AC) return;
  const f = 50 + speedN*90 + (boost?40:0);
  engineOsc.frequency.setTargetAtTime(f, AC.currentTime, 0.08);
  engineOsc2.frequency.setTargetAtTime(f*1.013, AC.currentTime, 0.08);
  engineFilter.frequency.setTargetAtTime(300 + speedN*900, AC.currentTime, 0.1);
  engineGain.gain.setTargetAtTime(0.05 + speedN*0.06, AC.currentTime, 0.1);
}
function sfxPing(){
  if (!AC) return;
  const t = AC.currentTime;
  const o = AC.createOscillator(), g = AC.createGain();
  o.type='sine'; o.frequency.setValueAtTime(880, t); o.frequency.exponentialRampToValueAtTime(1760, t+0.08);
  g.gain.setValueAtTime(0.18, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.25);
  o.connect(g); g.connect(AC.destination); o.start(t); o.stop(t+0.3);
}
function sfxWhoosh(){
  if (!AC) return;
  const t = AC.currentTime;
  const src = AC.createBufferSource(); src.buffer = noiseBuf;
  const f = AC.createBiquadFilter(); f.type='bandpass'; f.Q.value=1.2;
  f.frequency.setValueAtTime(300, t); f.frequency.exponentialRampToValueAtTime(2400, t+0.18);
  const g = AC.createGain();
  g.gain.setValueAtTime(0.0001, t);
  g.gain.exponentialRampToValueAtTime(0.3, t+0.06);
  g.gain.exponentialRampToValueAtTime(0.001, t+0.35);
  src.connect(f); f.connect(g); g.connect(AC.destination);
  src.start(t); src.stop(t+0.4);
}
function sfxCrash(){
  if (!AC) return;
  const t = AC.currentTime;
  const src = AC.createBufferSource(); src.buffer = noiseBuf;
  const f = AC.createBiquadFilter(); f.type='lowpass';
  f.frequency.setValueAtTime(1200, t); f.frequency.exponentialRampToValueAtTime(80, t+0.5);
  const g = AC.createGain();
  g.gain.setValueAtTime(0.4, t); g.gain.exponentialRampToValueAtTime(0.001, t+0.6);
  src.connect(f); f.connect(g); g.connect(AC.destination);
  src.start(t); src.stop(t+0.7);
}
// ---------- input ----------
const keys = {};
addEventListener('keydown', e => {
  keys[e.code] = true;
  if (['ArrowUp','ArrowDown','Space'].includes(e.code)) e.preventDefault();
  initAudio();
  if (AC && AC.state === 'suspended') AC.resume();
});
addEventListener('keyup', e => keys[e.code] = false);

// ---------- UI refs ----------
const $ = id => document.getElementById(id);
const ui = {
  score:$('score'), speedFill:$('speedFill'), speedNum:$('speedNum'), mult:$('mult'),
  energyFill:$('energyFill'), turboFill:$('turboFill'), turboWrap:$('turboWrap'),
  startScreen:$('startScreen'), gameOver:$('gameOver'),
  finalScore:$('finalScore'), finalSpeed:$('finalSpeed'), finalDist:$('finalDist'),
  goTitle:$('goTitle'), goReason:$('goReason'),
};

function resetGame(){
  for (const [idx, ch] of chunks){
    scene.remove(ch.group);
    ch.group.traverse(o => { if (o.geometry) o.geometry.dispose(); });
  }
  chunks.clear();
  seed = (Math.random()*2147483000)|0 + 1;
  Object.assign(G, {
    running:true, over:false, score:0, mult:1, multTimer:0,
    speed:42, baseSpeed:42, maxSpeed:0, dist:0,
    energy:100, turbo:0, boosting:false, shake:0,
    px:0, pvx:0, py:8, pvy:0, pitch:0, roll:0,
  });
  player.position.set(0, 8, 0);
  camera.position.set(0, 12, 14);
  updateChunks();
  ui.startScreen.classList.add('hidden');
  ui.gameOver.classList.add('hidden');
}
$('startBtn').onclick = () => { initAudio(); resetGame(); };
$('restartBtn').onclick = resetGame;

function gameOver(reason){
  G.running = false; G.over = true;
  sfxCrash();
  if (AC) engineGain.gain.setTargetAtTime(0, AC.currentTime, 0.2);
  ui.goReason.textContent = reason;
  ui.finalScore.textContent = Math.floor(G.score).toLocaleString();
  ui.finalSpeed.textContent = Math.floor(G.maxSpeed*3.6/10)*10 + ' KM/H';
  ui.finalDist.textContent = (G.dist/1000).toFixed(1) + ' KM';
  ui.gameOver.classList.remove('hidden');
}

// ---------- collision & near-miss ----------
const tmpBox = new THREE.Box3();
const pSphere = new THREE.Sphere();

function checkCollisions(dt){
  const cur = chunkIndexAt(player.position.z);
  let nearest = Infinity;
  for (let i = cur-1; i <= cur+1; i++){
    const ch = chunks.get(i);
    if (!ch) continue;
    for (const c of ch.colliders){
      // skip if already behind player or far ahead (forward = -z)
      if (c.box.min.z > player.position.z + 30 || c.box.max.z < player.position.z - 60) continue;
      pSphere.set(player.position, PLAYER_R);
      if (pSphere.intersectsBox(c.box)){
        gameOver('HULL INTEGRITY FAILURE'); return;
      }
      // distance to box surface (approx, xz)
      const dx = Math.max(c.box.min.x - player.position.x, 0, player.position.x - c.box.max.x);
      const dz = Math.max(c.box.min.z - player.position.z, 0, player.position.z - c.box.max.z);
      const dy = Math.max(c.box.min.y - player.position.y, 0, player.position.y - c.box.max.y);
      const d = Math.sqrt(dx*dx + dz*dz + dy*dy);
      if (d < nearest) nearest = d;
      // near miss: within 5 units while passing alongside
      if (!c.nm && d < 6 && d > 0.01){
        c.nm = true;
        G.multTimer = 3.5;
        G.mult = Math.min(20, G.mult + 1);
        G.score += 50 * G.mult;
        G.turbo = Math.min(100, G.turbo + 14);
        G.shake = Math.min(1, G.shake + 0.45);
        sfxWhoosh();
        ui.mult.classList.remove('pulse');
        void ui.mult.offsetWidth;
        ui.mult.classList.add('pulse');
      }
    }
  }
  G.nearest = nearest;
}

function checkOrbs(){
  const cur = chunkIndexAt(player.position.z);
  for (let i = cur-1; i <= cur+1; i++){
    const ch = chunks.get(i);
    if (!ch) continue;
    for (let j = ch.orbs.length-1; j >= 0; j--){
      const o = ch.orbs[j];
      o.rotation.y += 0.03; o.rotation.x += 0.015;
      if (o.position.distanceToSquared(player.position) < 36){
        ch.group.remove(o);
        ch.orbs.splice(j, 1);
        G.energy = Math.min(100, G.energy + 22);
        G.score += 25 * G.mult;
        G.turbo = Math.min(100, G.turbo + 20);
        sfxPing();
      }
    }
  }
}

// ---------- trail ----------
function updateTrail(dt){
  trailGeo.attributes.position.array; // noop keep ref
  // emit two points per frame at engines
  for (let k=0;k<2;k++){
    const i = trailHead = (trailHead+1) % TRAIL_N;
    trailPos[i*3]   = player.position.x + (k?1.4:-1.4)*Math.cos(player.rotation.z);
    trailPos[i*3+1] = player.position.y + (k?1.4:-1.4)*Math.sin(player.rotation.z)*0.3;
    trailPos[i*3+2] = player.position.z + 3.4;
    trailLife[i] = 1;
  }
  for (let i=0;i<TRAIL_N;i++){
    if (trailLife[i] > 0){
      trailLife[i] -= dt*1.6;
      trailPos[i*3+2] += G.speed*dt*0.35;
      if (trailLife[i] <= 0) trailPos[i*3+1] = -999;
    }
  }
  trailGeo.attributes.position.needsUpdate = true;
  trail.material.opacity = 0.4 + Math.min(0.5, G.speed/220);
}

// ---------- main loop ----------
let lastT = performance.now();
function tick(){
  requestAnimationFrame(tick);
  const now = performance.now();
  let dt = Math.min(0.05, (now - lastT)/1000);
  lastT = now;
  G.time += dt;

  if (G.running){
    // ----- difficulty scaling -----
    const diff = Math.min(1, G.dist / 9000);
    G.baseSpeed = 42 + diff*55;

    // ----- boost -----
    const wantBoost = (keys['ShiftLeft']||keys['ShiftRight']||keys['Space']) && G.turbo > 1;
    G.boosting = wantBoost;
    if (wantBoost){ G.turbo = Math.max(0, G.turbo - dt*26); }

    // ----- pitch-based acceleration -----
    let pitchInput = 0;
    if (keys['ArrowDown']||keys['KeyS']) pitchInput -= 1;   // nose down = faster
    if (keys['ArrowUp']||keys['KeyW'])   pitchInput += 1;   // nose up = brake/lift
    G.pitch += (pitchInput*0.42 - G.pitch) * Math.min(1, dt*6);

    const targetSpeed = G.baseSpeed * (1 + G.pitch*-0.28) * (G.boosting ? 1.65 : 1);
    G.speed += (targetSpeed - G.speed) * Math.min(1, dt*1.8);
    G.maxSpeed = Math.max(G.maxSpeed, G.speed);

    // ----- lateral momentum physics -----
    let lat = 0;
    if (keys['ArrowLeft']||keys['KeyA'])  lat -= 1;
    if (keys['ArrowRight']||keys['KeyD']) lat += 1;
    const accel = 95 * (G.boosting ? 0.75 : 1);
    G.pvx += lat * accel * dt;
    G.pvx *= Math.pow(0.02, dt);              // drag (momentum)
    G.pvx = THREE.MathUtils.clamp(G.pvx, -55, 55);
    G.px += G.pvx * dt;

    // vertical: gentle bob + slight pitch lift
    let vIn = 0;
    if (keys['ArrowUp']||keys['KeyW']) vIn += 1;
    if (keys['ArrowDown']||keys['KeyS']) vIn -= 1;
    G.pvy += (vIn*38 - G.pvy) * Math.min(1, dt*5);
    G.py += G.pvy * dt;
    G.py = THREE.MathUtils.clamp(G.py, 3.2, 46);
    G.py += Math.sin(G.time*2.2)*0.008;

    player.position.set(G.px, G.py, player.position.z - G.speed*dt);
    G.dist += G.speed*dt;

    // banking roll + visual pitch
    const targetRoll = -G.pvx * 0.016 + lat * -0.25;
    G.roll += (targetRoll - G.roll) * Math.min(1, dt*7);
    player.rotation.set(-G.pitch*0.5 + G.pvy*0.004, G.pvx*0.002, G.roll);
    player.userData.engine.scale.setScalar(0.9 + G.speed/300 + (G.boosting?0.7:0) + Math.sin(G.time*30)*0.06);

    // ----- world systems -----
    updateChunks();
    checkCollisions(dt);
    if (!G.over) checkOrbs();
    updateTrail(dt);

    // ----- energy decay -----
    G.energy -= dt * (3.2 + diff*2.4 + (G.boosting ? 3 : 0));
    if (G.energy <= 0){ G.energy = 0; gameOver('ENERGY DEPLETED'); }

    // ----- score / multiplier decay -----
    G.multTimer -= dt;
    if (G.multTimer <= 0 && G.mult > 1){ G.mult--; G.multTimer = 2.5; }
    G.score += G.speed * dt * 0.5 * G.mult;

    // ----- dust particles fly past -----
    const dp = dustGeo.attributes.position.array;
    for (let i=0;i<DUST_N;i++){
      dp[i*3+2] += G.speed*dt*1.4;
      if (dp[i*3+2] > 40){
        dp[i*3+2] = -500 - rnd()*100;
        dp[i*3]   = player.position.x + (rnd()-0.5)*260;
        dp[i*3+1] = rnd()*70;
      }
    }
    dustGeo.attributes.position.needsUpdate = true;

    // ----- audio -----
    sfxEngine(Math.min(1, G.speed/200), G.boosting);

    // ----- HUD -----
    ui.score.firstChild.nodeValue = Math.floor(G.score).toLocaleString();
    const spN = Math.min(1, G.speed/230);
    ui.speedFill.style.width = (spN*100)+'%';
    ui.speedNum.textContent = Math.floor(G.speed*3.6)+' KM/H';
    ui.mult.textContent = '×'+G.mult;
    ui.mult.style.opacity = G.mult>1 ? 1 : 0.45;
    ui.energyFill.style.width = G.energy+'%';
    ui.energyFill.classList.toggle('low', G.energy < 28);
    ui.turboFill.style.width = G.turbo+'%';
    ui.turboWrap.classList.toggle('ready', G.turbo > 15);
  }

  // ----- camera follow + shake + FOV -----
  const camTargetX = G.px*0.86, camTargetY = G.py + 4.2;
  camera.position.x += (camTargetX - camera.position.x)*Math.min(1, dt*5);
  camera.position.y += (camTargetY - camera.position.y)*Math.min(1, dt*5);
  camera.position.z = player.position.z + 13;
  G.shake *= Math.pow(0.05, dt);
  const sh = G.shake*0.9 + (G.boosting ? 0.18 : 0) + (G.nearest < 3 && G.running ? 0.08 : 0);
  camera.position.x += (Math.random()-0.5)*sh;
  camera.position.y += (Math.random()-0.5)*sh;
  camera.lookAt(player.position.x*0.94, player.position.y + 1, player.position.z - 24);
  camera.rotation.z += G.roll*0.28;
  const targetFov = G.boosting ? BOOST_FOV : BASE_FOV + Math.min(8, (G.speed-42)*0.06);
  camera.fov += (targetFov - camera.fov)*Math.min(1, dt*4);
  camera.updateProjectionMatrix();

  // sun shadow frustum follows player
  sun.position.set(player.position.x - 160, 60, player.position.z + 60);
  sun.target.position.set(player.position.x, 0, player.position.z - 80);

  sky.position.copy(camera.position);
  floor.position.z = Math.floor(player.position.z/100)*100 - 1500;

  // motion blur scales with speed
  afterimage.uniforms['damp'].value = G.running ? Math.min(0.82, 0.45 + G.speed/500 + (G.boosting?0.12:0)) : 0.3;
  bloom.strength = 0.85 + (G.boosting?0.5:0);

  composer.render();
}

// idle demo state before start: slow drift so scene is alive behind menu
player.position.set(0, 8, 0);
updateChunks();
G.nearest = Infinity;
tick();
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"7d11f9fb-7b14-4aeb-bd12-f8bec21133e2","tokensIn":356313,"tokensOut":14769,"tokensTotal":371082,"cost":0,"turns":16,"toolCalls":15,"failedToolCalls":0,"timestamp":"2026-08-21T21:30:43.432Z"} -->