← Canyon Run results

Canyon Run

Swift Qwen3.8 27B GGUF · 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 41,026 bytes · SHA-256 9bc9b8962fdb
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SUNDRIFT — High-Speed Procedural Canyon Racer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&display=swap" rel="stylesheet">
<style>
  :root {
    --orange: #ff9a3c; --orange-soft: #ffd9a8; --purple: #7a5aa8; --silver: #c8cfdd;
    --bg: #0a0514;
  }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { height: 100%; overflow: hidden; background: var(--bg);
    font-family: 'Rajdhani', 'Segoe UI', sans-serif; color: #e8ecf5; user-select: none; -webkit-user-select: none; }
  canvas { position: fixed; inset: 0; display: block; z-index: 0; }
  #speedfx { position: fixed; inset: 0; z-index: 1; pointer-events: none; opacity: 0;
    background: radial-gradient(ellipse at center, transparent 42%, rgba(255,110,50,0.30) 100%); }

  /* ============ HUD ============ */
  #hud { position: fixed; inset: 0; z-index: 2; pointer-events: none;
    display: flex; flex-direction: column; justify-content: space-between; padding: 26px 34px; }
  .hidden { display: none !important; }
  .label { font-size: 11px; font-weight: 600; letter-spacing: 4px; color: rgba(232,236,245,0.45); }
  .hud-top { display: flex; justify-content: space-between; align-items: flex-start; }
  #score { font-size: 34px; font-weight: 700; letter-spacing: 2px; line-height: 1.05;
    text-shadow: 0 0 18px rgba(255,154,60,0.35); }
  #mult-wrap { text-align: right; }
  #mult { font-size: 74px; font-weight: 700; line-height: 1; color: var(--orange);
    text-shadow: 0 0 26px rgba(255,138,42,0.55); display: inline-block; }
  #mult.pulse { animation: multPulse 0.35s ease-out; }
  @keyframes multPulse { 0% { transform: scale(1); } 35% { transform: scale(1.3); color: #fff; } 100% { transform: scale(1); } }
  #nearmiss { position: fixed; top: 26%; left: 50%; transform: translate(-50%, 0); z-index: 2;
    pointer-events: none; font-size: 20px; font-weight: 700; letter-spacing: 10px;
    color: var(--orange-soft); opacity: 0; text-shadow: 0 0 20px rgba(255,154,60,0.8); }
  #nearmiss.flash { animation: nmFlash 0.7s ease-out; }
  @keyframes nmFlash {
    0% { opacity: 0; transform: translate(-50%, 6px) scale(0.92); }
    18% { opacity: 1; transform: translate(-50%, 0) scale(1.04); }
    75% { opacity: 0.85; }
    100% { opacity: 0; transform: translate(-50%, -8px) scale(1.08); } }
  .hud-bottom { display: flex; justify-content: space-between; align-items: flex-end; gap: 40px; }
  #speed-wrap { display: flex; flex-direction: column; gap: 6px; }
  #speedbar { width: 190px; height: 2px; background: rgba(232,236,245,0.18); }
  #speedfill { height: 100%; width: 0%; background: var(--orange); box-shadow: 0 0 10px rgba(255,154,60,0.8); }
  #speedval { font-size: 24px; font-weight: 700; letter-spacing: 1px; }
  #speedval small { font-size: 11px; letter-spacing: 3px; color: rgba(232,236,245,0.45); font-weight: 600; }
  #bars { display: flex; flex-direction: column; gap: 12px; align-items: flex-end; }
  .bar-row { display: flex; flex-direction: column; gap: 5px; align-items: flex-end; }
  #energybar { width: 280px; height: 7px; background: rgba(232,236,245,0.12); }
  #energyfill { height: 100%; width: 100%; background: linear-gradient(90deg, #ff6a2a, #ffd9a8);
    box-shadow: 0 0 14px rgba(255,154,60,0.7); }
  #energybar.low #energyfill { background: linear-gradient(90deg, #ff2d20, #ff7a3c); animation: lowPulse 0.7s infinite; }
  @keyframes lowPulse { 50% { opacity: 0.45; } }
  #turbobar { width: 280px; height: 3px; background: rgba(232,236,245,0.12); }
  #turbofill { height: 100%; width: 0%; background: #a8b8e8; }
  #turbo-wrap.ready #turbofill { background: #dfe8ff; box-shadow: 0 0 12px #a8b8e8; }
  #boostkey { opacity: 0; transition: opacity 0.25s; color: #cdd8f5; }
  #turbo-wrap.ready #boostkey { opacity: 1; }

  /* ============ Overlays ============ */
  .overlay { position: fixed; inset: 0; z-index: 3; display: flex; flex-direction: column;
    align-items: center; justify-content: center; gap: 18px; text-align: center;
    background: radial-gradient(ellipse at center, rgba(10,5,20,0.25) 0%, rgba(10,5,20,0.72) 100%); }
  #menu { cursor: pointer; }
  .title { font-size: clamp(56px, 9vw, 96px); font-weight: 700; letter-spacing: 18px; padding-left: 18px;
    background: linear-gradient(180deg, #fff2e0 10%, #ff9a3c 90%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    filter: drop-shadow(0 0 28px rgba(255,138,42,0.45)); }
  .subtitle { font-size: 14px; font-weight: 600; letter-spacing: 7px; color: rgba(232,236,245,0.6); }
  .controls { display: flex; gap: 34px; margin-top: 26px; flex-wrap: wrap; justify-content: center; }
  .ctl { display: flex; flex-direction: column; gap: 8px; align-items: center; }
  kbd { font-family: inherit; font-size: 13px; font-weight: 700; letter-spacing: 2px; color: var(--orange-soft);
    border: 1px solid rgba(255,154,60,0.55); border-radius: 4px; padding: 5px 12px;
    background: rgba(255,154,60,0.08); box-shadow: 0 0 12px rgba(255,138,42,0.18); }
  .start-hint { margin-top: 34px; font-size: 15px; font-weight: 700; letter-spacing: 6px;
    color: var(--orange); animation: hintPulse 1.6s ease-in-out infinite; }
  @keyframes hintPulse { 50% { opacity: 0.35; } }
  #go-reason { font-size: 13px; font-weight: 700; letter-spacing: 8px; color: #ff6a5a; }
  .go-title { font-size: 44px; font-weight: 700; letter-spacing: 12px; padding-left: 12px;
    color: #f2f5fb; text-shadow: 0 0 24px rgba(255,154,60,0.35); }
  .stats { display: flex; flex-direction: column; gap: 10px; margin-top: 18px; min-width: 320px; }
  .stat { display: flex; justify-content: space-between; border-bottom: 1px solid rgba(232,236,245,0.15); padding-bottom: 8px; }
  .stat span { font-size: 12px; font-weight: 600; letter-spacing: 4px; color: rgba(232,236,245,0.5); }
  .stat b { font-size: 22px; font-weight: 700; letter-spacing: 1px; color: var(--orange-soft); }
  #restart { pointer-events: auto; margin-top: 30px; font-family: inherit; font-size: 16px; font-weight: 700;
    letter-spacing: 6px; color: var(--orange-soft); background: transparent; border: 1px solid var(--orange);
    padding: 13px 42px; cursor: pointer; transition: all 0.2s ease; }
  #restart:hover { background: var(--orange); color: #1a0b06; box-shadow: 0 0 30px rgba(255,138,42,0.5); }
  .hint { font-size: 11px; letter-spacing: 4px; color: rgba(232,236,245,0.35); }
</style>
</head>
<body>
  <div id="speedfx"></div>

  <div id="hud" class="hidden">
    <div class="hud-top">
      <div>
        <div class="label">SCORE</div>
        <div id="score">0</div>
      </div>
      <div id="mult-wrap">
        <div id="mult">×1</div>
        <div class="label" style="text-align:right">NEAR-MISS MULT</div>
      </div>
    </div>
    <div class="hud-bottom">
      <div id="speed-wrap">
        <div class="label">VELOCITY</div>
        <div id="speedbar"><div id="speedfill"></div></div>
        <div id="speedval">0 <small>KM/H</small></div>
      </div>
      <div id="bars">
        <div class="bar-row">
          <div class="label">ENERGY</div>
          <div id="energybar"><div id="energyfill"></div></div>
        </div>
        <div class="bar-row" id="turbo-wrap">
          <div class="label">TURBO&nbsp;<span id="boostkey">SPACE ▸</span></div>
          <div id="turbobar"><div id="turbofill"></div></div>
        </div>
      </div>
    </div>
  </div>

  <div id="nearmiss">NEAR MISS</div>

  <div id="menu" class="overlay">
    <div class="title">SUNDRIFT</div>
    <div class="subtitle">HIGH-SPEED PROCEDURAL CANYON RACER</div>
    <div class="controls">
      <div class="ctl"><kbd>A&nbsp;&nbsp;D</kbd><div class="label">BANK / STEER</div></div>
      <div class="ctl"><kbd>W&nbsp;&nbsp;S</kbd><div class="label">PITCH / THROTTLE</div></div>
      <div class="ctl"><kbd>SPACE</kbd><div class="label">TURBO BOOST</div></div>
    </div>
    <div class="start-hint">CLICK OR PRESS ENTER TO LAUNCH</div>
    <div class="hint">SKIM THE WALLS FOR MULTIPLIERS · COLLECT ORBS TO STAY FUELED</div>
  </div>

  <div id="gameover" class="overlay hidden">
    <div id="go-reason">WALL IMPACT</div>
    <div class="go-title">FLIGHT TERMINATED</div>
    <div class="stats">
      <div class="stat"><span>FINAL SCORE</span><b id="go-score">0</b></div>
      <div class="stat"><span>MAX SPEED</span><b id="go-speed">0</b></div>
      <div class="stat"><span>BEST MULTIPLIER</span><b id="go-mult">×1</b></div>
      <div class="stat"><span>DISTANCE</span><b id="go-dist">0</b></div>
    </div>
    <button id="restart">RESTART</button>
    <div class="hint">OR PRESS ENTER</div>
  </div>

  <script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
      "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
    }
  }
  </script>

  <script type="module">
import * as THREE from 'three';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { AfterimagePass } from 'three/addons/postprocessing/AfterimagePass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';

/* ============================== helpers ============================== */
let seed = Math.random() * 1000;
const hash1 = n => { const s = Math.sin(n * 127.1 + seed * 0.373) * 43758.5453; return s - Math.floor(s); };
const damp = (a, b, l, dt) => b + (a - b) * Math.exp(-l * dt);
const clamp = THREE.MathUtils.clamp;

/* ============================== constants ============================== */
const CHUNK_LEN = 220;
const AHEAD_CHUNKS = 7;
const PLAYER_R = 1.6;
const MAXV = 210;
const sunDir = new THREE.Vector3(0.42, 0.13, 0.89).normalize();

/* ============================== renderer / scene ============================== */
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.prepend(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x2a1440, 0.0011);

const camera = new THREE.PerspectiveCamera(68, window.innerWidth / window.innerHeight, 0.1, 2500);
camera.position.set(0, 12, -14);

scene.add(new THREE.HemisphereLight(0x7a55a5, 0x4a2438, 0.9));

const sun = new THREE.DirectionalLight(0xffa050, 2.6);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
Object.assign(sun.shadow.camera, { left: -150, right: 150, top: 150, bottom: -150, near: 20, far: 800 });
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.bias = -0.0004;
scene.add(sun, sun.target);

const fillLight = new THREE.DirectionalLight(0x9a6ab0, 0.55);
const fillDir = new THREE.Vector3(-0.55, 0.25, -0.4).normalize();
scene.add(fillLight, fillLight.target);

/* ---- cyber-sunset skydome ---- */
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide, depthWrite: false, fog: false,
  uniforms: {
    uTop: { value: new THREE.Color(0x140b2e) },
    uHorizon: { value: new THREE.Color(0xff7038) },
    uSunDir: { value: sunDir }
  },
  vertexShader: `
    varying vec3 vDir;
    void main() {
      vDir = position;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }`,
  fragmentShader: `
    varying vec3 vDir;
    uniform vec3 uTop; uniform vec3 uHorizon; uniform vec3 uSunDir;
    void main() {
      vec3 d = normalize(vDir);
      float h = clamp(d.y * 0.5 + 0.5, 0.0, 1.0);
      vec3 col = mix(uHorizon, uTop, pow(h, 0.55));
      float sd = max(dot(d, uSunDir), 0.0);
      col += vec3(1.0, 0.50, 0.22) * pow(sd, 5.0) * 0.22;
      col += vec3(1.0, 0.72, 0.42) * smoothstep(0.99975, 0.99996, sd) * 1.8;
      gl_FragColor = vec4(col, 1.0);
    }`
});
const sky = new THREE.Mesh(new THREE.SphereGeometry(1600, 24, 16), skyMat);
sky.frustumCulled = false;
scene.add(sky);

/* ---- canyon floor ---- */
const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(20000, 20000),
  new THREE.MeshStandardMaterial({ color: 0x150a24, roughness: 1, metalness: 0 })
);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -46;
ground.receiveShadow = true;
scene.add(ground);

/* ============================== player craft ============================== */
const craft = new THREE.Group();
let flame, engineMat;
{
  const bodyMat = new THREE.MeshStandardMaterial({ color: 0xd8dee9, roughness: 0.35, metalness: 0.55, flatShading: true });
  const wingMat = new THREE.MeshStandardMaterial({ color: 0x9aa2b8, roughness: 0.5, metalness: 0.4, flatShading: true });
  const accentMat = new THREE.MeshStandardMaterial({ color: 0x2a1a3e, emissive: 0xff6a2a, emissiveIntensity: 1.4, roughness: 0.6, flatShading: true });
  engineMat = new THREE.MeshStandardMaterial({ color: 0x110805, emissive: 0xffa040, emissiveIntensity: 2.2, roughness: 0.5 });

  const nose = new THREE.Mesh(new THREE.ConeGeometry(0.9, 3.4, 5), bodyMat);
  nose.rotation.x = Math.PI / 2; nose.position.z = 1.9;
  const body = new THREE.Mesh(new THREE.BoxGeometry(1.3, 0.75, 3.4), bodyMat);
  body.position.z = -0.2;
  const wing = new THREE.Mesh(new THREE.BoxGeometry(4.8, 0.14, 1.5), wingMat);
  wing.position.set(0, -0.15, -0.9);
  const tipL = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.22, 0.9), accentMat);
  tipL.position.set(-2.3, -0.15, -0.9);
  const tipR = tipL.clone(); tipR.position.x = 2.3;
  const fin = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.95, 1.3), wingMat);
  fin.position.set(0, 0.6, -1.1);
  const canopy = new THREE.Mesh(new THREE.OctahedronGeometry(0.42), accentMat);
  canopy.position.set(0, 0.52, 0.7);
  flame = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1.6, 5), engineMat);
  flame.rotation.x = -Math.PI / 2; flame.position.set(0, -0.05, -2.7);

  craft.add(nose, body, wing, tipL, tipR, fin, canopy, flame);
  craft.traverse(o => { if (o.isMesh) o.castShadow = true; });
  scene.add(craft);
}

/* ============================== particles ============================== */
const P_MAX = 900;
const pGeo = new THREE.BufferGeometry();
pGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(P_MAX * 3), 3));
pGeo.setAttribute('aColor', new THREE.BufferAttribute(new Float32Array(P_MAX * 3), 3));
pGeo.setAttribute('aSize', new THREE.BufferAttribute(new Float32Array(P_MAX), 1));
pGeo.setAttribute('aAlpha', new THREE.BufferAttribute(new Float32Array(P_MAX), 1));
const pPos = pGeo.attributes.position.array;
const pCol = pGeo.attributes.aColor.array;
const pSize = pGeo.attributes.aSize.array;
const pAlpha = pGeo.attributes.aAlpha.array;
for (let i = 0; i < P_MAX; i++) pPos[i * 3 + 1] = -99999;
const pMat = new THREE.ShaderMaterial({
  transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
  vertexShader: `
    attribute vec3 aColor; attribute float aSize; attribute float aAlpha;
    varying vec3 vColor; varying float vAlpha;
    void main() {
      vColor = aColor; vAlpha = aAlpha;
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      gl_PointSize = aSize * (160.0 / -mv.z);
      gl_Position = projectionMatrix * mv;
    }`,
  fragmentShader: `
    varying vec3 vColor; varying float vAlpha;
    void main() {
      float d = length(gl_PointCoord - 0.5);
      float a = smoothstep(0.5, 0.06, d) * vAlpha;
      if (a < 0.01) discard;
      gl_FragColor = vec4(vColor, a);
    }`
});
const points = new THREE.Points(pGeo, pMat);
points.frustumCulled = false;
scene.add(points);
const P = new Array(P_MAX).fill(null);
function spawnP(x, y, z, vx, vy, vz, life, size, r, g, b, drag, grav) {
  for (let i = 0; i < P_MAX; i++) {
    if (!P[i]) {
      P[i] = { x, y, z, vx, vy, vz, life, maxLife: life, size, drag, grav };
      pCol[i * 3] = r; pCol[i * 3 + 1] = g; pCol[i * 3 + 2] = b;
      return;
    }
  }
}
function updateParticles(dt) {
  for (let i = 0; i < P_MAX; i++) {
    const p = P[i];
    if (!p) continue;
    p.life -= dt;
    if (p.life <= 0) { P[i] = null; pAlpha[i] = 0; pPos[i * 3 + 1] = -99999; continue; }
    const dr = Math.exp(-p.drag * dt);
    p.vx *= dr; p.vz *= dr; p.vy = p.vy * dr - p.grav * dt;
    p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt;
    const lt = p.life / p.maxLife;
    pPos[i * 3] = p.x; pPos[i * 3 + 1] = p.y; pPos[i * 3 + 2] = p.z;
    pAlpha[i] = lt * lt;
    pSize[i] = p.size * (0.4 + 0.6 * lt);
  }
  pGeo.attributes.position.needsUpdate = true;
  pGeo.attributes.aAlpha.needsUpdate = true;
  pGeo.attributes.aSize.needsUpdate = true;
  pGeo.attributes.aColor.needsUpdate = true;
}

/* ============================== procedural chunks ============================== */
const wallMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.95, metalness: 0.05, flatShading: true, vertexColors: true });
const monolithMat = new THREE.MeshStandardMaterial({ color: 0x6a5a8a, roughness: 0.8, metalness: 0.15, flatShading: true });
const edgeMat = new THREE.LineBasicMaterial({ color: 0xff7a3c, transparent: true, opacity: 0.55 });
const orbMat = new THREE.MeshStandardMaterial({ color: 0xffc060, emissive: 0xffa030, emissiveIntensity: 2.6, roughness: 0.4 });

const chunks = [];
let chunkIdx = 0;
let lastSqueezeZ = -9999;
let squeezeZ = -1, squeezeOffset = 0;

function pathX(z) {
  const a = 0.45 + 0.55 * clamp(z / 9000, 0, 1);
  return (Math.sin(z * 0.004) * 78 + Math.sin(z * 0.0013 + 2.0) * 46) * a;
}
function wallHalf(z, side) {
  let half = 62 + 20 * Math.sin(z * 0.005 + side * 2.5) + 14 * Math.sin(z * 0.011 + side * 1.7);
  if (squeezeZ > 0) {
    const dz = Math.abs(z - squeezeZ);
    if (dz < 90) half *= 0.15 + 0.85 * Math.pow(dz / 90, 1.5);
  }
  return Math.max(7, half);
}
function squeezeShift(z) {
  if (squeezeZ < 0) return 0;
  const dz = z - squeezeZ;
  if (Math.abs(dz) > 260) return 0;
  return squeezeOffset * (1 - Math.abs(dz) / 260);
}
function wallTopY(z, side) { return 26 + 10 * Math.sin(z * 0.007 + side * 3.1); }

function buildWallSide(side, z0, colliders) {
  const N = 40;
  const pos = [], col = [], idx = [];
  const c1 = new THREE.Color(0x8a4a52), c2 = new THREE.Color(0x6a4a80), cTop = new THREE.Color(0xff9a60);
  for (let i = 0; i <= N; i++) {
    const z = z0 + (i / N) * CHUNK_LEN;
    const x = pathX(z) + squeezeShift(z) + (side > 0 ? wallHalf(z, side) : -wallHalf(z, side));
    const topY = wallTopY(z, side);
    pos.push(x, -46, z, x, topY * 0.55, z, x, topY, z);
    col.push(c1.r, c1.g, c1.b, c2.r, c2.g, c2.b, cTop.r, cTop.g, cTop.b);
  }
  for (let i = 0; i < N; i++) {
    const a = i * 3, b = (i + 1) * 3;
    idx.push(a, b, a + 1, a + 1, b, b + 1, a + 1, b + 1, a + 2, a + 2, b + 1, b + 2);
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
  g.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
  g.setIndex(idx);
  g.computeVertexNormals();
  const mesh = new THREE.Mesh(g, wallMat);
  mesh.castShadow = mesh.receiveShadow = true;
  for (let i = 0; i < N; i++) {
    const z = z0 + (i + 0.5) / N * CHUNK_LEN;
    const cx = pathX(z) + squeezeShift(z);
    const half = wallHalf(z, side);
    const topY = wallTopY(z, side);
    const innerX = side > 0 ? cx + half : cx - half;
    const outerX = side > 0 ? innerX + 60 : innerX - 60;
    colliders.push({ type: 'box', minx: Math.min(innerX, outerX), maxx: Math.max(innerX, outerX), miny: -46, maxy: topY, minz: z - CHUNK_LEN / (N * 2), maxz: z + CHUNK_LEN / (N * 2) });
  }
  return mesh;
}

function createChunk(idx) {
  const z0 = idx * CHUNK_LEN;
  const group = new THREE.Group();
  const colliders = [];
  const diff = clamp((z0 - 700) / 9000, 0, 1);
  let sqZ = -1;
  if (z0 > 1000 && hash1(idx * 7.31) < 0.45 + 0.4 * diff && z0 - lastSqueezeZ > 750) {
    sqZ = z0 + 80 + hash1(idx * 3.17) * (CHUNK_LEN - 160);
    lastSqueezeZ = sqZ;
    squeezeZ = sqZ;
    squeezeOffset = (hash1(sqZ * 0.77) - 0.5) * 56;
  }
  group.add(buildWallSide(1, z0, colliders), buildWallSide(-1, z0, colliders));
  const nMon = Math.min(4, Math.floor(hash1(idx * 5.77) * (1 + 2.6 * diff)));
  for (let m = 0; m < nMon; m++) {
    const mz = z0 + 70 + hash1(idx * 11.3 + m * 4.9) * (CHUNK_LEN - 140);
    if (sqZ > 0 && Math.abs(mz - sqZ) < 130) continue;
    const cx = pathX(mz) + squeezeShift(mz);
    const half = wallHalf(mz, 1);
    const mx = cx + (hash1(idx * 17.9 + m * 7.1) - 0.5) * half * 1.1;
    const r = 6 + 9 * hash1(idx * 23.7 + m * 3.3);
    const h = 34 + 44 * hash1(idx * 29.3 + m * 5.9);
    const faces = 5 + (hash1(idx + m * 2.1) > 0.5 ? 1 : 0);
    const geo = new THREE.CylinderGeometry(r * 0.78, r, h, faces, 1);
    const mesh = new THREE.Mesh(geo, monolithMat);
    mesh.position.set(mx, h / 2 - 4, mz);
    mesh.rotation.y = hash1(idx * 31.7 + m) * Math.PI;
    mesh.castShadow = mesh.receiveShadow = true;
    group.add(mesh);
    const edges = new THREE.LineSegments(new THREE.EdgesGeometry(geo, 20), edgeMat);
    edges.position.copy(mesh.position);
    edges.rotation.copy(mesh.rotation);
    group.add(edges);
    colliders.push({ type: 'sph', x: mx, y: h / 2 - 4, z: mz, r: r * 0.85 });
  }
  scene.add(group);
  const c = { z0, group, colliders, idx, sqZ };
  chunks.push(c);
  spawnOrbsForChunk(c, diff);
  return c;
}
function disposeChunk(c) {
  scene.remove(c.group);
  c.group.traverse(o => { if (o.isMesh || o.isLineSegments) o.geometry.dispose(); });
}

/* ============================== orbs ============================== */
const orbGeo = new THREE.IcosahedronGeometry(1.4, 0);
const orbPool = [];
const orbs = [];
for (let i = 0; i < 48; i++) {
  const m = new THREE.Mesh(orbGeo, orbMat);
  m.visible = false;
  m.userData.active = false;
  scene.add(m);
  orbPool.push(m);
}
function spawnOrb(x, y, z) {
  const m = orbPool.find(o => !o.userData.active);
  if (!m) return;
  m.userData.active = true;
  m.visible = true;
  m.position.set(x, y, z);
  orbs.push({ m, y0: y, phase: Math.random() * 6.28 });
}
function spawnOrbsForChunk(c, diff) {
  const n = 5 + Math.floor(hash1(c.idx * 9.23) * 4);
  for (let i = 0; i < n; i++) {
    const z = c.z0 + 30 + (i / n) * (CHUNK_LEN - 60);
    const cx = pathX(z) + squeezeShift(z);
    const half = wallHalf(z, 1);
    const off = (hash1(c.idx * 13.7 + i * 3.31) - 0.5) * Math.min(half * 1.15, 74);
    spawnOrb(cx + off, 3 + hash1(c.idx * 4.9 + i * 7.7) * 15, z);
  }
  if (c.sqZ > 0) {
    const cx = pathX(c.sqZ) + squeezeShift(c.sqZ);
    spawnOrb(cx, 6, c.sqZ - 26);
    spawnOrb(cx, 7, c.sqZ);
    spawnOrb(cx, 6, c.sqZ + 26);
  }
}
/* ============================== audio ============================== */
const AudioSys = {
  ctx: null, master: null, engine: null,
  init() {
    if (this.ctx) { if (this.ctx.state === 'suspended') this.ctx.resume(); return; }
    const C = window.AudioContext || window.webkitAudioContext;
    this.ctx = new C();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.5;
    this.master.connect(this.ctx.destination);
    const len = this.ctx.sampleRate * 2;
    const buf = this.ctx.createBuffer(1, len, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
    this.noise = buf;
    // engine: two detuned oscillators + lowpass
    const o1 = this.ctx.createOscillator(); o1.type = 'sawtooth'; o1.frequency.value = 48;
    const o2 = this.ctx.createOscillator(); o2.type = 'sawtooth'; o2.frequency.value = 48.7;
    const lp = this.ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 240; lp.Q.value = 4;
    const g = this.ctx.createGain(); g.gain.value = 0;
    o1.connect(lp); o2.connect(lp); lp.connect(g); g.connect(this.master);
    o1.start(); o2.start();
    this.engine = { o1, o2, lp, g };
  },
  update(speed, boost) {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const f = 42 + speed * 1.15 + (boost ? 40 : 0);
    this.engine.o1.frequency.setTargetAtTime(f, t, 0.08);
    this.engine.o2.frequency.setTargetAtTime(f * 1.013, t, 0.08);
    this.engine.lp.frequency.setTargetAtTime(200 + speed * 9 + (boost ? 900 : 0), t, 0.1);
    this.engine.g.gain.setTargetAtTime(0.05 + speed / MAXV * 0.1 + (boost ? 0.05 : 0), t, 0.15);
  },
  ping() {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const o = this.ctx.createOscillator(); o.type = 'sine';
    o.frequency.setValueAtTime(880, t);
    o.frequency.exponentialRampToValueAtTime(1760, t + 0.09);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(0.22, t + 0.015);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.35);
    o.connect(g); g.connect(this.master);
    o.start(t); o.stop(t + 0.4);
  },
  whoosh(intensity) {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const src = this.ctx.createBufferSource(); src.buffer = this.noise;
    const bp = this.ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = 1.2;
    bp.frequency.setValueAtTime(300, t);
    bp.frequency.exponentialRampToValueAtTime(2400, t + 0.18);
    bp.frequency.exponentialRampToValueAtTime(500, t + 0.45);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(0.3 * intensity, t + 0.06);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 0.5);
    src.connect(bp); bp.connect(g); g.connect(this.master);
    src.start(t); src.stop(t + 0.55);
  },
  crash() {
    if (!this.ctx) return;
    const t = this.ctx.currentTime;
    const src = this.ctx.createBufferSource(); src.buffer = this.noise; src.loop = true;
    const lp = this.ctx.createBiquadFilter(); lp.type = 'lowpass';
    lp.frequency.setValueAtTime(3000, t);
    lp.frequency.exponentialRampToValueAtTime(120, t + 0.8);
    const g = this.ctx.createGain();
    g.gain.setValueAtTime(0.5, t);
    g.gain.exponentialRampToValueAtTime(0.0001, t + 1.0);
    src.connect(lp); lp.connect(g); g.connect(this.master);
    src.start(t); src.stop(t + 1.1);
    const o = this.ctx.createOscillator(); o.type = 'sine';
    o.frequency.setValueAtTime(160, t);
    o.frequency.exponentialRampToValueAtTime(38, t + 0.7);
    const g2 = this.ctx.createGain();
    g2.gain.setValueAtTime(0.4, t);
    g2.gain.exponentialRampToValueAtTime(0.0001, t + 0.8);
    o.connect(g2); g2.connect(this.master);
    o.start(t); o.stop(t + 0.9);
  },
  stopEngine() {
    if (!this.ctx) return;
    this.engine.g.gain.setTargetAtTime(0, this.ctx.currentTime, 0.2);
  }
};

/* ============================== post-processing ============================== */
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const afterimage = new AfterimagePass(0.6);
afterimage.uniforms['damp'].value = 0.35;
composer.addPass(afterimage);
const bloom = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 0.7, 0.5, 0.35);
composer.addPass(bloom);
composer.addPass(new OutputPass());

/* ============================== game state ============================== */
const G = {
  state: 'menu',          // menu | playing | dead
  px: 0, py: 10, pz: 0,
  vx: 0, vy: 0, vz: 28,
  roll: 0, pitch: 0,
  speed: 28,
  energy: 100, turbo: 0, boosting: false, boostLeft: 0,
  score: 0, mult: 1, bestMult: 1, maxSpeed: 0,
  shake: 0, time: 0, dead: 0,
  nearCd: 0
};
const keys = {};
window.addEventListener('keydown', e => {
  keys[e.code] = true;
  if (e.code === 'Space') e.preventDefault();
  if (e.code === 'Enter') {
    if (G.state === 'menu') startGame();
    else if (G.state === 'dead' && G.dead > 0.8) startGame();
  }
});
window.addEventListener('keyup', e => { keys[e.code] = false; });

const el = id => document.getElementById(id);
window.__game = () => ({ G, startGame, chunks, orbs, pathX, wallHalf, keys });
const ui = {
  hud: el('hud'), menu: el('menu'), over: el('gameover'),
  score: el('score'), mult: el('mult'), speedfill: el('speedfill'), speedval: el('speedval'),
  energybar: el('energybar'), energyfill: el('energyfill'),
  turboWrap: el('turbo-wrap'), turbofill: el('turbofill'),
  nearmiss: el('nearmiss'), speedfx: el('speedfx'),
  goScore: el('go-score'), goSpeed: el('go-speed'), goMult: el('go-mult'), goDist: el('go-dist'), goReason: el('go-reason'),
  restart: el('restart')
};
ui.menu.addEventListener('click', () => { if (G.state === 'menu') startGame(); });
ui.restart.addEventListener('click', () => { if (G.state === 'dead' && G.dead > 0.5) startGame(); });

function startGame() {
  AudioSys.init();
  // reset world
  for (const c of chunks) disposeChunk(c);
  chunks.length = 0;
  for (const o of orbs) { o.m.userData.active = false; o.m.visible = false; }
  orbs.length = 0;
  chunkIdx = 0; lastSqueezeZ = -9999; squeezeZ = -1; squeezeOffset = 0;
  seed = Math.random() * 1000;
  Object.assign(G, {
    state: 'playing', px: pathX(0), py: 10, pz: 0, vx: 0, vy: 0, vz: 28,
    roll: 0, pitch: 0, speed: 28, energy: 100, turbo: 0, boosting: false, boostLeft: 0,
    score: 0, mult: 1, bestMult: 1, maxSpeed: 28, shake: 0, time: 0, dead: 0, nearCd: 0
  });
  for (let i = 0; i < AHEAD_CHUNKS + 1; i++) createChunk(chunkIdx++);
  ui.menu.classList.add('hidden');
  ui.over.classList.add('hidden');
  ui.hud.classList.remove('hidden');
}
function gameOver(reason) {
  if (G.state !== 'playing') return;
  G.state = 'dead'; G.dead = 0;
  AudioSys.crash(); AudioSys.stopEngine();
  G.shake = 1.6;
  ui.hud.classList.add('hidden');
  // explosion burst
  for (let i = 0; i < 70; i++) {
    const a = Math.random() * 6.28, b = Math.random() * 6.28, s = 8 + Math.random() * 30;
    spawnP(G.px, G.py, G.pz, Math.cos(a) * Math.cos(b) * s, Math.sin(b) * s, Math.sin(a) * Math.cos(b) * s,
      0.6 + Math.random() * 0.8, 1.5 + Math.random() * 2, 1, 0.45 + Math.random() * 0.5, 0.15, 1.5, 6);
  }
  craft.visible = false;
  ui.speedfx.style.opacity = 0;
  ui.goReason.textContent = reason;
  ui.goScore.textContent = Math.floor(G.score).toLocaleString();
  ui.goSpeed.textContent = Math.round(G.maxSpeed * 3.6) + ' KM/H';
  ui.goMult.textContent = '×' + G.bestMult.toFixed(1);
  ui.goDist.textContent = Math.round(G.pz / 100).toLocaleString() + ' KM';
  ui.over.classList.remove('hidden');
}

/* ============================== physics & collisions ============================== */
function stepPhysics(dt) {
  const diff = clamp(G.pz / 12000, 0, 1);
  G.speed = damp(G.speed, 58 + 88 * diff, 0.22, dt);
  if (keys['KeyS']) G.speed = damp(G.speed, G.speed * 0.9, 0.3, dt); // throttle down
  if (keys['KeyW'] && G.energy > 0) G.speed = damp(G.speed, Math.min(MAXV, G.speed + 18), 0.5, dt);

  // turbo
  const wantBoost = keys['Space'] && G.turbo >= 100 && G.boostLeft <= 0 && G.energy > 0;
  if (wantBoost) { G.boosting = true; G.boostLeft = 2.6; }
  if (G.boosting) {
    G.boostLeft -= dt;
    G.turbo = Math.max(0, G.turbo - 40 * dt);
    G.energy = Math.max(0, G.energy - 10 * dt);
    G.speed = damp(G.speed, MAXV * 1.28, 1.4, dt);
    if (G.boostLeft <= 0) G.boosting = false;
  } else {
    if (G.speed > 120) G.turbo = Math.min(100, G.turbo + (G.speed - 110) * 0.09 * dt * 10);
    G.turbo = Math.max(0, G.turbo - 1.2 * dt);
  }
  G.speed = Math.min(G.speed, MAXV * 1.3);

  // flight controls: lateral / vertical drift, damped
  const steer = (keys['KeyD'] ? 1 : 0) - (keys['KeyA'] ? 1 : 0);
  const pitchIn = (keys['KeyW'] ? 1 : 0) - (keys['KeyS'] ? 1 : 0);
  G.vx = damp(G.vx, steer * G.speed * 0.62, 2.4, dt);
  G.vy = damp(G.vy, pitchIn * G.speed * 0.5, 2.4, dt);
  G.px += G.vx * dt;
  G.py += G.vy * dt;
  G.pz += G.speed * dt;
  G.py = clamp(G.py, 2.2, 52);

  // banking
  G.roll = damp(G.roll, -steer * 0.85, 5, dt);
  G.pitch = damp(G.pitch, -pitchIn * 0.45, 5, dt);

  // energy decay (faster at speed & while boosting)
  G.energy = Math.max(0, G.energy - (1.5 + G.speed / MAXV * 1.6) * dt);
  if (G.energy <= 0) { gameOver('OUT OF ENERGY'); return; }

  G.maxSpeed = Math.max(G.maxSpeed, G.speed);
  G.score += G.speed * dt * 0.5 * G.mult;

  // collisions & near-miss
  G.nearCd -= dt;
  let minDist = 1e9;
  for (const c of chunks) {
    if (c.z0 > G.pz + 80 || c.z0 + CHUNK_LEN < G.pz - 80) continue;
    for (const col of c.colliders) {
      let d;
      if (col.type === 'box') {
        const dx = G.px - clamp(G.px, col.minx, col.maxx);
        const dy = G.py - clamp(G.py, col.miny, col.maxy);
        const dz = G.pz - clamp(G.pz, col.minz, col.maxz);
        d = Math.sqrt(dx * dx + dy * dy + dz * dz);
      } else {
        const dx = G.px - col.x, dy = G.py - col.y, dz = G.pz - col.z;
        d = Math.sqrt(dx * dx + dy * dy + dz * dz) - col.r;
      }
      if (d < PLAYER_R) { gameOver('WALL IMPACT'); return; }
      if (d < minDist) minDist = d;
    }
  }
  // monolith Z-range check (spheres are infinite cylinders in XZ; restrict to their height band)
  if (G.state !== 'playing') return;
  const near = minDist - PLAYER_R;
  if (near < 5 && G.nearCd <= 0) {
    G.nearCd = 0.35;
    const closeness = clamp(1 - near / 5, 0, 1);
    const gained = 0.25 + closeness * 0.9;
    G.mult = Math.min(20, G.mult + gained);
    G.bestMult = Math.max(G.bestMult, G.mult);
    G.shake = Math.max(G.shake, 0.35 + closeness * 0.5);
    AudioSys.whoosh(0.4 + closeness * 0.6);
    ui.mult.classList.remove('pulse'); void ui.mult.offsetWidth; ui.mult.classList.add('pulse');
    if (closeness > 0.55) {
      ui.nearmiss.classList.remove('flash'); void ui.nearmiss.offsetWidth; ui.nearmiss.classList.add('flash');
    }
  }
  // multiplier decays
  G.mult = Math.max(1, G.mult - 0.35 * dt);

  // orbs
  for (let i = orbs.length - 1; i >= 0; i--) {
    const o = orbs[i];
    const dx = G.px - o.m.position.x, dy = G.py - o.m.position.y, dz = G.pz - o.m.position.z;
    if (dx * dx + dy * dy + dz * dz < 26) {
      o.m.userData.active = false; o.m.visible = false;
      orbs.splice(i, 1);
      G.energy = Math.min(100, G.energy + 24);
      G.turbo = Math.min(100, G.turbo + 14);
      G.score += 150 * G.mult;
      AudioSys.ping();
      for (let k = 0; k < 14; k++) {
        const a = Math.random() * 6.28, s = 4 + Math.random() * 10;
        spawnP(o.m.position.x, o.m.position.y, o.m.position.z,
          Math.cos(a) * s, Math.sin(a) * s, 0, 0.4 + Math.random() * 0.4, 1.2, 1, 0.75, 0.3, 2, 0);
      }
    } else if (o.m.position.z < G.pz - 30) {
      o.m.userData.active = false; o.m.visible = false;
      orbs.splice(i, 1);
    }
  }
}

/* ============================== camera & effects ============================== */
const camPos = new THREE.Vector3(0, 12, -14);
function updateCamera(dt) {
  const target = new THREE.Vector3(
    G.px + G.vx * 0.06,
    G.py + 2.9 + G.vy * 0.05,
    G.pz - 11.5
  );
  camPos.x = damp(camPos.x, target.x, 6, dt);
  camPos.y = damp(camPos.y, target.y, 6, dt);
  camPos.z = damp(camPos.z, target.z, 10, dt);
  G.shake = Math.max(0, G.shake - dt * 2.2);
  const sh = G.shake * (G.boosting ? 0.5 : 1);
  camera.position.set(
    camPos.x + (Math.random() - 0.5) * sh * 0.7,
    camPos.y + (Math.random() - 0.5) * sh * 0.7,
    camPos.z + (Math.random() - 0.5) * sh * 0.4
  );
  camera.lookAt(G.px + G.vx * 0.15, G.py + G.vy * 0.12 + 0.5, G.pz + 12);
  const fovT = 68 + (G.speed / MAXV) * 16 + (G.boosting ? 14 : 0);
  camera.fov = damp(camera.fov, fovT, 4, dt);
  camera.updateProjectionMatrix();
  // sky + sun follow
  sky.position.set(G.px, 0, G.pz);
  sun.position.set(G.px + sunDir.x * 260, G.py + sunDir.y * 260, G.pz + sunDir.z * 260);
  sun.target.position.set(G.px, 0, G.pz);
  fillLight.position.set(G.px + fillDir.x * 200, G.py + fillDir.y * 200, G.pz + fillDir.z * 200);
  fillLight.target.position.set(G.px, 0, G.pz);
  ground.position.x = G.px; ground.position.z = G.pz;
  // speed FX
  const sp = clamp((G.speed - 100) / (MAXV * 1.3 - 100), 0, 1);
  ui.speedfx.style.opacity = (sp * 0.55 + (G.boosting ? 0.3 : 0)).toFixed(2);
}

function updateTrail(dt) {
  const n = Math.floor(G.speed * dt * 1.6) + 1;
  for (let i = 0; i < n; i++) {
    const t = Math.random();
    spawnP(
      G.px - Math.sin(G.roll) * 0.8 + (Math.random() - 0.5) * 0.7,
      G.py - 0.1 + (Math.random() - 0.5) * 0.5,
      G.pz - 2.6 - t * 0.8,
      -G.vx * 0.05 + (Math.random() - 0.5) * 2,
      (Math.random() - 0.5) * 2,
      -G.speed * 0.35 - Math.random() * 6,
      0.25 + Math.random() * 0.3,
      0.9 + Math.random() * 0.8,
      1, 0.55 + Math.random() * 0.3, 0.15, 3, 0
    );
  }
  // speed dust streaks
  if (G.speed > 90) {
    const k = Math.floor((G.speed - 90) / 40);
    for (let i = 0; i < k; i++) {
      const a = Math.random() * 6.28, r = 6 + Math.random() * 10;
      spawnP(
        G.px + Math.cos(a) * r, G.py + Math.sin(a) * r * 0.6, G.pz + 2,
        0, 0, -G.speed * 1.5,
        0.3, 0.5 + Math.random() * 0.6,
        0.75, 0.7, 0.85, 0, 0
      );
    }
  }
}

/* ============================== HUD ============================== */
function updateHUD() {
  ui.score.textContent = Math.floor(G.score).toLocaleString();
  ui.mult.textContent = '×' + G.mult.toFixed(1);
  const kmh = Math.round(G.speed * 3.6);
  ui.speedval.innerHTML = kmh + ' <small>KM/H</small>';
  ui.speedfill.style.width = clamp(G.speed / (MAXV * 1.3), 0, 1) * 100 + '%';
  ui.energyfill.style.width = G.energy + '%';
  ui.energybar.classList.toggle('low', G.energy < 25);
  ui.turbofill.style.width = G.turbo + '%';
  ui.turboWrap.classList.toggle('ready', G.turbo >= 100);
}

/* ============================== chunk streaming ============================== */
function streamChunks() {
  while (chunks[chunks.length - 1].z0 + CHUNK_LEN < G.pz + AHEAD_CHUNKS * CHUNK_LEN) {
    createChunk(chunkIdx++);
  }
  while (chunks.length && chunks[0].z0 + CHUNK_LEN < G.pz - 200) {
    disposeChunk(chunks.shift());
  }
}

/* ============================== main loop ============================== */
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  G.time += dt;

  if (G.state === 'playing') {
    stepPhysics(dt);
    streamChunks();
    updateCamera(dt);
    updateTrail(dt);
    updateHUD();
    AudioSys.update(G.speed, G.boosting);
    // craft orientation
    craft.position.set(G.px, G.py, G.pz);
    craft.rotation.z = G.roll;
    craft.rotation.x = G.pitch;
    flame.scale.set(1, 1 + G.speed / MAXV * 1.6 + (G.boosting ? 1.4 : 0), 1);
    flame.scale.x = flame.scale.z = 0.8 + Math.random() * 0.3;
    engineMat.emissiveIntensity = 1.2 + G.speed / MAXV * 1.8 + (G.boosting ? 2.5 : 0);
    afterimage.uniforms['damp'].value = clamp(0.2 + G.speed / MAXV * 0.45 + (G.boosting ? 0.25 : 0), 0, 0.92);
    bloom.strength = 0.85 + (G.boosting ? 0.5 : 0);
  } else if (G.state === 'dead') {
    G.dead += dt;
    updateCamera(dt);
    G.shake = Math.max(0, G.shake - dt * 1.5);
    camera.fov = damp(camera.fov, 68, 3, dt);
    camera.updateProjectionMatrix();
    afterimage.uniforms['damp'].value = damp(afterimage.uniforms['damp'].value, 0.2, 2, dt);
    bloom.strength = damp(bloom.strength, 0.7, 2, dt);
    sky.position.set(G.px, 0, G.pz);
    sun.position.set(G.px + sunDir.x * 260, G.py + sunDir.y * 260, G.pz + sunDir.z * 260);
    sun.target.position.set(G.px, 0, G.pz);
  } else {
    // menu idle: slow drift through the canyon
    G.pz += 30 * dt;
    G.px = damp(G.px, pathX(G.pz) * 0.5, 0.8, dt);
    G.py = damp(G.py, 12 + Math.sin(G.time * 0.5) * 3, 0.8, dt);
    streamChunks();
    updateCamera(dt);
    craft.position.set(G.px, G.py, G.pz);
    craft.rotation.z = damp(craft.rotation.z, Math.sin(G.time * 0.7) * 0.3, 2, dt);
    flame.scale.set(1, 1.2, 1);
    sky.position.set(G.px, 0, G.pz);
    sun.position.set(G.px + sunDir.x * 260, 12 + sunDir.y * 260, G.pz + sunDir.z * 260);
    sun.target.position.set(G.px, 0, G.pz);
  }

  // orbs bob & spin
  for (const o of orbs) {
    o.m.rotation.y += dt * 2.2;
    o.m.position.y = o.y0 + Math.sin(G.time * 2.4 + o.phase) * 0.9;
  }
  updateParticles(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);
});

/* boot: pre-build menu world */
for (let i = 0; i < AHEAD_CHUNKS + 1; i++) createChunk(chunkIdx++);
animate();
</script>
</body>
</html>
<!-- agent-meta {"model":"ukisai/Swift-Qwen3.8-27B-GGUF","provider":"brigid","persona":"full","sessionId":"5d5b843b-51dc-40b6-a84a-89f1543b3e25","tokensIn":5932239,"tokensOut":101171,"tokensTotal":6033410,"cost":0,"turns":69,"toolCalls":79,"failedToolCalls":2,"timestamp":"2026-09-29T19:11:46.708Z"} -->