← Canyon Run results

Canyon Run

Qwen 3.8 · 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 42,055 bytes · SHA-256 5eb5e76d5601
<!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 RUSH — High-Speed Procedural Canyon Racer</title>
<style>
:root{
  --orange:#ff8a4d; --gold:#ffd9a0; --silver:#cfd3e8; --purple:#8a63c9; --deep:#120820;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:var(--deep)}
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;color:var(--silver);user-select:none;-webkit-user-select:none;touch-action:none}
canvas{display:block}
.mono{font-family:ui-monospace,'Cascadia Code','SF Mono',Menlo,Consolas,monospace}
[hidden]{display:none !important}

#edgeVig{position:fixed;inset:0;pointer-events:none;z-index:9;box-shadow:inset 0 0 130px 24px rgba(8,2,18,.6)}
#vignette{position:fixed;inset:0;pointer-events:none;z-index:9;box-shadow:inset 0 0 170px 46px rgba(255,92,40,.9);opacity:0}

#hud{position:fixed;inset:0;pointer-events:none;z-index:10}
#multBlock{position:absolute;top:24px;left:32px}
#mult{font-size:54px;font-weight:800;font-style:italic;line-height:1;color:var(--silver);text-shadow:0 0 24px rgba(255,138,77,.55);transform-origin:left top}
#mult.pulse{animation:mpulse .35s ease-out}
@keyframes mpulse{0%{transform:scale(1)}35%{transform:scale(1.34);color:var(--gold)}100%{transform:scale(1)}}
#multLabel{font-size:10px;letter-spacing:.34em;color:var(--purple);margin-top:6px}
#scoreBlock{position:absolute;top:26px;right:32px;text-align:right}
#score{font-size:34px;font-weight:700;letter-spacing:.05em;color:var(--gold);text-shadow:0 0 18px rgba(255,138,77,.5)}
#best{font-size:11px;letter-spacing:.28em;color:var(--purple);margin-top:3px}
#gauge{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;gap:9px}
#energyWrap{position:relative;width:92px;height:92px}
#energyWrap svg{width:92px;height:92px;transform:rotate(-90deg);filter:drop-shadow(0 0 6px rgba(255,217,160,.55))}
#energyText{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;color:var(--gold)}
#energyWrap.low #energyText{color:#ff5a3c;animation:blink .5s infinite}
#energyWrap.low svg{filter:drop-shadow(0 0 8px rgba(255,90,60,.8))}
@keyframes blink{50%{opacity:.35}}
#speedLine{position:relative;width:240px;height:3px;background:rgba(207,211,232,.16)}
#speedFill{position:absolute;left:0;top:0;height:100%;width:0;background:linear-gradient(90deg,#8a63c9,#cfd3e8);box-shadow:0 0 8px rgba(207,211,232,.7)}
#turboFill{position:absolute;left:0;top:-2px;height:7px;width:0;background:linear-gradient(90deg,#ff8a4d,#ffd9a0);box-shadow:0 0 10px rgba(255,138,77,.85)}
#speedText{font-size:13px;letter-spacing:.24em;color:var(--silver)}
#boostReady{font-size:10px;letter-spacing:.4em;color:var(--gold);opacity:0;transition:opacity .25s;height:12px}
#boostBtn{position:absolute;bottom:30px;right:32px;font-size:11px;letter-spacing:.32em;color:var(--purple);border:1px solid rgba(138,99,201,.35);padding:10px 18px 10px 22px;background:rgba(18,8,32,.55)}
#boostBtn.ready{color:var(--gold);border-color:var(--orange);box-shadow:0 0 18px rgba(255,138,77,.45);text-shadow:0 0 10px rgba(255,138,77,.8)}

.screen{position:fixed;inset:0;z-index:20;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(12,4,22,.74);backdrop-filter:blur(3px);text-align:center;cursor:pointer;padding:20px}
.screen h1{font-family:'Arial Black','Segoe UI',sans-serif;font-size:clamp(40px,7vw,72px);font-weight:900;font-style:italic;letter-spacing:.12em;color:var(--gold);text-shadow:0 0 32px rgba(255,138,77,.85),0 0 90px rgba(255,138,77,.4)}
.screen .sub{margin-top:12px;font-size:12px;letter-spacing:.42em;color:var(--purple)}
.controls{margin-top:38px;display:flex;gap:30px;font-size:11px;letter-spacing:.2em;color:var(--silver);flex-wrap:wrap;justify-content:center}
.controls b{display:block;color:var(--gold);font-size:13px;letter-spacing:.3em;margin-bottom:6px}
.cta{margin-top:46px;padding:15px 46px;border:1px solid var(--orange);color:var(--gold);font-size:13px;letter-spacing:.42em;background:transparent;cursor:pointer;font-family:inherit;pointer-events:auto}
.cta:hover{background:rgba(255,138,77,.16);box-shadow:0 0 26px rgba(255,138,77,.4)}
.statRow{display:flex;gap:44px;margin-top:34px;flex-wrap:wrap;justify-content:center}
.stat .v{font-size:32px;font-weight:700;color:var(--gold);text-shadow:0 0 16px rgba(255,138,77,.5)}
.stat .k{font-size:10px;letter-spacing:.3em;color:var(--purple);margin-top:6px}
#loadFail{z-index:30}
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.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/shaders/GammaCorrectionShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/AfterimageShader.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/postprocessing/UnrealBloomPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/AfterimagePass.js"></script>
</head>
<body>
<div id="edgeVig"></div>
<div id="vignette"></div>

<div id="hud">
  <div id="multBlock">
    <div id="mult" class="mono">&times;1.0</div>
    <div id="multLabel">MULTIPLIER</div>
  </div>
  <div id="scoreBlock">
    <div id="score" class="mono">0</div>
    <div id="best">BEST 0</div>
  </div>
  <div id="gauge">
    <div id="energyWrap">
      <svg viewBox="0 0 92 92">
        <circle cx="46" cy="46" r="40" fill="none" stroke="rgba(207,211,232,.15)" stroke-width="5"/>
        <circle id="energyArc" cx="46" cy="46" r="40" fill="none" stroke="#ffd9a0" stroke-width="5" stroke-linecap="round" stroke-dasharray="251.33" stroke-dashoffset="0"/>
      </svg>
      <div id="energyText" class="mono">100</div>
    </div>
    <div id="speedLine"><div id="turboFill"></div><div id="speedFill"></div></div>
    <div id="speedText" class="mono">0</div>
    <div id="boostReady">BOOST READY</div>
  </div>
  <div id="boostBtn">BOOST</div>
</div>

<div id="startScreen" class="screen">
  <h1>CANYON RUSH</h1>
  <div class="sub">HIGH-SPEED PROCEDURAL CANYON RACER</div>
  <div class="controls">
    <div><b>MOUSE</b>STEER</div>
    <div><b>SHIFT</b>BOOST</div>
    <div><b>ORBS</b>ENERGY</div>
    <div><b>WALLS</b>MULTIPLIER</div>
  </div>
  <button class="cta" id="startBtn">CLICK TO LAUNCH</button>
</div>

<div id="overScreen" class="screen" hidden>
  <h1 id="overTitle">CRASHED</h1>
  <div class="sub" id="overSub">SIGNAL LOST</div>
  <div class="statRow">
    <div class="stat"><div class="v mono" id="finalScore">0</div><div class="k">FINAL SCORE</div></div>
    <div class="stat"><div class="v mono" id="finalSpeed">0</div><div class="k">MAX SPEED</div></div>
    <div class="stat"><div class="v mono" id="finalNear">0</div><div class="k">NEAR MISSES</div></div>
    <div class="stat"><div class="v mono" id="finalOrbs">0</div><div class="k">ORBS</div></div>
  </div>
  <button class="cta" id="restartBtn">RESTART</button>
</div>

<div id="loadFail" class="screen" hidden>
  <h1 style="font-size:34px">ENGINE OFFLINE</h1>
  <div class="sub" id="loadFailMsg">Failed to load Three.js</div>
</div>

<script>
'use strict';
var $ = id => document.getElementById(id);
var clamp = (v, a, b) => v < a ? a : v > b ? b : v;

// ================= Procedural Audio (Web Audio API) =================
var sfx = {
  ctx: null, master: null, engGain: null, engOsc1: null, engOsc2: null, engFilter: null, noiseBuf: null,
  init(){
    if (this.ctx){ if (this.ctx.state === 'suspended') this.ctx.resume(); return; }
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.5;
    this.master.connect(this.ctx.destination);
    const len = this.ctx.sampleRate;
    this.noiseBuf = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
    const d = this.noiseBuf.getChannelData(0);
    for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
    // Engine: detuned saw + square through lowpass
    this.engGain = this.ctx.createGain();
    this.engGain.gain.value = 0;
    this.engFilter = this.ctx.createBiquadFilter();
    this.engFilter.type = 'lowpass';
    this.engFilter.frequency.value = 300;
    this.engOsc1 = this.ctx.createOscillator();
    this.engOsc1.type = 'sawtooth';
    this.engOsc1.frequency.value = 46;
    this.engOsc2 = this.ctx.createOscillator();
    this.engOsc2.type = 'square';
    this.engOsc2.frequency.value = 23;
    const g2 = this.ctx.createGain();
    g2.gain.value = 0.35;
    this.engOsc1.connect(this.engFilter);
    this.engOsc2.connect(g2);
    g2.connect(this.engFilter);
    this.engFilter.connect(this.engGain);
    this.engGain.connect(this.master);
    this.engOsc1.start();
    this.engOsc2.start();
  },
  engine(f, boosting){
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    this.engGain.gain.setTargetAtTime(0.03 + f * 0.11 + (boosting ? 0.05 : 0), t, 0.12);
    const base = 40 + f * 72 + (boosting ? 30 : 0);
    this.engOsc1.frequency.setTargetAtTime(base, t, 0.12);
    this.engOsc2.frequency.setTargetAtTime(base * 0.5, t, 0.12);
    this.engFilter.frequency.setTargetAtTime(240 + f * 1300 + (boosting ? 900 : 0), t, 0.15);
  },
  engineOff(){
    if (!this.ctx) return;
    this.engGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.25);
  },
  whoosh(i){
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const src = this.ctx.createBufferSource();
    src.buffer = this.noiseBuf;
    const f = this.ctx.createBiquadFilter();
    f.type = 'bandpass'; f.Q.value = 1.3;
    f.frequency.setValueAtTime(280 + i * 420, t);
    f.frequency.exponentialRampToValueAtTime(1100 + i * 1600, t + 0.16);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(0.2 + i * 0.3, t + 0.05);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.3);
    src.connect(f); f.connect(g); g.connect(this.master);
    src.start(t); src.stop(t + 0.32);
  },
  ping(){
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    [660, 990].forEach((fr, k) => {
      const o = this.ctx.createOscillator();
      o.type = 'sine'; o.frequency.value = fr;
      const g = this.ctx.createGain();
      const t0 = t + k * 0.07;
      g.gain.setValueAtTime(0.0001, t0);
      g.gain.exponentialRampToValueAtTime(0.2, t0 + 0.02);
      g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.22);
      o.connect(g); g.connect(this.master);
      o.start(t0); o.stop(t0 + 0.26);
    });
  },
  boost(){
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const src = this.ctx.createBufferSource();
    src.buffer = this.noiseBuf;
    const f = this.ctx.createBiquadFilter();
    f.type = 'bandpass'; f.Q.value = 0.8;
    f.frequency.setValueAtTime(240, t);
    f.frequency.exponentialRampToValueAtTime(2600, t + 0.5);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(0.28, t + 0.12);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55);
    src.connect(f); f.connect(g); g.connect(this.master);
    src.start(t); src.stop(t + 0.6);
  },
  explode(){
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const src = this.ctx.createBufferSource();
    src.buffer = this.noiseBuf;
    const f = this.ctx.createBiquadFilter();
    f.type = 'lowpass';
    f.frequency.setValueAtTime(1200, t);
    f.frequency.exponentialRampToValueAtTime(60, t + 0.9);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.5, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 1);
    src.connect(f); f.connect(g); g.connect(this.master);
    src.start(t); src.stop(t + 1.05);
    const o = this.ctx.createOscillator();
    o.type = 'sine';
    o.frequency.setValueAtTime(110, t);
    o.frequency.exponentialRampToValueAtTime(28, t + 0.7);
    const og = this.ctx.createGain();
    og.gain.setValueAtTime(0.4, t);
    og.gain.exponentialRampToValueAtTime(0.0001, t + 0.8);
    o.connect(og); og.connect(this.master);
    o.start(t); o.stop(t + 0.85);
  }
};
</script>
<script>
'use strict';
// ================= Guard: Three.js CDN loaded? =================
if (typeof THREE === 'undefined' || !THREE.EffectComposer || !THREE.UnrealBloomPass || !THREE.AfterimagePass) {
  document.getElementById('loadFail').hidden = false;
  document.getElementById('loadFailMsg').textContent = 'Failed to load Three.js from CDN';
  throw new Error('THREE not available');
}

// ================= Tunables =================
const CFG = {
  baseSpeed: 34, maxSpeed: 86, boostSpeed: 150,
  energyDecay: 3.2, orbEnergy: 24,
  multDecay: 1.6, multMax: 10,
  chunkLen: 60, chunksAhead: 10,
  playerRadius: 1.15, nearMissDist: 4.2,
};
const hash = n => { const s = Math.sin(n) * 43758.5453; return s - Math.floor(s); };
const damp = (a, b, l, dt) => a + (b - a) * (1 - Math.exp(-l * dt));
const difficultyAt = z => clamp(z * 0.00035, 0, 0.85);

// ================= Renderer / scene =================
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
let pixelCap = Math.min(window.devicePixelRatio, 1.75);
let shadowRes = 1024;
{
  const gl = renderer.getContext();
  const dbg = gl.getExtension('WEBGL_debug_renderer_info');
  const rstr = dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : '';
  if (/swiftshader|llvmpipe|software/i.test(rstr)) { pixelCap = 1; shadowRes = 512; }
}
renderer.setPixelRatio(pixelCap);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMappingExposure = 1.05;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x8a3550, 70, 380);

// Sky gradient (cyber-sunset)
{
  const c = document.createElement('canvas');
  c.width = 2; c.height = 512;
  const g = c.getContext('2d');
  const grad = g.createLinearGradient(0, 0, 0, 512);
  grad.addColorStop(0, '#150a28');
  grad.addColorStop(0.42, '#3d1c4e');
  grad.addColorStop(0.68, '#8a2f56');
  grad.addColorStop(0.84, '#e05a3e');
  grad.addColorStop(1, '#ffb377');
  g.fillStyle = grad;
  g.fillRect(0, 0, 2, 512);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  scene.background = tex;
}

const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 700);
camera.position.set(0, 8, -12);

// Sun disc at the end of the canyon
const sun = new THREE.Mesh(new THREE.CircleGeometry(34, 32), new THREE.MeshBasicMaterial({ color: new THREE.Color(2.3, 1.85, 1.15), fog: false }));
const sunGlow = new THREE.Mesh(new THREE.CircleGeometry(74, 32), new THREE.MeshBasicMaterial({ color: new THREE.Color(1.6, 0.9, 0.5), transparent: true, opacity: 0.3, fog: false }));
sunGlow.rotation.y = Math.PI;
sun.rotation.y = Math.PI;
scene.add(sun, sunGlow);

// Lights — low golden-hour sun for long shadows
const hemi = new THREE.HemisphereLight(0xffb070, 0x2a1430, 0.75);
scene.add(hemi);
const sunLight = new THREE.DirectionalLight(0xffb070, 2.7);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(shadowRes, shadowRes);
sunLight.shadow.camera.left = -55;
sunLight.shadow.camera.right = 55;
sunLight.shadow.camera.top = 55;
sunLight.shadow.camera.bottom = -55;
sunLight.shadow.camera.near = 5;
sunLight.shadow.camera.far = 140;
sunLight.shadow.bias = -0.002;
scene.add(sunLight, sunLight.target);

// ================= Shared geometry / materials =================
const wallMat = new THREE.MeshStandardMaterial({ vertexColors: true, flatShading: true, roughness: 0.92, metalness: 0.05, side: THREE.DoubleSide });
const monoMat = new THREE.MeshStandardMaterial({ color: 0x6b4a86, flatShading: true, roughness: 0.7, metalness: 0.25, emissive: 0x180d2c, emissiveIntensity: 0.9 });
const orbMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(2.4, 1.8, 1.1) });
const orbGeo = new THREE.IcosahedronGeometry(0.8, 0);
const monoGeo = new THREE.CylinderGeometry(1, 1, 30, 8, 1);

// ================= Procedural corridor =================
function pathX(z) {
  const s = difficultyAt(z);
  const amp = 6 + s * 9;
  return Math.sin(z * 0.011) * amp + Math.sin(z * 0.0037 + 1.7) * amp * 0.6;
}
function pathY(z) {
  const s = difficultyAt(z);
  return 4 + Math.sin(z * 0.007 + 0.8) * (2 + s * 3);
}
function corridor(z, s) {
  const cx = pathX(z) + Math.sin(z * 0.021 + 2.1) * 4;
  const cy = pathY(z) + Math.sin(z * 0.017 + 0.5) * 2.5;
  let halfGap = (14.5 + s * 5) * (0.78 + 0.3 * Math.sin(z * 0.033 + 1.2));
  const sq = (Math.sin(z * 0.013 + 4.2) + 1) * 0.5;
  const squeeze = clamp((sq - 0.8) / 0.2, 0, 1);
  if (s > 0.12) halfGap *= 1 - squeeze * (0.28 + s * 0.14);
  halfGap = Math.max(halfGap, 5.6 - s * 1.2);
  return { cx, cy, halfGap };
}

// ================= Chunk building =================
const SURF_BASE = [new THREE.Color(0x7b4a8e), new THREE.Color(0x9a5548), new THREE.Color(0x3b2549), new THREE.Color(0x2b1a3d)];

function genSurface(kind, i, cz, s) {
  const steps = 48;
  const P = [], C = [], I = [];
  const base = SURF_BASE[kind];
  const outSign = kind === 0 ? -1 : kind === 1 ? 1 : kind === 2 ? -1 : 1;
  for (let k = 0; k <= steps; k++) {
    const z = cz + (k / steps) * CFG.chunkLen;
    const { cx, cy, halfGap } = corridor(z, s);
    const a0 = kind < 2 ? cy - halfGap : cx - halfGap;
    const a1 = kind < 2 ? cy + halfGap : cx + halfGap;
    const innerX = kind === 0 ? cx - halfGap : kind === 1 ? cx + halfGap : 0;
    const innerY = kind === 2 ? cy - halfGap : kind === 3 ? cy + halfGap : 0;
    for (let j = 0; j < 3; j++) {
      const n1 = hash(i * 57.3 + k * 2.7 + j * 9.1);
      const n2 = hash(i * 31.7 + k * 4.3 + j * 15.7);
      const out = j === 0 ? 0 : j === 1 ? 4 + 4 * n1 : 9 + 7 * n2;
      for (let r = 0; r < 2; r++) {
        const lift = j === 0 ? 0 : j === 1 ? (r === 0 ? 3 + 3 * n1 : 1.5 + 2 * n2) : (r === 0 ? 7 + 5 * n2 : 4 + 4 * n1);
        const span = r === 0 ? a0 : a1;
        let x, y;
        if (kind < 2) { x = innerX + outSign * (out + lift); y = span; }
        else { y = innerY + outSign * (out + lift); x = span; }
        P.push(x, y, z);
        const m = 0.72 + 0.55 * hash(i * 13.1 + k * 1.3 + j * 3.7 + r * 2.1);
        C.push(base.r * m, base.g * m, base.b * m);
      }
    }
  }
  for (let k = 0; k < steps; k++) {
    for (let j = 0; j < 2; j++) {
      const a = k * 6 + j * 2, b = a + 1, c = a + 6, d = c + 1;
      I.push(a, c, b, b, c, d);
    }
  }
  return { P, C, I };
}

function buildChunk(i) {
  const cz = i * CFG.chunkLen;
  const s = difficultyAt(cz);
  const group = new THREE.Group();
  let P = [], C = [], I = [], offset = 0;
  for (let kind = 0; kind < 4; kind++) {
    const g = genSurface(kind, i, cz, s);
    for (let n = 0; n < g.I.length; n++) I.push(g.I[n] + offset);
    P = P.concat(g.P);
    C = C.concat(g.C);
    offset += g.P.length / 3;
  }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(P, 3));
  geo.setAttribute('color', new THREE.Float32BufferAttribute(C, 3));
  geo.setIndex(I);
  const mesh = new THREE.Mesh(geo, wallMat);
  mesh.receiveShadow = true;
  group.add(mesh);
  scene.add(group);

  const chunk = { group, wallGeo: geo, orbs: [], monos: [] };

  // Floating monolith (bank to avoid)
  if (i > 2 && i % 3 === 1) {
    const z = cz + CFG.chunkLen * 0.5;
    const { cx, cy, halfGap } = corridor(z, s);
    const r = 2.2 + hash(i * 3.3) * 1.4;
    const maxOff = Math.max(1, halfGap - r - 2);
    const off = (hash(i * 7.7) - 0.5) * 2 * maxOff;
    const m = new THREE.Mesh(monoGeo, monoMat);
    m.scale.set(r, 1, r);
    m.position.set(cx + off, cy, z);
    m.castShadow = true;
    m.receiveShadow = true;
    group.add(m);
    chunk.monos.push({ x: cx + off, y: cy, z, r, mesh: m, spin: (hash(i * 9.1) - 0.5) * 0.8 });
  }

  // Energy orbs — biased toward the risky edges
  const nOrbs = 5 + (i % 3);
  for (let k = 0; k < nOrbs; k++) {
    const z = cz + ((k + 0.5) / (nOrbs + 1)) * CFG.chunkLen;
    const { cx, cy, halfGap } = corridor(z, s);
    const side = hash(i * 11.3 + k) < 0.5 ? -1 : 1;
    const x = cx + side * halfGap * (0.55 + 0.3 * hash(i * 5.1 + k * 2.2));
    const y = cy + (hash(i * 13.7 + k * 3.3) - 0.5) * halfGap * 0.7;
    const o = new THREE.Mesh(orbGeo, orbMat);
    o.position.set(x, y, z);
    group.add(o);
    chunk.orbs.push({ mesh: o, baseY: y, phase: hash(i * 17.1 + k) * 6.28, taken: false });
  }
  return chunk;
}

function disposeChunk(c) {
  scene.remove(c.group);
  c.wallGeo.dispose();
}

const chunks = new Map();
let chunkIndex = -2;
function syncChunks() {
  const pz = player.pos.z;
  const minI = Math.floor((pz - 130) / CFG.chunkLen);
  const maxI = Math.floor((pz + CFG.chunksAhead * CFG.chunkLen) / CFG.chunkLen);
  while (chunkIndex <= maxI) {
    chunks.set(chunkIndex, buildChunk(chunkIndex));
    chunkIndex++;
  }
  for (const [k, c] of chunks) {
    if (k < minI) { disposeChunk(c); chunks.delete(k); }
  }
}

// ================= Player craft =================
function buildCraft() {
  const g = new THREE.Group();
  const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd9dcef, flatShading: true, metalness: 0.6, roughness: 0.3, emissive: 0x241536, emissiveIntensity: 0.7 });
  const darkMat = new THREE.MeshStandardMaterial({ color: 0x3a2a55, flatShading: true, metalness: 0.4, roughness: 0.5 });
  const accentMat = new THREE.MeshStandardMaterial({ color: 0xff8a4d, flatShading: true, emissive: 0xff5a2a, emissiveIntensity: 2.4 });
  const glowMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(3.2, 2.1, 1.1) });
  const fus = new THREE.Mesh(new THREE.OctahedronGeometry(1, 0), bodyMat);
  fus.scale.set(0.55, 0.38, 2.4);
  fus.castShadow = true;
  const wing = new THREE.Mesh(new THREE.BoxGeometry(3.6, 0.1, 1.5), bodyMat);
  wing.position.set(0, -0.05, -0.4);
  wing.castShadow = true;
  const tipGeo = new THREE.BoxGeometry(0.35, 0.22, 0.9);
  const tipL = new THREE.Mesh(tipGeo, accentMat);
  tipL.position.set(-1.7, 0.02, -0.5);
  const tipR = tipL.clone();
  tipR.position.x = 1.7;
  const tail = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.7, 1.1), darkMat);
  tail.position.set(0, 0.45, -1.1);
  const cockpit = new THREE.Mesh(new THREE.TetrahedronGeometry(0.5, 0), darkMat);
  cockpit.position.set(0, 0.32, 0.5);
  const glow = new THREE.Mesh(new THREE.IcosahedronGeometry(0.42, 0), glowMat);
  glow.position.set(0, 0, -1.55);
  g.add(fus, wing, tipL, tipR, tail, cockpit, glow);
  scene.add(g);
  return { group: g, glow };
}
const craft = buildCraft();

// ================= Particles =================
// Engine trail
const TRAIL_N = 90;
const trailPos = new Float32Array(TRAIL_N * 3);
for (let i = 0; i < TRAIL_N; i++) trailPos[i * 3 + 1] = -9999;
const trailGeo = new THREE.BufferGeometry();
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
const trailMat = new THREE.LineBasicMaterial({ color: 0xffb37a, transparent: true, opacity: 0.5 });
const trail = new THREE.Line(trailGeo, trailMat);
trail.frustumCulled = false;
scene.add(trail);

// Speed streaks
const LINE_N = 110;
const linePos = new Float32Array(LINE_N * 6);
const lineGeo = new THREE.BufferGeometry();
lineGeo.setAttribute('position', new THREE.BufferAttribute(linePos, 3));
const lineMat = new THREE.LineBasicMaterial({ color: 0xcfd3e8, transparent: true, opacity: 0 });
const speedLines = new THREE.LineSegments(lineGeo, lineMat);
speedLines.frustumCulled = false;
scene.add(speedLines);

// Dust
const DUST_N = 220;
const dustPos = new Float32Array(DUST_N * 3);
for (let i = 0; i < DUST_N; i++) {
  const a = hash(i * 3.1) * 6.283, rad = 14 + hash(i * 4.7) * 22;
  dustPos[i * 3] = Math.cos(a) * rad;
  dustPos[i * 3 + 1] = 4 + Math.sin(a) * rad * 0.7;
  dustPos[i * 3 + 2] = -30 + hash(i * 7.3) * 110;
}
const dustGeo = new THREE.BufferGeometry();
dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3));
const dust = new THREE.Points(dustGeo, new THREE.PointsMaterial({ color: 0xffc9a0, size: 0.22, transparent: true, opacity: 0.5 }));
dust.frustumCulled = false;
scene.add(dust);

// Explosion
const EXP_N = 150;
const expPos = new Float32Array(EXP_N * 3);
const expVel = new Float32Array(EXP_N * 3);
const expLife = new Float32Array(EXP_N);
for (let i = 0; i < EXP_N; i++) expPos[i * 3 + 1] = -9999;
const expGeo = new THREE.BufferGeometry();
expGeo.setAttribute('position', new THREE.BufferAttribute(expPos, 3));
const explosion = new THREE.Points(expGeo, new THREE.PointsMaterial({ color: new THREE.Color(2.5, 1.5, 0.8), size: 0.6, transparent: true, opacity: 0.95 }));
explosion.frustumCulled = false;
scene.add(explosion);

const ring = new THREE.Mesh(new THREE.RingGeometry(1, 1.4, 48), new THREE.MeshBasicMaterial({ color: 0xffb37a, transparent: true, opacity: 0.8, side: THREE.DoubleSide }));
ring.visible = false;
scene.add(ring);
let ringLife = 1;

function explodeAt(p) {
  for (let i = 0; i < EXP_N; i++) {
    expPos[i * 3] = p.x; expPos[i * 3 + 1] = p.y; expPos[i * 3 + 2] = p.z;
    const a = Math.random() * Math.PI * 2, b = Math.acos(2 * Math.random() - 1), sp = 6 + Math.random() * 26;
    expVel[i * 3] = Math.sin(b) * Math.cos(a) * sp;
    expVel[i * 3 + 1] = Math.cos(b) * sp;
    expVel[i * 3 + 2] = Math.sin(b) * Math.sin(a) * sp;
    expLife[i] = 0.8 + Math.random() * 0.8;
  }
  ring.visible = true;
  ring.position.copy(p);
  ring.scale.set(0.1, 0.1, 0.1);
  ringLife = 0;
}

// ================= Post-processing =================
const composer = new THREE.EffectComposer(renderer);
composer.addPass(new THREE.RenderPass(scene, camera));
const afterimage = new THREE.AfterimagePass(0.82);
composer.addPass(afterimage);
const bloom = new THREE.UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.7, 0.5, 0.6);
composer.addPass(bloom);
composer.addPass(new THREE.ShaderPass(THREE.GammaCorrectionShader));

// ================= State =================
const state = {
  mode: 'menu',
  score: 0,
  best: 0,
  speed: CFG.baseSpeed,
  energy: 100,
  mult: 1,
  nearMisses: 0,
  orbs: 0,
  maxSpeed: 0,
  turbo: 0,
  boosting: false,
  boostT: 0,
  shake: 0,
  time: 0,
  lastWhoosh: -1,
  crashReason: 'CRASHED',
};
try { state.best = parseInt(localStorage.getItem('cr_best') || '0', 10) || 0; } catch (e) {}

const player = {
  pos: new THREE.Vector3(0, 5, 0),
  vel: new THREE.Vector3(0, 0, 0),
  bank: 0,
  pitch: 0,
};

// ================= Input =================
let targetMX = 0, targetMY = 0, mouseX = 0, mouseY = 0, rawMX = 0, rawMY = 0;
const keys = {};
window.addEventListener('pointermove', e => {
  rawMX = (e.clientX / window.innerWidth) * 2 - 1;
  rawMY = (e.clientY / window.innerHeight) * 2 - 1;
  targetMX = rawMX;
  targetMY = rawMY;
});
window.addEventListener('keydown', e => {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
  if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
  if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = true;
  if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = true;
  if (e.code === 'Space' || e.code === 'ShiftLeft' || e.code === 'ShiftRight') { e.preventDefault(); tryBoost(); }
});
window.addEventListener('keyup', e => {
  if (e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
  if (e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
  if (e.code === 'ArrowUp' || e.code === 'KeyW') keys.up = false;
  if (e.code === 'ArrowDown' || e.code === 'KeyS') keys.down = false;
});

function tryBoost() {
  if (state.mode !== 'playing' || state.boosting || state.turbo < 100) return;
  state.boosting = true;
  state.boostT = 2.3;
  state.turbo = 0;
  state.shake = Math.max(state.shake, 0.5);
  sfx.boost();
}

// ================= HUD refs =================
const elMult = $('mult'), elScore = $('score'), elBest = $('best');
const elEnergyArc = $('energyArc'), elEnergyText = $('energyText'), elEnergyWrap = $('energyWrap');
const elSpeedFill = $('speedFill'), elTurboFill = $('turboFill'), elSpeedText = $('speedText');
const elBoostReady = $('boostReady'), elBoostBtn = $('boostBtn'), elVignette = $('vignette');
const ARCL = 251.33;
elBest.textContent = 'BEST ' + state.best;

function pulseMult() {
  elMult.classList.remove('pulse');
  void elMult.offsetWidth;
  elMult.classList.add('pulse');
}

// ================= Game flow =================
function reset() {
  for (const c of chunks.values()) disposeChunk(c);
  chunks.clear();
  chunkIndex = -2;
  state.score = 0; state.speed = CFG.baseSpeed; state.energy = 100; state.mult = 1;
  state.nearMisses = 0; state.orbs = 0; state.maxSpeed = 0; state.turbo = 0;
  state.boosting = false; state.boostT = 0; state.shake = 0; state.time = 0; state.lastWhoosh = -1;
  player.pos.set(0, pathY(0), 0);
  player.vel.set(0, 0, 0);
  player.bank = 0; player.pitch = 0;
  craft.group.visible = true;
  trail.visible = true;
  for (let i = 0; i < TRAIL_N; i++) trailPos[i * 3 + 1] = -9999;
  for (let i = 0; i < EXP_N; i++) expPos[i * 3 + 1] = -9999;
  ring.visible = false;
  camera.position.set(player.pos.x, player.pos.y + 2.6, player.pos.z - 9.5);
  syncChunks();
  state.mode = 'playing';
}

function crash(reason) {
  if (state.mode !== 'playing') return;
  state.mode = 'over';
  state.crashReason = reason;
  sfx.explode();
  sfx.engineOff();
  state.shake = 1.2;
  explodeAt(player.pos);
  craft.group.visible = false;
  trail.visible = false;
  state.best = Math.max(state.best, Math.floor(state.score));
  try { localStorage.setItem('cr_best', String(state.best)); } catch (e) {}
  setTimeout(showOver, 900);
}

function showOver() {
  $('overTitle').textContent = state.crashReason;
  $('overSub').textContent = state.crashReason === 'CRASHED' ? 'SIGNAL LOST' : 'POWER DEPLETED';
  $('finalScore').textContent = Math.floor(state.score).toLocaleString();
  $('finalSpeed').textContent = Math.round(state.maxSpeed * 3.4);
  $('finalNear').textContent = state.nearMisses;
  $('finalOrbs').textContent = state.orbs;
  elBest.textContent = 'BEST ' + state.best;
  $('overScreen').hidden = false;
}

$('startScreen').addEventListener('click', () => {
  sfx.init();
  reset();
  $('startScreen').hidden = true;
});
$('restartBtn').addEventListener('click', e => {
  e.stopPropagation();
  sfx.init();
  reset();
  $('overScreen').hidden = true;
});

// ================= Update =================
function update(dt) {
  state.time += dt;
  const t = state.time;
  const speedFrac = clamp((state.speed - CFG.baseSpeed) / (CFG.boostSpeed - CFG.baseSpeed), 0, 1);

  // Sun stays ahead
  sun.position.set(player.pos.x * 0.4, 9, player.pos.z + 430);
  sunGlow.position.set(player.pos.x * 0.4, 9, player.pos.z + 429);

  // Light follows player (long shadows)
  sunLight.position.set(player.pos.x - 28, player.pos.y + 20, player.pos.z + 26);
  sunLight.target.position.copy(player.pos);
  sunLight.target.updateMatrixWorld();

  if (state.mode === 'menu') {
    const p0 = pathY(0);
    craft.group.position.set(0, p0 + Math.sin(t * 1.1) * 0.5, 0);
    craft.group.rotation.set(Math.sin(t * 0.5) * 0.12, Math.sin(t * 0.35) * 0.4, Math.sin(t * 0.7) * 0.28);
    const a = t * 0.35;
    camera.position.set(4 + Math.cos(a) * 7, p0 + 1.5 + Math.sin(t * 0.6) * 1.2, Math.sin(a) * 7 - 1);
    camera.lookAt(0, p0, 0);
    camera.fov = damp(camera.fov, 70, 4, dt);
    camera.updateProjectionMatrix();
    updateOrbSpin(dt);
    updateExplosion(dt);
    state.shake = Math.max(0, state.shake - dt * 1.8);
    updateHUD();
    return;
  }

  if (state.mode === 'over') {
    camera.position.z += 10 * dt;
    camera.lookAt(player.pos.x, player.pos.y, player.pos.z);
    updateExplosion(dt);
    state.shake = Math.max(0, state.shake - dt * 1.8);
    updateHUD();
    return;
  }

  // ---------- playing ----------
  // Steering (mouse + keys), smoothed
  if (keys.left) targetMX = -1;
  else if (keys.right) targetMX = 1;
  else targetMX = rawMX;
  if (keys.up) targetMY = -1;
  else if (keys.down) targetMY = 1;
  else targetMY = rawMY;
  mouseX = damp(mouseX, targetMX, 10, dt);
  mouseY = damp(mouseY, targetMY, 10, dt);

  const s = difficultyAt(player.pos.z);
  let targetSpeed = CFG.baseSpeed + s * (CFG.maxSpeed - CFG.baseSpeed);
  if (state.boosting) {
    state.boostT -= dt;
    if (state.boostT <= 0) state.boosting = false;
    targetSpeed = CFG.boostSpeed;
  }
  state.speed = damp(state.speed, targetSpeed, 1.5, dt);
  state.maxSpeed = Math.max(state.maxSpeed, state.speed);

  // Flight physics: momentum-based lateral/vertical velocity
  const steer = mouseX, pitchIn = mouseY;
  player.vel.x = damp(player.vel.x, steer * state.speed * 0.42, 6, dt);
  player.vel.y = damp(player.vel.y, pitchIn * state.speed * 0.30, 6, dt);
  player.pos.x += player.vel.x * dt;
  player.pos.y += player.vel.y * dt;
  player.pos.z += state.speed * dt;

  // Proximity / collision vs corridor walls
  const cor = corridor(player.pos.z, s);
  const dx = player.pos.x - cor.cx, dy = player.pos.y - cor.cy;
  const proxX = cor.halfGap - Math.abs(dx);
  const proxY = cor.halfGap - Math.abs(dy);
  let prox = Math.min(proxX, proxY);

  if (prox < CFG.playerRadius) { crash('CRASHED'); updateHUD(); return; }

  // Monoliths
  for (const [k, c] of chunks) {
    const cz = k * CFG.chunkLen;
    if (Math.abs(cz + CFG.chunkLen * 0.5 - player.pos.z) > CFG.chunkLen) continue;
    for (const m of c.monos) {
      if (Math.abs(m.z - player.pos.z) > 18) continue;
      const mdx = player.pos.x - m.x, mdy = player.pos.y - m.y;
      const d = Math.sqrt(mdx * mdx + mdy * mdy) - (m.r + CFG.playerRadius);
      if (d < 0) { crash('CRASHED'); updateHUD(); return; }
      if (d < CFG.nearMissDist) {
        prox = Math.min(prox, d);
      }
    }
  }

  // Near-miss reward
  if (prox < CFG.nearMissDist) {
    state.mult = Math.min(CFG.multMax, state.mult + (CFG.nearMissDist - prox) * 3.5 * dt);
    if (prox < 2.4 && t - state.lastWhoosh > 0.35) {
      state.lastWhoosh = t;
      state.nearMisses++;
      state.shake = Math.max(state.shake, 0.22 + (2.4 - prox) * 0.25);
      sfx.whoosh(clamp((2.4 - prox) / 2.4, 0, 1));
      pulseMult();
    }
  } else {
    state.mult = Math.max(1, state.mult - CFG.multDecay * dt);
  }

  // Orbs
  for (const [k, c] of chunks) {
    const cz = k * CFG.chunkLen;
    if (Math.abs(cz + CFG.chunkLen * 0.5 - player.pos.z) > CFG.chunkLen) continue;
    for (const o of c.orbs) {
      if (o.taken) continue;
      const odx = player.pos.x - o.mesh.position.x;
      const ody = player.pos.y - o.mesh.position.y;
      const odz = player.pos.z - o.mesh.position.z;
      if (odx * odx + ody * ody + odz * odz < 3.6) {
        o.taken = true;
        o.mesh.visible = false;
        state.orbs++;
        state.energy = Math.min(100, state.energy + CFG.orbEnergy);
        state.score += 250 * state.mult;
        state.turbo = Math.min(100, state.turbo + 8);
        state.shake = Math.max(state.shake, 0.12);
        sfx.ping();
      }
    }
  }

  // Energy decay
  state.energy -= (CFG.energyDecay + s * 2.6 + (state.boosting ? 4.5 : 0)) * dt;
  if (state.energy <= 0) { state.energy = 0; crash('POWER DEPLETED'); updateHUD(); return; }

  // Score + turbo
  state.score += state.speed * dt * state.mult * 0.6;
  state.turbo = clamp(state.turbo + ((state.speed > 55 ? 16 : 6) + (prox < 3 ? 12 : 0)) * dt, 0, 100);

  // Craft transform: bank + pitch
  player.bank = damp(player.bank, -steer * 0.85, 8, dt);
  player.pitch = damp(player.pitch, pitchIn * 0.55, 8, dt);
  craft.group.position.copy(player.pos);
  craft.group.rotation.set(-player.pitch, -steer * 0.35, player.bank);
  craft.glow.scale.setScalar(1 + speedFrac * 1.3 + Math.sin(t * 22) * 0.15);

  // Trail
  trailGeo.attributes.position.array.copyWithin(3, 6);
  trailPos[0] = player.pos.x;
  trailPos[1] = player.pos.y - 0.3;
  trailPos[2] = player.pos.z - 1.7;
  trailGeo.attributes.position.needsUpdate = true;
  trailMat.opacity = 0.35 + speedFrac * 0.45;

  // Camera follow + shake + graze offset
  const graze = prox < 3 ? (dx > 0 ? -1 : 1) * (3 - prox) * 0.35 : 0;
  const camTX = player.pos.x * 0.82 + graze;
  const camTY = player.pos.y * 0.7 + 2.6;
  const camTZ = player.pos.z - 9.5;
  camera.position.x = damp(camera.position.x, camTX, 6, dt);
  camera.position.y = damp(camera.position.y, camTY, 6, dt);
  camera.position.z = damp(camera.position.z, camTZ, 6, dt);
  const shakeAmp = state.shake * 0.5;
  camera.position.x += (Math.random() - 0.5) * shakeAmp;
  camera.position.y += (Math.random() - 0.5) * shakeAmp;
  camera.lookAt(player.pos.x + player.vel.x * 0.8, player.pos.y + player.vel.y * 0.8, player.pos.z + 34);
  const targetFov = 70 + speedFrac * 16 + (state.boosting ? 8 : 0);
  camera.fov = damp(camera.fov, targetFov, 5, dt);
  camera.updateProjectionMatrix();

  // Speed streaks
  lineMat.opacity = clamp((state.speed - 42) / 70, 0, 0.4) * (state.boosting ? 1.5 : 1);
  for (let i = 0; i < LINE_N; i++) {
    const a = (i * 97.31) % 6.283;
    const rad = 10 + ((i * 57.31) % 26);
    const zOff = ((i * 131.7 + t * state.speed * 2.2) % 70) - 35;
    const x = player.pos.x + Math.cos(a) * rad;
    const y = player.pos.y + Math.sin(a) * rad;
    const len = 3 + state.speed * 0.09;
    linePos[i * 6] = x; linePos[i * 6 + 1] = y; linePos[i * 6 + 2] = player.pos.z + zOff;
    linePos[i * 6 + 3] = x; linePos[i * 6 + 4] = y; linePos[i * 6 + 5] = player.pos.z + zOff - len;
  }
  lineGeo.attributes.position.needsUpdate = true;

  // Dust
  for (let i = 0; i < DUST_N; i++) {
    dustPos[i * 3 + 2] -= state.speed * 1.5 * dt;
    if (dustPos[i * 3 + 2] < player.pos.z - 55) {
      const a = Math.random() * 6.283, rad = 14 + Math.random() * 22;
      dustPos[i * 3] = player.pos.x + Math.cos(a) * rad;
      dustPos[i * 3 + 1] = player.pos.y + Math.sin(a) * rad * 0.7;
      dustPos[i * 3 + 2] = player.pos.z + 45 + Math.random() * 20;
    }
  }
  dustGeo.attributes.position.needsUpdate = true;

  // Post FX intensity
  afterimage.uniforms.damp.value = clamp(0.72 + speedFrac * 0.15 + (state.boosting ? 0.07 : 0), 0, 0.93);
  bloom.strength = 0.55 + speedFrac * 0.35 + (state.boosting ? 0.35 : 0);

  sfx.engine(clamp(state.speed / CFG.boostSpeed, 0, 1), state.boosting);
  state.shake = Math.max(0, state.shake - dt * 1.8);
  updateOrbSpin(dt);
  updateExplosion(dt);
  syncChunks();
  updateHUD();
}

function updateOrbSpin(dt) {
  const t = state.time;
  for (const c of chunks.values()) {
    for (const o of c.orbs) {
      if (o.taken) continue;
      o.mesh.rotation.y += dt * 2.2;
      o.mesh.position.y = o.baseY + Math.sin(t * 2 + o.phase) * 0.5;
    }
    for (const m of c.monos) m.mesh.rotation.y += m.spin * dt;
  }
}

function updateExplosion(dt) {
  let any = false;
  for (let i = 0; i < EXP_N; i++) {
    if (expLife[i] > 0) {
      any = true;
      expLife[i] -= dt;
      expPos[i * 3] += expVel[i * 3] * dt;
      expPos[i * 3 + 1] += expVel[i * 3 + 1] * dt;
      expPos[i * 3 + 2] += expVel[i * 3 + 2] * dt;
      const dr = 1 - 1.5 * dt;
      expVel[i * 3] *= dr; expVel[i * 3 + 1] *= dr; expVel[i * 3 + 2] *= dr;
      if (expLife[i] <= 0) expPos[i * 3 + 1] = -9999;
    }
  }
  expGeo.attributes.position.needsUpdate = true;
  if (ring.visible) {
    ringLife += dt;
    const sc = 1 + ringLife * 90;
    ring.scale.set(sc, sc, sc);
    ring.material.opacity = Math.max(0, 0.8 - ringLife);
    ring.lookAt(camera.position);
    if (ringLife > 0.9) ring.visible = false;
  }
}

function updateHUD() {
  const playing = state.mode === 'playing';
  document.getElementById('hud').style.opacity = playing ? 1 : 0.35;
  elMult.textContent = '\u00d7' + state.mult.toFixed(1);
  elScore.textContent = Math.floor(state.score).toLocaleString();
  elSpeedFill.style.width = (state.speed / CFG.boostSpeed * 100).toFixed(1) + '%';
  elTurboFill.style.width = state.turbo.toFixed(1) + '%';
  elSpeedText.textContent = Math.round(state.speed * 3.4);
  const e = state.energy;
  elEnergyArc.style.strokeDashoffset = (ARCL * (1 - e / 100)).toFixed(1);
  elEnergyText.textContent = Math.round(e);
  elEnergyWrap.classList.toggle('low', e < 30 && playing);
  const ready = state.turbo >= 100 && !state.boosting;
  elBoostReady.style.opacity = ready ? 1 : 0;
  elBoostBtn.classList.toggle('ready', ready);
  elVignette.style.opacity = playing && e < 30 ? ((0.5 + 0.5 * Math.sin(state.time * 6)) * (1 - e / 30)).toFixed(2) : 0;
}

// ================= Main loop =================
let last = performance.now();
function loop(now) {
  requestAnimationFrame(loop);
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;
  update(dt);
  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);
});

// Init: build initial chunks, start menu
player.pos.set(0, pathY(0), 0);
syncChunks();
requestAnimationFrame(loop);
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"full","sessionId":"7ee28c59-dcb0-40e1-a4fd-0a0c9abd7156","tokensIn":4233021,"tokensOut":123062,"tokensTotal":4356083,"cost":0,"turns":50,"toolCalls":56,"failedToolCalls":2,"timestamp":"2026-09-01T20:51:17.690Z"} -->