← Canyon Run results

Canyon Run

Qwen 3.8 27B GGUF · typescript

◈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 73,530 bytes · SHA-256 c6badb1be9aa
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SUNDRIFT — High-Speed Procedural Canyon Racer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&amp;display=swap">
<style>
/* ── css: base ── */
:root {
  --ink: #f6e7ff;
  --gold: #ffb86b;
  --ember: #ff6b3d;
  --violet: #b04dff;
  --ice: #8be9ff;
  --danger: #ff4d6d;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #14091f; }
body {
  font-family: "Orbitron", "Segoe UI", sans-serif;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  user-select: none;
}
#app { position: fixed; inset: 0; }
#gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#hud, #overlay, #paused { position: absolute; inset: 0; pointer-events: none; }

/* ---------- speed / letterbox bars (boost drama) ---------- */
.bar {
  position: absolute; left: 0; right: 0; height: 7vh;
  background: #0a0512;
  z-index: 5; pointer-events: none;
  transition: height 0.35s cubic-bezier(.2,.8,.2,1);
}
#bar-top { top: 0; border-bottom: 1px solid rgba(255,150,80,.15); }
#bar-bottom { bottom: 0; border-top: 1px solid rgba(255,150,80,.15); }

/* ---------- energy (bottom-left) ---------- */
#energy {
  position: absolute; left: 34px; bottom: 34px; width: 250px; height: 16px;
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,180,120,.35);
  clip-path: polygon(10px 0, 100% 0, calc(100% - 10px) 100%, 0 100%);
}
#energy-fill {
  position: absolute; inset: 0; width: 100%;
  background: linear-gradient(90deg, #ff8a3d, #ffc46b, #8be9ff);
  box-shadow: 0 0 18px rgba(255,170,90,.55);
  transform-origin: left center;
}
#energy-ticks {
  position: absolute; inset: 0;
  background-image: repeating-linear-gradient(90deg, transparent 0 24px, rgba(10,5,18,.85) 24px 26px);
}
#energy.low #energy-fill { animation: pulseLow .5s infinite alternate; }
@keyframes pulseLow { from { filter: brightness(.6); } to { filter: brightness(1.5); } }

/* ---------- turbo (bottom-right) ---------- */
#turbo-wrap { position: absolute; right: 34px; bottom: 34px; width: 250px; text-align: right; }
#turbo-label {
  font-size: 10px; letter-spacing: .3em; color: rgba(246,231,255,.55);
  display: block; margin-bottom: 6px;
}
#turbo-track {
  position: relative; height: 8px; width: 100%;
  background: rgba(255,255,255,.06);
  border: 1px solid rgba(139,233,255,.35);
  clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 100%, 10px 100%);
}
#turbo-fill {
  position: absolute; inset: 0; width: 0%;
  background: linear-gradient(90deg, #5d7cff, #8be9ff, #ffffff);
  box-shadow: 0 0 14px rgba(139,233,255,.8);
  transform-origin: left center;
}
#turbo-wrap.ready #turbo-label { color: #8be9ff; animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: .35; } }

/* ---------- score block (top-right) ---------- */
#near { position: absolute; top: 30px; right: 36px; text-align: right; }
#mult { display: flex; justify-content: flex-end; align-items: baseline; gap: 4px; }
#mult-x { font-size: 26px; font-weight: 700; color: rgba(255,184,107,.8); }
#mult-n {
  font-size: 72px; font-weight: 900; line-height: 1;
  color: #fff; text-shadow: 0 0 24px rgba(255,150,60,.8);
}
#mult.hot { color: #ffd27a; }
#mult.hot #mult-x { color: #ffd27a; }
#mult.pulse { animation: multPulse .45s ease-out; }
@keyframes multPulse {
  0% { transform: scale(1); }
  30% { transform: scale(1.28) rotate(-2deg); text-shadow: 0 0 40px rgba(255,220,150,1); }
  100% { transform: scale(1); }
}
#near-badge {
  display: block; font-size: 11px; letter-spacing: .5em; font-weight: 700;
  color: #ffd27a; opacity: 0; margin-bottom: 2px;
}
#near-badge.show { animation: badgePop .6s ease-out forwards; }
@keyframes badgePop {
  0% { opacity: 0; transform: translateY(8px) scale(1.4); }
  25% { opacity: 1; transform: translateY(0) scale(1); }
  80% { opacity: 1; }
  100% { opacity: 0; }
}
#score {
  margin-top: 6px; font-size: 20px; font-weight: 500; letter-spacing: .25em;
  color: rgba(246,231,255,.85);
}
#score::before { content: "SCORE "; font-size: 10px; letter-spacing: .4em; color: rgba(246,231,255,.4); }

/* ---------- speedometer (left edge, vertical) ---------- */
#speedo {
  position: absolute; left: 34px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; gap: 10px; height: 46vh;
}
#speedo-track {
  position: relative; width: 4px; height: 100%;
  background: rgba(255,255,255,.1);
}
#speedo-fill {
  position: absolute; left: 0; right: 0; bottom: 0; height: 20%;
  background: linear-gradient(0deg, #ff6b3d, #ffb86b 55%, #8be9ff);
  box-shadow: 0 0 12px rgba(255,150,80,.7);
}
#speedo-num { font-size: 15px; font-weight: 700; letter-spacing: .15em; color: rgba(246,231,255,.9); }
#speedo-num::after { content: " KM/H"; font-size: 8px; color: rgba(246,231,255,.4); letter-spacing: .3em; }

/* ---------- pause ---------- */
#paused { display: flex; align-items: center; justify-content: center; background: rgba(10,5,18,.45); z-index: 8; }
#paused[hidden] { display: none; }
.pa-box { text-align: center; border: 1px solid rgba(255,184,107,.4); padding: 26px 54px; background: rgba(20,9,31,.8); }
.pa-box span { font-size: 30px; font-weight: 900; letter-spacing: .5em; }
.pa-box small { display: block; margin-top: 10px; font-size: 10px; letter-spacing: .3em; color: rgba(246,231,255,.5); }

/* ---------- overlay ---------- */
#overlay {
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(ellipse at 50% 120%, rgba(120,40,90,.35), rgba(10,5,18,.82) 65%);
  z-index: 10; transition: opacity .5s;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
#panel {
  width: min(560px, 86vw); text-align: center;
  border: 1px solid rgba(255,184,107,.25);
  background: linear-gradient(180deg, rgba(24,11,38,.92), rgba(16,8,28,.92));
  padding: 46px 44px 40px;
  box-shadow: 0 0 80px rgba(255,120,60,.12), inset 0 0 60px rgba(120,60,180,.10);
}
.p-kicker { font-size: 10px; letter-spacing: .55em; color: rgba(255,184,107,.75); margin-bottom: 14px; }
.p-title {
  font-size: 64px; font-weight: 900; letter-spacing: .12em; line-height: 1;
  color: #fff; text-shadow: 0 0 34px rgba(255,140,60,.55);
}
.p-title span { color: var(--gold); }
.p-sub { margin: 16px auto 26px; max-width: 400px; font-size: 12px; line-height: 1.8; letter-spacing: .08em; color: rgba(246,231,255,.65); font-weight: 500; }
.p-keys { display: inline-block; text-align: left; margin-bottom: 28px; }
.k-row { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 8px 0; min-width: 320px; }
.k-row span { font-size: 9px; letter-spacing: .35em; color: rgba(246,231,255,.5); }
.kbd { font-family: inherit; }
kbd {
  display: inline-block; min-width: 26px; padding: 4px 8px; margin-left: 6px;
  border: 1px solid rgba(255,184,107,.4); font-size: 10px; letter-spacing: .1em;
  color: var(--gold); background: rgba(255,150,80,.06);
}
.p-btn {
  pointer-events: auto; cursor: pointer;
  font-family: inherit; font-size: 15px; font-weight: 700; letter-spacing: .4em;
  color: #1c0b22; background: linear-gradient(90deg, #ffb86b, #ff8a3d);
  border: none; padding: 16px 46px 16px 50px;
  clip-path: polygon(14px 0, 100% 0, calc(100% - 14px) 100%, 0 100%);
  box-shadow: 0 0 34px rgba(255,140,60,.45);
  transition: transform .15s, box-shadow .15s, filter .15s;
}
.p-btn em { font-style: normal; margin-left: 12px; }
.p-btn:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 0 54px rgba(255,150,60,.75); filter: brightness(1.08); }
.p-btn:active { transform: translateY(0) scale(.98); }
.p-hint { margin-top: 22px; font-size: 9px; letter-spacing: .28em; color: rgba(246,231,255,.4); line-height: 2; }

/* ---------- game over ---------- */
#overlay.death .p-kicker { color: rgba(255,77,109,.85); }
#overlay.death .p-title { text-shadow: 0 0 34px rgba(255,77,109,.5); }
.stat-grid { display: flex; justify-content: center; gap: 44px; margin: 6px 0 26px; }
.stat { text-align: center; }
.stat b { display: block; font-size: 34px; font-weight: 900; color: #fff; text-shadow: 0 0 20px rgba(255,150,60,.6); }
.stat i { display: block; font-style: normal; margin-top: 8px; font-size: 9px; letter-spacing: .35em; color: rgba(246,231,255,.45); }
#overlay.death #startBtn { background: linear-gradient(90deg, #ff6b8a, #ff6b3d); }

/* ---------- hurt / boost tints ---------- */
#tint {
  position: absolute; inset: 0; pointer-events: none; z-index: 4; opacity: 0;
  background: radial-gradient(ellipse at center, transparent 42%, rgba(255,30,50,.55) 100%);
}
#boostTint {
  position: absolute; inset: 0; pointer-events: none; z-index: 4; opacity: 0;
  background: radial-gradient(ellipse at center, rgba(139,233,255,.05) 50%, rgba(90,140,255,.18) 100%);
}
</style>
</head>
<body>
<div id="app">
  <canvas id="gl"></canvas><div id="bar-top" class="bar"></div>
<div id="bar-bottom" class="bar"></div>
<div id="tint"></div>
<div id="boostTint"></div>
  <div id="hud">
    <div id="energy" title="ENERGY"><div id="energy-fill"></div><div id="energy-ticks"></div></div>
    <div id="turbo-wrap"><div id="turbo-track"><div id="turbo-fill"></div></div><span id="turbo-label">TURBO · press SHIFT</span></div>
    <div id="near">
      <span id="near-badge">NEAR MISS</span>
      <div id="mult"><span id="mult-x">x</span><span id="mult-n">1</span></div>
      <div id="score">0000000</div>
    </div>
    <div id="speedo"><div id="speedo-fill"></div><span id="speedo-num">000</span></div>
  </div>
  <div id="overlay">
    <div id="panel">
      <div class="p-kicker">high-speed procedural canyon racer</div>
      <h1 class="p-title">SUN<span>DRIFT</span></h1>
      <p class="p-sub">Carve the endless dusk canyon. Every wall you kiss pays. Run out of energy and the sky closes in.</p>
      <div class="p-keys">
        <div class="k-row"><span>STEER · BANK</span><kbd>A D</kbd><kbd>◄ ►</kbd></div>
        <div class="k-row"><span>PITCH · BRAKE / BURN</span><kbd>S W</kbd><kbd>▼ ▲</kbd></div>
        <div class="k-row"><span>TURBO BURN</span><kbd>SHIFT</kbd></div>
        <div class="k-row"><span>RESTART</span><kbd>R</kbd></div>
      </div>
      <button id="startBtn" class="p-btn">LAUNCH <em>▶</em></button>
      <div class="p-hint">near-misses stack the multiplier · orbs refill energy · turbo builds on straightaways</div>
    </div>
  </div>
  <div id="paused" hidden=""><div class="pa-box"><span>PAUSED</span><small>ESC to resume</small></div></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts"},"src/worldgen.ts":{"./types":"src/types.ts"},"src/player.ts":{"./types":"src/types.ts"},"src/scene.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./scene":"src/scene.ts","./worldgen":"src/worldgen.ts","./player":"src/player.ts","./particles":"src/particles.ts","./audio":"src/audio.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// Entry point: boot the SUNDRIFT game.
const game_1 = require("./game");
(0, game_1.boot)();
};

// ── types: src/globals.d.ts ──
__mods["src/globals.d.ts"] = function (exports) {};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Shared types, tuning constants and palette for SUNDRIFT.
Object.defineProperty(exports, "__esModule", { value: true });
exports.smooth = exports.rand = exports.lerp = exports.clamp = exports.PAL = exports.CFG = void 0;
exports.difficultyOf = difficultyOf;
exports.maxSpeedFor = maxSpeedFor;
exports.cruiseTarget = cruiseTarget;
exports.gapHalfW = gapHalfW;
exports.wallThickness = wallThickness;
exports.wallHeight = wallHeight;
exports.monolithDensity = monolithDensity;
exports.gapYRange = gapYRange;
exports.CFG = {
    // player
    BASE_SPEED: 52,
    MAX_SPEED: 240,
    BOOST_SPEED: 340,
    SPEED_ACCEL: 26, // units/s^2 toward cruise
    PITCH_ACCEL: 55, // speed gain from pitch up
    PITCH_BRAKE: 42, // speed loss from pitch down
    BOOST_MULT: 1.45,
    BOOST_MIN_ENERGY: 18,
    BOOST_DRAIN: 26, // energy/s while boosting
    DRAG_XY: 2.2, // lateral damping factor
    MAX_LAT_SPEED: 33,
    GRAVITY: 30,
    ENERGY_DECAY: 2.4, // energy/s
    ORB_ENERGY: 26,
    // world
    CH_LEN: 130,
    N_AHEAD: 5,
    GEN_SPACING: 118,
    SPAWN_AHEAD: 640,
    SUN_Z: -3800,
    GATE_HALF_W: 27,
    GATE_HALF_H: 15,
    GATE_MIN: 11,
    GATE_MAX: 17,
    WALL_H: 95,
    WALL_BASE_HZ: 9,
    WALL_MAX_HZ: 15,
    // near miss
    NM_GRACE: 4.8,
    NM_CREDIT: 9.0,
    NM_WINDOW: 3.2,
    HIT_GRACE: 0.9,
    HIT_R: 1.9,
    HIT_E_LOSS: 12,
    HIT_MULT_KEEP: 0.5,
    // scoring
    SCORE_SPEED: 0.2,
    SCORE_NM: 120,
    SCORE_ORB: 25,
};
exports.PAL = {
    skyTop: 0x1a0f38,
    skyMid: 0x6b2d6e,
    skyLow: 0xff8a4d,
    horizon: 0xffd9a0,
    sun: 0xffe6b0,
    sunCore: 0xfff3d6,
    fog: 0xd96b3f,
    rockA: 0x8a3a48,
    rockB: 0x5e2c56,
    rockC: 0x3a2150,
    wallEdge: 0xff8a5c,
    monolith: 0x46326e,
    monolithEdge: 0xc07cff,
    frame: 0x2a1a44,
    frameEdge: 0xffb36b,
    orb: 0x8be9ff,
    orbCore: 0xeaffff,
    engine: 0xffb347,
    ship: 0xcfd6e6,
};
// difficulty 0..1 (score-driven)
function difficultyOf(score) {
    return Math.min(1, score / 30000);
}
function maxSpeedFor(score) {
    const d = difficultyOf(score);
    return exports.CFG.BASE_SPEED + (exports.CFG.MAX_SPEED - exports.CFG.BASE_SPEED) * d;
}
function cruiseTarget(score) {
    return exports.CFG.BASE_SPEED + (exports.CFG.MAX_SPEED * 0.55 - exports.CFG.BASE_SPEED) * Math.min(1, score / 12000);
}
function gapHalfW(score) {
    const d = difficultyOf(score);
    return exports.CFG.GATE_HALF_W - d * 7.5;
}
function wallThickness(score) {
    const d = difficultyOf(score);
    return exports.CFG.WALL_BASE_HZ + d * (exports.CFG.WALL_MAX_HZ - exports.CFG.WALL_BASE_HZ);
}
function wallHeight(score) {
    const d = difficultyOf(score);
    return exports.CFG.WALL_H + d * 45;
}
function monolithDensity(score) {
    return 2 + Math.floor(difficultyOf(score) * 4); // per 2 chunks
}
function gapYRange(score) {
    return exports.CFG.GATE_HALF_H - difficultyOf(score) * 3;
}
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
exports.clamp = clamp;
const lerp = (a, b, t) => a + (b - a) * t;
exports.lerp = lerp;
const rand = (a, b) => a + Math.random() * (b - a);
exports.rand = rand;
const smooth = (t) => t * t * (3 - 2 * t);
exports.smooth = smooth;
};

// ── module: src/worldgen.ts ──
__mods["src/worldgen.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.World = void 0;
// Chunk-based procedural canyon generation with shared geometry and clean disposal.
const types_1 = require("./types");
const L = types_1.CFG.CH_LEN;
class World {
    constructor(scene, sunLight) {
        this.gates = new Map();
        this.chunks = [];
        this.maxIndex = 0;
        this.score = 0;
        this.scene = scene;
        this.sunLight = sunLight;
        this.slabMat = new THREE.MeshLambertMaterial({ vertexColors: true, side: THREE.DoubleSide });
        this.monoMat = new THREE.MeshLambertMaterial({ color: types_1.PAL.monolith, emissive: 0x241340, emissiveIntensity: 0.9 });
        this.frameMat = new THREE.MeshLambertMaterial({ color: types_1.PAL.frame, emissive: 0x3a1a10, emissiveIntensity: 0.8 });
        this.orbMat = new THREE.MeshLambertMaterial({ color: 0x06202a, emissive: types_1.PAL.orb, emissiveIntensity: 1.6 });
        this.tipMat = new THREE.MeshBasicMaterial({ color: types_1.PAL.monolithEdge });
        this.boxGeo = new THREE.BoxGeometry(1, 1, 1);
        this.orbGeo = new THREE.IcosahedronGeometry(1, 0);
        this.tipGeo = new THREE.OctahedronGeometry(1, 0);
    }
    // ---------------- gate path ----------------
    gate(i) {
        let g = this.gates.get(i);
        if (g)
            return g;
        const base = i === 0 ? { x: 0, y: 2, hw: 22 } : this.gate(i - 1);
        const d = this.score > 0 ? Math.min(1, this.score / 30000) : 0;
        const x = (0, types_1.clamp)(base.x + (0, types_1.rand)(-1, 1) * (15 + 20 * d), -44, 44);
        const y = (0, types_1.clamp)(base.y + (0, types_1.rand)(-1, 1) * (5 + 9 * d), -22, 24);
        let hw = (0, types_1.clamp)((0, types_1.gapHalfW)(this.score) + (0, types_1.rand)(-1.5, 1.5), 9, 21);
        if (i < 4)
            hw = Math.max(hw, 19); // gentle intro
        g = { x, y, hw };
        this.gates.set(i, g);
        return g;
    }
    // path point sampled along chunk i, t in [0,1] (0 = far end, 1 = near end)
    pathAt(i, t) {
        const tS = (0, types_1.smooth)(t);
        const a = this.gate(i + 1);
        const b = this.gate(i);
        return {
            x: (0, types_1.lerp)(a.x, b.x, tS),
            y: (0, types_1.lerp)(a.y, b.y, tS),
            hw: (0, types_1.lerp)(a.hw, b.hw, tS),
            z: -(i + 1 - t) * L,
        };
    }
    // ---------------- chunk building ----------------
    ensure(playerZ, lookahead) {
        while ((this.maxIndex + 1) * L < -playerZ + lookahead) {
            this.buildChunk(this.maxIndex);
            this.maxIndex++;
        }
    }
    disposeAll() {
        for (const c of this.chunks) {
            for (const m of c.meshes)
                if (m.parent)
                    m.parent.remove(m);
            c.dispose();
        }
        this.chunks.length = 0;
        this.gates.clear();
        this.maxIndex = 0;
        this.score = 0;
    }
    cull(playerZ) {
        // flight is toward -z: a chunk is only reclaimable once its FAR
        // (most-negative) end z0 is behind the player (+ margin), so every
        // collider ahead of the player survives.
        const behind = playerZ + 30;
        for (let i = this.chunks.length - 1; i >= 0; i--) {
            const c = this.chunks[i];
            if (c.z0 > behind) {
                for (const m of c.meshes)
                    if (m.parent)
                        m.parent.remove(m);
                c.dispose();
                this.chunks.splice(i, 1);
            }
        }
    }
    buildChunkPublic(i) {
        this.buildChunk(i);
    }
    buildChunk(i) {
        const z0 = -(i + 1) * L; // far
        const z1 = -i * L; // near
        const meshes = [];
        const boxes = [];
        const spheres = [];
        const disposes = [];
        // ---- canyon wall slabs (left + right) ----
        const H = (0, types_1.wallHeight)(0) + 14;
        const hz = (0, types_1.wallThickness)(this.score);
        const baseY = -13;
        for (const side of [-1, 1]) {
            const N = 8;
            const pos = [];
            const col = [];
            const idx = [];
            const cDeep = new THREE.Color(0x1d0f2e);
            const cWarm = new THREE.Color(types_1.PAL.rockA);
            const cTop = new THREE.Color(0xff9a5e);
            const cTmp = new THREE.Color();
            for (let s = 0; s <= N; s++) {
                const t = s / N;
                const p = this.pathAt(i, t);
                const jag = Math.sin(Math.PI * t) * (4 + 7 * Math.abs(Math.sin(t * 7.3 + i * 2.1))) * (0.6 + 0.4 * Math.sin(t * 13.7 + i));
                const xIn = p.x + side * p.hw;
                const xOut = p.x + side * (p.hw + hz + Math.max(2, jag));
                const topIn = baseY + H - Math.sin(Math.PI * t) * 10 * (0.5 + 0.5 * Math.sin(t * 9.1 + i * 3.7));
                const topOut = baseY + H + 8 - Math.sin(Math.PI * t) * (14 + 8 * Math.sin(t * 5.3 + i * 1.9));
                const v = (x, y, warm, inner) => {
                    pos.push(x, y, p.z);
                    cTmp.copy(cDeep).lerp(cWarm, (0, types_1.clamp)((y - baseY) / H, 0, 1) * 0.85).lerp(cTop, warm);
                    if (inner)
                        cTmp.multiplyScalar(0.62); // shadowed inner face
                    col.push(cTmp.r, cTmp.g, cTmp.b);
                };
                v(xOut, baseY, 0, false); // 0
                v(xOut, topOut, 0.55, false); // 1
                v(xIn, baseY, 0, true); // 2
                v(xIn, topIn, 0.35, true); // 3
            }
            const quad = (a, b, c2, d) => {
                idx.push(a, b, c2, a, c2, d);
            };
            for (let s = 0; s < N; s++) {
                const o = s * 4;
                quad(o + 0, o + 2, o + 6, o + 4); // bottom
                quad(o + 1, o + 3, o + 7, o + 5); // top
                quad(o + 2, o + 6, o + 7, o + 3); // inner face
                quad(o + 0, o + 4, o + 5, o + 1); // outer face
            }
            const geo = new THREE.BufferGeometry();
            geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(pos), 3));
            geo.setAttribute("color", new THREE.BufferAttribute(new Float32Array(col), 3));
            geo.setIndex(idx);
            geo.computeVertexNormals();
            const mesh = new THREE.Mesh(geo, this.slabMat);
            mesh.castShadow = true;
            mesh.receiveShadow = true;
            this.scene.add(mesh);
            meshes.push(mesh);
            disposes.push(() => geo.dispose());
            // wall near-miss spheres along each edge
            for (let s = 0; s < 3; s++) {
                const t = (s + 0.5) / 3;
                const p = this.pathAt(i, t);
                spheres.push({ cx: p.x + side * (p.hw + hz * 0.5), cy: 4, cz: p.z, r: hz * 0.5 + 3.2, type: "wall" });
            }
            // wall hit boxes (split in thirds)
            for (let s = 0; s < 3; s++) {
                const ta = s / 3, tb = (s + 1) / 3;
                const pa = this.pathAt(i, ta), pb = this.pathAt(i, tb);
                const zx = (pa.z + pb.z) / 2;
                const zlen = L / 3 + 1;
                const xEdge = (pa.x + pb.x) / 2 + side * ((pa.hw + pb.hw) / 2);
                boxes.push({ cx: xEdge + side * (hz / 2 + 2), cy: 2, cz: zx, hx: hz / 2 + 2, hy: 60, hz: zlen / 2 });
            }
        }
        // ---- monoliths ----
        const d = this.score > 0 ? Math.min(1, this.score / 30000) : 0;
        if (i > 1 && Math.random() < 0.55 + 0.35 * d) {
            const n = Math.random() < 0.35 + 0.45 * d ? 2 : 1;
            for (let k = 0; k < n; k++) {
                const t = (0, types_1.rand)(0.18, 0.82);
                const p = this.pathAt(i, t);
                const r = (0, types_1.rand)(3.2, 6 + 3 * d);
                const h = (0, types_1.rand)(18, 34);
                const ox = (0, types_1.rand)(-0.5, 0.5) * (p.hw - r - 2);
                const oy = p.y + (0, types_1.rand)(-0.35, 0.35) * 2 * (0, types_1.gapYRange)(this.score);
                const cx = p.x + ox, cy = oy, cz = p.z;
                const body = new THREE.Mesh(this.boxGeo, this.monoMat);
                body.position.set(cx, cy, cz);
                body.scale.set(r * 2, h, r * 2);
                body.rotation.y = (0, types_1.rand)(0, Math.PI) * 0.25;
                body.castShadow = true;
                body.receiveShadow = true;
                const tip = new THREE.Mesh(this.tipGeo, this.tipMat);
                tip.position.set(cx, cy + h / 2 + 1.6, cz);
                tip.scale.setScalar(0.9);
                this.scene.add(body, tip);
                meshes.push(body, tip);
                boxes.push({ cx, cy, cz, hx: r, hy: h / 2, hz: r });
                spheres.push({ cx, cy, cz, r: r + 2.0, type: "mono" });
            }
        }
        // ---- squeeze frames ----
        if (i > 2 && i % 2 === 0 && Math.random() < 0.22 + 0.45 * d) {
            const t = 0.5;
            const p = this.pathAt(i, t);
            const ghSq = 7.5 + (0, types_1.rand)(0, 3.5) - d * 1.5; // vertical half gap
            const hwSq = p.hw - 2 - (0, types_1.rand)(0, 2.5);
            const bw = 2.4; // beam width
            const z = p.z;
            const xL = p.x - hwSq, xR = p.x + hwSq;
            const yB = p.y - ghSq, yT = p.y + ghSq;
            const span = (w, cx, cy, cx2, cy2) => {
                const dx = cx2 - cx, dy = cy2 - cy;
                const m = new THREE.Mesh(this.boxGeo, this.frameMat);
                m.position.set((cx + cx2) / 2, (cy + cy2) / 2, z);
                m.scale.set(w + Math.abs(dx), w + Math.abs(dy), bw);
                m.castShadow = true;
                this.scene.add(m);
                meshes.push(m);
            };
            span(bw, xL, yT, xR, yT); // top beam
            span(bw, xL, yB, xR, yB); // bottom beam
            span(bw, xL, yB, xL, yT); // left post
            span(bw, xR, yB, xR, yT); // right post
            boxes.push({ cx: p.x, cy: yT + bw / 2, cz: z, hx: (xR - xL) / 2 + bw / 2, hy: bw / 2, hz: bw / 2 }, { cx: p.x, cy: yB - bw / 2, cz: z, hx: (xR - xL) / 2 + bw / 2, hy: bw / 2, hz: bw / 2 }, { cx: xL - bw / 2, cy: p.y, cz: z, hx: bw / 2, hy: ghSq + bw / 2, hz: bw / 2 }, { cx: xR + bw / 2, cy: p.y, cz: z, hx: bw / 2, hy: ghSq + bw / 2, hz: bw / 2 });
            for (const [sx, sy] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) {
                spheres.push({ cx: p.x + sx * hwSq, cy: p.y + sy * ghSq, cz: z, r: 3.4, type: "squeeze" });
            }
        }
        // ---- energy orbs (placed on the risky edge: near a wall or monolith) ----
        if (i > 0 && Math.random() < 0.85) {
            const n = 3 + Math.floor((0, types_1.rand)(0, 2) + d * 1.6);
            const side = Math.random() < 0.5 ? -1 : 1;
            const t0 = (0, types_1.rand)(0.12, 0.55);
            for (let k = 0; k < n; k++) {
                const t = (0, types_1.clamp)(t0 + k * 0.11, 0.05, 0.95);
                const p = this.pathAt(i, t);
                const risky = Math.random() < 0.65;
                const edge = p.hw * (risky ? 0.62 + Math.random() * 0.2 : 0.25 + Math.random() * 0.15);
                const x = p.x + side * edge;
                const y = (0, types_1.clamp)(p.y + (Math.random() - 0.5) * 5, p.y - (0, types_1.gapYRange)(this.score) + 2, p.y + (0, types_1.gapYRange)(this.score) - 2);
                const orb = new THREE.Mesh(this.orbGeo, this.orbMat);
                orb.position.set(x, y, p.z);
                orb.scale.setScalar(1.5);
                this.scene.add(orb);
                meshes.push(orb);
                spheres.push({ cx: x, cy: y, cz: p.z, r: 3.4, type: "orb", mesh: orb });
            }
        }
        this.chunks.push({
            type: "wall",
            z0, z1,
            meshes,
            boxes,
            spheres,
            dispose: () => { for (const fn of disposes)
                fn(); },
        });
    }
}
exports.World = World;
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sfx = void 0;
// Procedural Web Audio: engine hum, orb pings, near-miss whooshes, crash, turbo.
class Sfx {
    constructor() {
        this.ctx = null;
        this.master = null;
        this.engineOsc = null;
        this.engineSub = null;
        this.engineGain = null;
        this.engineLfo = null;
        this.noiseBuf = null;
        this.muted = false;
        this._filt = null;
    }
    init() {
        if (this.ctx) {
            void this.ctx.resume();
            return;
        }
        const AC = window.AudioContext || window.webkitAudioContext;
        if (!AC)
            return;
        const ctx = new AC();
        this.ctx = ctx;
        this.master = ctx.createGain();
        this.master.gain.value = 0.55;
        this.master.connect(ctx.destination);
        // noise buffer (shared)
        const len = ctx.sampleRate * 1;
        const buf = ctx.createBuffer(1, len, ctx.sampleRate);
        const d = buf.getChannelData(0);
        for (let i = 0; i < len; i++)
            d[i] = Math.random() * 2 - 1;
        this.noiseBuf = buf;
        // engine drone: detuned saw + sub sine, filtered
        const g = ctx.createGain();
        g.gain.value = 0.0;
        const filt = ctx.createBiquadFilter();
        filt.type = "lowpass";
        filt.frequency.value = 380;
        filt.Q.value = 6;
        const o1 = ctx.createOscillator();
        o1.type = "sawtooth";
        o1.frequency.value = 55;
        const o2 = ctx.createOscillator();
        o2.type = "sawtooth";
        o2.frequency.value = 55.7;
        const sub = ctx.createOscillator();
        sub.type = "sine";
        sub.frequency.value = 28;
        const subG = ctx.createGain();
        subG.gain.value = 0.5;
        o1.connect(filt);
        o2.connect(filt);
        filt.connect(g);
        sub.connect(subG);
        subG.connect(g);
        g.connect(this.master);
        const lfo = ctx.createOscillator();
        lfo.type = "sine";
        lfo.frequency.value = 11;
        const lfoG = ctx.createGain();
        lfoG.gain.value = 22;
        lfo.connect(lfoG);
        lfoG.connect(o1.frequency);
        o1.start();
        o2.start();
        sub.start();
        lfo.start();
        this.engineOsc = o1;
        this.engineSub = sub;
        this.engineGain = g;
        this.engineLfo = lfo;
        filt.__sfx_filt = true;
        this._filt = filt;
    }
    setEngine(speedNorm, boost, alive) {
        if (!this.ctx || !this.engineOsc || !this.engineGain)
            return;
        const t = this.ctx.currentTime;
        const on = alive && !this.muted;
        const base = on ? (24 + speedNorm * 40 + (boost ? 26 : 0)) : 0;
        this.engineOsc.frequency.setTargetAtTime(55 + speedNorm * 70 + (boost ? 40 : 0), t, 0.08);
        if (this.engineSub)
            this.engineSub.frequency.setTargetAtTime(28 + speedNorm * 44, t, 0.08);
        if (this._filt)
            this._filt.frequency.setTargetAtTime(300 + speedNorm * 1600 + (boost ? 900 : 0), t, 0.1);
        this.engineGain.gain.setTargetAtTime(on ? 0.05 + speedNorm * 0.07 + (boost ? 0.045 : 0) : 0.0, t, 0.15);
        void base;
    }
    env(g, t, a, peak, dec) {
        g.gain.cancelScheduledValues(t);
        g.gain.setValueAtTime(0.0001, t);
        g.gain.linearRampToValueAtTime(peak, t + a);
        g.gain.exponentialRampToValueAtTime(0.0001, t + a + dec);
    }
    orb(step) {
        if (!this.ctx || !this.master)
            return;
        const t = this.ctx.currentTime;
        const o = this.ctx.createOscillator();
        o.type = "sine";
        const f = 660 * Math.pow(1.06, step % 12);
        o.frequency.value = f;
        const g = this.ctx.createGain();
        this.env(g, t, 0.008, 0.22, 0.35);
        o.connect(g);
        g.connect(this.master);
        o.start(t);
        o.stop(t + 0.5);
        // sparkle octave
        const o2 = this.ctx.createOscillator();
        o2.type = "triangle";
        o2.frequency.value = f * 2;
        const g2 = this.ctx.createGain();
        this.env(g2, t + 0.02, 0.008, 0.1, 0.2);
        o2.connect(g2);
        g2.connect(this.master);
        o2.start(t + 0.02);
        o2.stop(t + 0.4);
    }
    whoosh(intensity) {
        if (!this.ctx || !this.master || !this.noiseBuf)
            return;
        const t = this.ctx.currentTime;
        const src = this.ctx.createBufferSource();
        src.buffer = this.noiseBuf;
        src.loop = true;
        src.playbackRate.value = 0.7 + intensity * 0.8;
        const bp = this.ctx.createBiquadFilter();
        bp.type = "bandpass";
        bp.Q.value = 1.4;
        const g = this.ctx.createGain();
        const dur = 0.45;
        bp.frequency.setValueAtTime(160, t);
        bp.frequency.exponentialRampToValueAtTime(2400 + 2400 * intensity, t + dur * 0.5);
        bp.frequency.exponentialRampToValueAtTime(300, t + dur);
        this.env(g, t, 0.04, 0.16 + intensity * 0.3, dur);
        src.connect(bp);
        bp.connect(g);
        g.connect(this.master);
        src.start(t);
        src.stop(t + dur + 0.1);
    }
    crash() {
        if (!this.ctx || !this.master || !this.noiseBuf)
            return;
        const t = this.ctx.currentTime;
        const src = this.ctx.createBufferSource();
        src.buffer = this.noiseBuf;
        const lp = this.ctx.createBiquadFilter();
        lp.type = "lowpass";
        lp.frequency.setValueAtTime(2600, t);
        lp.frequency.exponentialRampToValueAtTime(80, t + 0.9);
        const g = this.ctx.createGain();
        this.env(g, t, 0.005, 0.9, 0.9);
        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(110, t);
        o.frequency.exponentialRampToValueAtTime(32, t + 0.5);
        const g2 = this.ctx.createGain();
        this.env(g2, t, 0.005, 0.5, 0.5);
        o.connect(g2);
        g2.connect(this.master);
        o.start(t);
        o.stop(t + 0.7);
    }
    turbo(on) {
        if (!this.ctx || !this.master || !this.noiseBuf)
            return;
        const t = this.ctx.currentTime;
        const src = this.ctx.createBufferSource();
        src.buffer = this.noiseBuf;
        src.loop = true;
        const bp = this.ctx.createBiquadFilter();
        bp.type = "bandpass";
        bp.Q.value = 0.8;
        bp.frequency.value = 900;
        const g = this.ctx.createGain();
        if (on) {
            this.env(g, t, 0.25, 0.22, 0.3);
        }
        else {
            g.gain.cancelScheduledValues(t);
            g.gain.setTargetAtTime(0.0001, t, 0.2);
        }
        src.connect(bp);
        bp.connect(g);
        g.connect(this.master);
        src.start(t);
        src.stop(t + (on ? 0.6 : 0.6));
    }
    uiClick() {
        if (!this.ctx || !this.master)
            return;
        const t = this.ctx.currentTime;
        const o = this.ctx.createOscillator();
        o.type = "square";
        o.frequency.value = 880;
        const g = this.ctx.createGain();
        this.env(g, t, 0.004, 0.08, 0.08);
        o.connect(g);
        g.connect(this.master);
        o.start(t);
        o.stop(t + 0.12);
    }
    setMuted(m) {
        this.muted = m;
        if (this.master && this.ctx)
            this.master.gain.setTargetAtTime(m ? 0 : 0.55, this.ctx.currentTime, 0.05);
    }
    isMuted() { return this.muted; }
}
exports.sfx = new Sfx();
};

// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Particles = void 0;
exports.makeGlowCanvas = makeGlowCanvas;
// Pooled GPU point particles: engine trail, dust streaks, debris, burst sparks.
const MAX = 2400;
class Particles {
    constructor(scene, makeCanvas) {
        this.pool = [];
        this.cursor = 0;
        this.scene = scene;
        this.pos = new Float32Array(MAX * 3);
        this.col = new Float32Array(MAX * 3);
        for (let i = 0; i < MAX; i++) {
            this.pos[i * 3 + 1] = -9999;
            this.pool.push({
                x: 0, y: -9999, z: 0, vx: 0, vy: 0, vz: 0,
                life: 0, maxLife: 1, size: 1, r: 1, g: 1, b: 1,
                active: false, drag: 0, grav: 0,
            });
        }
        this.geo = new THREE.BufferGeometry();
        this.geo.setAttribute("position", new THREE.BufferAttribute(this.pos, 3));
        this.geo.setAttribute("color", new THREE.BufferAttribute(this.col, 3));
        const tex = new THREE.CanvasTexture(makeCanvas(64));
        this.mat = new THREE.PointsMaterial({
            size: 1.6,
            map: tex,
            vertexColors: true,
            transparent: true,
            depthWrite: false,
            blending: THREE.AdditiveBlending,
            sizeAttenuation: true,
        });
        this.pts = new THREE.Points(this.geo, this.mat);
        this.pts.frustumCulled = false;
        scene.add(this.pts);
    }
    grab() {
        // ring-find a slot, overwriting the oldest if full
        for (let i = 0; i < MAX; i++) {
            const idx = (this.cursor + i) % MAX;
            const p = this.pool[idx];
            if (!p.active) {
                this.cursor = (idx + 1) % MAX;
                return p;
            }
        }
        const p = this.pool[this.cursor];
        this.cursor = (this.cursor + 1) % MAX;
        p.active = true;
        return p;
    }
    spawn(x, y, z, vx, vy, vz, life, size, r, g, b, drag, grav) {
        const p = this.grab();
        p.x = x;
        p.y = y;
        p.z = z;
        p.vx = vx;
        p.vy = vy;
        p.vz = vz;
        p.life = life;
        p.maxLife = life;
        p.size = size;
        p.r = r;
        p.g = g;
        p.b = b;
        p.active = true;
        p.drag = drag;
        p.grav = grav;
    }
    burst(x, y, z, n, speed, size, r, g, b, life, grav = 0) {
        for (let i = 0; i < n; i++) {
            const a = Math.random() * Math.PI * 2;
            const b = Math.acos(Math.random() * 2 - 1);
            const s = speed * (0.3 + Math.random() * 0.7);
            this.spawn(x, y, z, Math.sin(b) * Math.cos(a) * s, Math.cos(b) * s, Math.sin(b) * Math.sin(a) * s, life * (0.5 + Math.random() * 0.5), size * (0.6 + Math.random() * 0.8), r, g, b, 0.4, grav);
        }
    }
    update(dt) {
        for (let i = 0; i < MAX; i++) {
            const p = this.pool[i];
            if (!p.active)
                continue;
            p.life -= dt;
            if (p.life <= 0) {
                p.active = false;
                this.pos[i * 3 + 1] = -9999;
                continue;
            }
            const k = Math.max(0, 1 - dt * p.drag);
            p.vx *= k;
            p.vy *= k;
            p.vz *= k;
            p.vy -= p.grav * dt;
            p.x += p.vx * dt;
            p.y += p.vy * dt;
            p.z += p.vz * dt;
            const j = i * 3;
            this.pos[j] = p.x;
            this.pos[j + 1] = p.y;
            this.pos[j + 2] = p.z;
            const f = p.life / p.maxLife;
            const bright = p.size > 2 ? 1 : f;
            this.col[j] = p.r * bright;
            this.col[j + 1] = p.g * bright;
            this.col[j + 2] = p.b * bright;
        }
        this.geo.getAttribute("position").needsUpdate = true;
        this.geo.getAttribute("color").needsUpdate = true;
    }
}
exports.Particles = Particles;
function makeGlowCanvas(size) {
    const c = document.createElement("canvas");
    c.width = size;
    c.height = size;
    const ctx = c.getContext("2d");
    const g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
    g.addColorStop(0, "rgba(255,255,255,1)");
    g.addColorStop(0.3, "rgba(255,255,255,.7)");
    g.addColorStop(1, "rgba(255,255,255,0)");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, size, size);
    return c;
}
};

// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Player = void 0;
// Player craft: low-poly mesh, flight physics (momentum / bank / pitch accel), chase camera.
const types_1 = require("./types");
class Player {
    constructor(scene) {
        this.pos = { x: 0, y: 2, z: 0 };
        this.vx = 0;
        this.vy = 0;
        this.vz = 0;
        this.pitch = 0;
        this.roll = 0;
        this.yaw = 0;
        this.alive = true;
        this.deadT = 0;
        this.boosting = false;
        this.wallFlash = 0;
        this.hitCd = 0; // post-hit invulnerability
        this.speedNorm = 0; // 0..1 for HUD/audio
        this.group = new THREE.Group();
        this.bodyMat = new THREE.MeshLambertMaterial({ color: 0xd8dfeb, emissive: 0x1a2030, emissiveIntensity: 0.8 });
        this.glowMat = new THREE.MeshBasicMaterial({ color: 0xffc36b });
        const nose = new THREE.Mesh(new THREE.ConeGeometry(1.15, 5.4, 5), this.bodyMat);
        nose.rotation.x = -Math.PI / 2;
        nose.position.z = -2.6;
        const hull = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.8, 3.4), this.bodyMat);
        hull.position.z = 0.6;
        const wing = new THREE.Mesh(new THREE.BoxGeometry(5.6, 0.22, 2.1), this.bodyMat);
        wing.position.set(0, -0.1, 1.4);
        const finL = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.1, 1.5), this.bodyMat);
        finL.position.set(-0.8, 0.55, 1.6);
        const finR = finL.clone();
        finR.position.x = 0.8;
        finR.material = this.bodyMat;
        const engine = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.85, 1.1, 6), this.bodyMat);
        engine.rotation.x = Math.PI / 2;
        engine.position.set(0, 0.1, 2.35);
        const engineGlow = new THREE.Mesh(new THREE.SphereGeometry(0.55, 6, 6), this.glowMat);
        engineGlow.position.set(0, 0.1, 2.9);
        this.engine = engine;
        this.engineGlow = engineGlow;
        for (const m of [nose, hull, wing, finL, finR, engine, engineGlow]) {
            m.castShadow = true;
            this.group.add(m);
        }
        this.group.position.set(this.pos.x, this.pos.y, this.pos.z);
        scene.add(this.group);
    }
    reset() {
        this.pos.x = 0;
        this.pos.y = 2;
        this.pos.z = 0;
        this.vx = 0;
        this.vy = 0;
        this.vz = types_1.CFG.BASE_SPEED;
        this.pitch = 0;
        this.roll = 0;
        this.yaw = 0;
        this.alive = true;
        this.deadT = 0;
        this.boosting = false;
        this.wallFlash = 0;
        this.hitCd = 0;
        this.group.position.set(0, 2, 0);
        this.group.quaternion.identity();
    }
    // input: ix -1..1 (steer), iy -1..1 (pitch: up positive)
    update(dt, ix, iy, cruise, boost, energy) {
        if (!this.alive) {
            this.deadT += dt;
            // tumble down after crash
            this.vy -= 40 * dt;
            this.pos.x += this.vx * dt * 0.3;
            this.pos.y += this.vy * dt;
            this.pos.z += this.vz * dt * 0.4;
            this.pitch += dt * 3.2;
            this.roll += dt * 2.1;
            this.applyVisual(1);
            return;
        }
        const maxSpd = boost && energy > types_1.CFG.BOOST_MIN_ENERGY ? types_1.CFG.BOOST_SPEED : cruise;
        this.boosting = boost && energy > types_1.CFG.BOOST_MIN_ENERGY && this.vz > cruise * 0.6;
        // pitch-based acceleration
        let target = cruise;
        if (iy > 0.05)
            target += types_1.CFG.PITCH_ACCEL * iy * 0.9;
        else if (iy < -0.05)
            target -= types_1.CFG.PITCH_BRAKE * (-iy) * 0.9;
        if (this.boosting)
            target = Math.max(target, types_1.CFG.BOOST_SPEED);
        const acc = this.boosting ? 140 : (this.vz < target ? types_1.CFG.SPEED_ACCEL : 34);
        this.vz += (0, types_1.clamp)(target - this.vz, -acc * dt, acc * dt) * (this.vz < target ? 1 : 1);
        this.vz = (0, types_1.clamp)(this.vz, 26, types_1.CFG.BOOST_SPEED);
        // lateral momentum toward input direction
        const latTarget = ix * types_1.CFG.MAX_LAT_SPEED;
        this.vx += (latTarget - this.vx) * Math.min(1, dt * 3.4);
        // pitch drives vertical (throttle-like, with slight weight bias)
        this.vy += (iy * 30 - 2.0 - this.vy) * Math.min(1, dt * 3.0);
        this.vx *= Math.max(0, 1 - types_1.CFG.DRAG_XY * dt * 0.35);
        this.pos.x += this.vx * dt;
        this.pos.y += this.vy * dt;
        this.pos.z -= this.vz * dt;
        // soft altitude bounds (canyons don't have floors — but the sky does)
        if (this.pos.y < -42) {
            this.pos.y = -42;
            this.vy = Math.abs(this.vy) * 0.35;
            this.wallFlash = Math.max(this.wallFlash, 0.5);
        }
        if (this.pos.y > 48) {
            this.pos.y = 48;
            this.vy = -Math.abs(this.vy) * 0.35;
        }
        if (Math.abs(this.pos.x) > 120) {
            this.pos.x = Math.sign(this.pos.x) * 120;
            this.vx *= -0.4;
        }
        // visual attitude
        const targetRoll = -ix * 0.85;
        const targetPitch = (0, types_1.clamp)(iy * -0.45 + (this.vy / 40) * -0.5, -0.7, 0.7);
        this.roll += (targetRoll - this.roll) * Math.min(1, dt * 6);
        this.pitch += (targetPitch - this.pitch) * Math.min(1, dt * 5);
        this.yaw = Math.atan2(this.vx, this.vz) * -1 * -1; // heading of travel
        this.speedNorm = (0, types_1.clamp)((this.vz - 30) / (types_1.CFG.BOOST_SPEED - 30), 0, 1);
        this.wallFlash = Math.max(0, this.wallFlash - dt * 2.5);
        this.hitCd = Math.max(0, this.hitCd - dt);
        this.applyVisual(dt);
        // engine glow pulse
        const gl = this.boosting ? 1.35 + Math.sin(performance.now() * 0.04) * 0.25 : 0.7 + this.speedNorm * 0.5;
        this.engineGlow.scale.setScalar(gl);
        this.engineGlow.material.color.setHex(this.boosting ? 0x9be9ff : 0xffc36b);
    }
    applyVisual(dt) {
        this.group.position.set(this.pos.x, this.pos.y, this.pos.z);
        const q = new THREE.Quaternion();
        q.setFromEuler(new THREE.Euler(this.pitch, this.yaw, this.roll, "YXZ"));
        this.group.quaternion.slerp(q, Math.min(1, dt * 10));
    }
    // chase camera with speed FOV + shake
    updateCamera(cam, dt, shake, fovBase, fovBoost, time) {
        const back = 9.5 + this.speedNorm * 4.5;
        const up = 3.4 + this.speedNorm * 1.4;
        const px = this.pos.x - this.vx * 0.12;
        const py = this.pos.y + up;
        const pz = this.pos.z + back;
        const k = Math.min(1, dt * 5.5);
        cam.position.x += (px - cam.position.x) * k;
        cam.position.y += (py - cam.position.y) * k;
        cam.position.z += (pz - cam.position.z) * k;
        if (shake > 0.001) {
            cam.position.x += (Math.random() - 0.5) * shake;
            cam.position.y += (Math.random() - 0.5) * shake * 0.7;
            cam.position.z += (Math.random() - 0.5) * shake * 0.5;
        }
        cam.lookAt(this.pos.x + this.vx * 0.35, this.pos.y + this.vy * 0.3 + 1.2, this.pos.z - 14);
        const targetFov = (0, types_1.lerp)(fovBase, fovBoost, this.speedNorm) + Math.sin(time * 13) * shake * 0.6;
        cam.fov += (targetFov - cam.fov) * Math.min(1, dt * 4);
        cam.updateProjectionMatrix();
    }
}
exports.Player = Player;
};

// ── module: src/scene.ts ──
__mods["src/scene.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeSkyTexture = makeSkyTexture;
exports.makeRadialTexture = makeRadialTexture;
exports.makeStarTexture = makeStarTexture;
exports.buildScene = buildScene;
// Scene construction: renderer, sky dome, sun, stars, lights with long shadows.
const types_1 = require("./types");
function makeSkyTexture() {
    const c = document.createElement("canvas");
    c.width = 4;
    c.height = 512;
    const ctx = c.getContext("2d");
    const g = ctx.createLinearGradient(0, 0, 0, 512);
    g.addColorStop(0.0, "#120a2c");
    g.addColorStop(0.42, "#3c1b52");
    g.addColorStop(0.62, "#8a3568");
    g.addColorStop(0.78, "#e06a45");
    g.addColorStop(0.9, "#ff9a55");
    g.addColorStop(1.0, "#ffc98a");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, 4, 512);
    return c;
}
function makeRadialTexture(stops) {
    const c = document.createElement("canvas");
    c.width = c.height = 128;
    const ctx = c.getContext("2d");
    const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
    for (const [o, col] of stops)
        g.addColorStop(o, col);
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, 128, 128);
    return c;
}
function makeStarTexture() {
    const c = document.createElement("canvas");
    c.width = c.height = 64;
    const ctx = c.getContext("2d");
    const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
    g.addColorStop(0, "rgba(255,240,255,1)");
    g.addColorStop(0.25, "rgba(255,230,255,.5)");
    g.addColorStop(1, "rgba(255,220,255,0)");
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, 64, 64);
    return c;
}
function buildScene(canvas) {
    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.6));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.outputEncoding = THREE.sRGBEncoding;
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    const scene = new THREE.Scene();
    scene.fog = new THREE.Fog(types_1.PAL.fog, 90, 820);
    const camera = new THREE.PerspectiveCamera(72, window.innerWidth / window.innerHeight, 0.1, 5000);
    camera.position.set(0, 6, 16);
    // sky dome
    const skyTex = new THREE.CanvasTexture(makeSkyTexture());
    const sky = new THREE.Mesh(new THREE.SphereGeometry(4200, 24, 16), new THREE.MeshBasicMaterial({ map: skyTex, side: THREE.BackSide, depthWrite: false, fog: false }));
    sky.position.set(0, 0, -1800);
    scene.add(sky);
    // sun disc + corona (far on the path)
    const sunDisc = new THREE.Mesh(new THREE.SphereGeometry(150, 24, 16), new THREE.MeshBasicMaterial({ color: types_1.PAL.sunCore, fog: false }));
    sunDisc.position.set(0, 160, types_1.CFG.SUN_Z);
    scene.add(sunDisc);
    const corona = new THREE.Sprite(new THREE.SpriteMaterial({
        map: new THREE.CanvasTexture(makeRadialTexture([
            [0, "rgba(255,235,190,.9)"],
            [0.25, "rgba(255,180,110,.45)"],
            [0.6, "rgba(255,120,80,.12)"],
            [1, "rgba(255,100,80,0)"],
        ])),
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        fog: false,
        transparent: true,
    }));
    corona.scale.setScalar(2600);
    corona.position.set(0, 220, types_1.CFG.SUN_Z + 100);
    scene.add(corona);
    // stars
    const starCount = 320;
    const sp = new Float32Array(starCount * 3);
    const scl = new Float32Array(starCount * 3);
    for (let i = 0; i < starCount; i++) {
        const a = Math.random() * Math.PI * 2;
        const r = Math.random();
        const y = 400 + Math.random() * 2600;
        sp[i * 3] = Math.cos(a) * 2600 * (0.3 + r * 0.7);
        sp[i * 3 + 1] = y;
        sp[i * 3 + 2] = -1200 - Math.random() * 2600;
        const b = 0.35 + Math.random() * 0.65;
        scl[i * 3] = b;
        scl[i * 3 + 1] = b * 0.9;
        scl[i * 3 + 2] = b * 1.1;
    }
    const starGeo = new THREE.BufferGeometry();
    starGeo.setAttribute("position", new THREE.BufferAttribute(sp, 3));
    starGeo.setAttribute("color", new THREE.BufferAttribute(scl, 3));
    const stars = new THREE.Points(starGeo, new THREE.PointsMaterial({
        size: 7,
        map: new THREE.CanvasTexture(makeStarTexture()),
        vertexColors: true,
        transparent: true,
        depthWrite: false,
        sizeAttenuation: true,
        fog: false,
    }));
    stars.frustumCulled = false;
    scene.add(stars);
    // lights
    scene.add(new THREE.AmbientLight(0x6a4478, 0.95));
    const hemi = new THREE.HemisphereLight(0xffb070, 0x3a1a4a, 0.65);
    scene.add(hemi);
    const sun = new THREE.DirectionalLight(0xffc98a, 1.5);
    sun.position.set(-180, 130, 20);
    sun.castShadow = true;
    sun.shadow.mapSize.x = 1024;
    sun.shadow.mapSize.y = 1024;
    const cam = sun.shadow.camera;
    cam.left = -150;
    cam.right = 150;
    cam.top = 120;
    cam.bottom = -120;
    cam.near = 1;
    cam.far = 700;
    sun.shadow.bias = -0.0004;
    const target = new THREE.Object3D();
    target.position.set(0, 0, -260);
    scene.add(target);
    sun.target = target;
    scene.add(sun);
    return { renderer, scene, camera, sun, sunDisc, corona, stars };
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
exports.boot = boot;
// SUNDRIFT — game state machine, loop, collisions, scoring, HUD wiring.
const types_1 = require("./types");
const scene_1 = require("./scene");
const worldgen_1 = require("./worldgen");
const player_1 = require("./player");
const particles_1 = require("./particles");
const audio_1 = require("./audio");
const $ = (id) => document.getElementById(id);
const fmt = (n) => Math.floor(n).toString().padStart(7, "0");
const MENU_PANEL = `
    <div class="p-kicker">high-speed procedural canyon racer</div>
    <h1 class="p-title">SUN<span>DRIFT</span></h1>
    <p class="p-sub">Carve the endless dusk canyon. Every wall you kiss pays. Run out of energy and the sky closes in.</p>
    <div class="p-keys">
      <div class="k-row"><span>STEER · BANK</span><kbd>A D</kbd><kbd>◄ ►</kbd></div>
      <div class="k-row"><span>PITCH · BRAKE / BURN</span><kbd>S W</kbd><kbd>▼ ▲</kbd></div>
      <div class="k-row"><span>TURBO BURN</span><kbd>SHIFT</kbd></div>
      <div class="k-row"><span>RESTART</span><kbd>R</kbd></div>
    </div>
    <button id="startBtn" class="p-btn">LAUNCH <em>▶</em></button>
    <div class="p-hint">near-misses stack the multiplier · orbs refill energy · turbo builds on straightaways</div>`;
class Game {
    constructor() {
        this.phase = "menu";
        this.stats = { score: 0, mult: 1, maxSpeed: 0, distance: 0, nearMisses: 0, orbs: 0, time: 0, topMult: 1 };
        this.energy = 100;
        this.turbo = 0;
        this.boostHeld = false;
        this.keys = {};
        this.time = 0;
        this.shake = 0;
        this.multPulseT = 0;
        this.orbPulse = 0;
        this.nmCredit = new Set();
        this.lastMultUp = 1;
        this.trailAcc = 0;
        this.dustAcc = 0;
        this.boostOn = false;
        this.overAt = 0;
        this.lastT = performance.now();
        const canvas = $("gl");
        this.H = (0, scene_1.buildScene)(canvas);
        this.world = new worldgen_1.World(this.H.scene, this.H.sun);
        this.player = new player_1.Player(this.H.scene);
        this.parts = new particles_1.Particles(this.H.scene, particles_1.makeGlowCanvas);
        this.world.score = 0;
        for (let i = 0; i < 12; i++)
            this.world.buildChunkPublic(i);
        window.addEventListener("keydown", (e) => this.onKey(e, true));
        window.addEventListener("keyup", (e) => this.onKey(e, false));
        window.addEventListener("blur", () => { this.keys = {}; if (this.phase === "playing")
            this.setPhase("paused"); });
        window.addEventListener("resize", () => this.onResize());
        $("startBtn").addEventListener("click", () => this.start());
    }
    // ---------------- input ----------------
    onKey(e, down) {
        const k = e.key.toLowerCase();
        if (["arrowup", "arrowdown", "arrowleft", "arrowright", " "].includes(k))
            e.preventDefault();
        this.keys[k] = down;
        if (!down)
            return;
        if (k === " " && (this.phase === "menu" || (this.phase === "dead" && this.time - this.overAt > 1.2)))
            this.start();
        if (k === "r" && this.phase !== "menu")
            this.start();
        if (k === "escape") {
            if (this.phase === "playing")
                this.setPhase("paused");
            else if (this.phase === "paused")
                this.setPhase("playing");
        }
        if (k === "m")
            audio_1.sfx.setMuted(!audio_1.sfx.isMuted());
    }
    inputAxis() {
        const k = this.keys;
        const ix = (k["d"] || k["arrowright"] ? 1 : 0) - (k["a"] || k["arrowleft"] ? 1 : 0);
        const iy = (k["w"] || k["arrowup"] ? 1 : 0) - (k["s"] || k["arrowdown"] ? 1 : 0);
        return { ix, iy };
    }
    // ---------------- flow ----------------
    start() {
        audio_1.sfx.init();
        audio_1.sfx.uiClick();
        this.world.disposeAll();
        this.world.score = 0;
        for (let i = 0; i < 12; i++)
            this.world.buildChunkPublic(i);
        this.player.reset();
        this.stats = { score: 0, mult: 1, maxSpeed: 0, distance: 0, nearMisses: 0, orbs: 0, time: 0, topMult: 1 };
        this.energy = 100;
        this.turbo = 0;
        this.nmCredit.clear();
        this.lastMultUp = 1;
        this.time = 0;
        this.shake = 0;
        this.lastT = performance.now();
        const ov = $("overlay");
        ov.classList.remove("hidden");
        this.setPhase("playing");
    }
    setPhasePublic(p) {
        this.setPhase(p);
    }
    startPublic() {
        this.start();
    }
    setPhase(p) {
        this.phase = p;
        const ov = $("overlay");
        const panel = $("panel");
        if (p === "dead") {
            const reason = this.energy <= 0 ? "ENERGY DEPLETED" : "HULL IMPACT";
            panel.innerHTML = `
        <div class="p-kicker">run terminated — ${reason}</div>
        <h1 class="p-title">GAME <span>OVER</span></h1>
        <div class="stat-grid">
          <div class="stat"><b>${fmt(this.stats.score)}</b><i>FINAL SCORE</i></div>
          <div class="stat"><b>${Math.round(this.stats.maxSpeed)}<small style="font-size:14px"> km/h</small></b><i>MAX SPEED</i></div>
          <div class="stat"><b>x${Math.max(1, this.stats.topMult)}</b><i>BEST MULTIPLIER</i></div>
        </div>
        <button id="startBtn" class="p-btn">RESTART <em>⟳</em></button>
        <div class="p-hint">near-misses stack the multiplier · orbs refill energy · turbo builds on straightaways</div>`;
            document.getElementById("startBtn").addEventListener("click", () => this.start());
            ov.classList.remove("hidden");
            ov.classList.add("death");
        }
        else if (p === "menu") {
            ov.classList.remove("hidden", "death");
            panel.innerHTML = MENU_PANEL;
            document.getElementById("startBtn").addEventListener("click", () => this.start());
        }
        if (p === "playing" || p === "paused")
            ov.classList.add("hidden");
        if (p === "paused")
            $("paused").hidden = false;
        else
            $("paused").hidden = true;
    }
    die() {
        if (this.phase !== "playing")
            return;
        this.player.alive = false;
        audio_1.sfx.crash();
        audio_1.sfx.turbo(false);
        this.boostOn = false;
        this.shake = 1.4;
        const p = this.player.pos;
        this.parts.burst(p.x, p.y, p.z, 90, 26, 2.6, 1.0, 0.45, 0.15, 1.4, 20);
        this.parts.burst(p.x, p.y, p.z, 40, 12, 3.4, 0.4, 0.9, 1.0, 1.0);
        this.overAt = this.time;
        setTimeout(() => this.setPhase("dead"), 900);
    }
    // ---------------- collision + near-miss ----------------
    collectInv() {
        const s = [];
        const b = [];
        const pz = this.player.pos.z;
        for (let gi = 0; gi < this.world.chunks.length; gi++) {
            const c = this.world.chunks[gi];
            // include chunks overlapping [pz-130, pz+60] (ahead is more negative z)
            if (c.z0 > pz + 60 || c.z1 < pz - 130)
                continue;
            for (let i = 0; i < c.spheres.length; i++) {
                const sp = c.spheres[i];
                s.push({ cx: sp.cx, cy: sp.cy, cz: sp.cz, r: sp.r, type: sp.type, g: gi, si: i });
            }
            for (let i = 0; i < c.boxes.length; i++) {
                const bx = c.boxes[i];
                b.push({ cx: bx.cx, cy: bx.cy, cz: bx.cz, hx: bx.hx, hy: bx.hy, hz: bx.hz, g: gi });
            }
        }
        return { s, b };
    }
    handleCollisions(inv) {
        const pl = this.player;
        const px = pl.pos.x, py = pl.pos.y, pz = pl.pos.z;
        // hits (AABB vs point) — with a short post-hit invulnerability window
        const hr = types_1.CFG.HIT_R;
        if (pl.hitCd <= 0) {
            for (let i = 0; i < inv.b.length; i++) {
                const bx = inv.b[i];
                if (Math.abs(px - bx.cx) < bx.hx + hr &&
                    Math.abs(py - bx.cy) < bx.hy + hr &&
                    Math.abs(pz - bx.cz) < bx.hz + hr) {
                    this.energy -= types_1.CFG.HIT_E_LOSS;
                    pl.vz *= 0.68;
                    pl.vx = -Math.sign(px - bx.cx || 1) * 20;
                    pl.vy += 5;
                    pl.hitCd = 1.25;
                    pl.wallFlash = 1;
                    this.shake = Math.max(this.shake, 1.0);
                    this.stats.mult = Math.max(1, Math.round(this.stats.mult * types_1.CFG.HIT_MULT_KEEP));
                    // surviving a clip counts as a near-miss: the wall forgives once
                    this.stats.nearMisses++;
                    this.stats.score += types_1.CFG.SCORE_NM * 0.4 * this.stats.mult;
                    this.multPulseT = 1;
                    audio_1.sfx.whoosh(0.9);
                    this.parts.burst(px, py, pz, 16, 14, 2, 1, 0.5, 0.2, 0.7);
                    if (this.energy <= 0) {
                        this.energy = 0;
                        this.die();
                        return;
                    }
                    break;
                }
            }
        }
        // orbs
        for (let i = 0; i < inv.s.length; i++) {
            const sp = inv.s[i];
            if (sp.type !== "orb")
                continue;
            const dx = px - sp.cx, dy = py - sp.cy, dz = pz - sp.cz;
            if (dx * dx + dy * dy + dz * dz < 3.6 * 3.6) {
                this.energy = Math.min(100, this.energy + types_1.CFG.ORB_ENERGY);
                this.stats.orbs++;
                this.stats.score += types_1.CFG.SCORE_ORB * this.stats.mult;
                this.turbo = Math.min(100, this.turbo + 7);
                this.orbPulse = 1;
                this.nmCredit.delete(i);
                audio_1.sfx.orb(this.stats.orbs);
                const m = this.world.chunks[sp.g]?.spheres[sp.si];
                if (m && m.mesh) {
                    m.mesh.scale.setScalar(0.001);
                    m.mesh.visible = false;
                }
            }
        }
        // near-miss scoring
        const pcx = this.player.pos.x, pcy = this.player.pos.y, pcz = this.player.pos.z;
        for (let i = 0; i < inv.s.length; i++) {
            const sp = inv.s[i];
            if (sp.type === "orb")
                continue;
            const dx = pcx - sp.cx, dy = pcy - sp.cy, dz = pcz - sp.cz;
            const d2 = dx * dx + dy * dy + dz * dz;
            const R = sp.r + types_1.CFG.NM_GRACE;
            if (d2 < R * R) {
                if (!this.nmCredit.has(i)) {
                    this.nmCredit.add(i);
                    const d = Math.sqrt(d2);
                    const closeness = 1 - d / R;
                    const bonus = 1 + closeness * 3 + (sp.type === "squeeze" ? 0.4 : 0);
                    const multBefore = this.stats.mult;
                    this.stats.mult = Math.min(99, 1 + Math.round((this.stats.mult - 1) * 0.35 + 1 * bonus));
                    if (this.stats.mult > multBefore)
                        this.lastMultUp = this.stats.mult;
                    this.stats.mult = Math.max(this.stats.mult, 1);
                    this.stats.score += types_1.CFG.SCORE_NM * closeness * this.stats.mult;
                    this.stats.nearMisses++;
                    this.stats.topMult = Math.max(this.stats.topMult, this.stats.mult);
                    this.shake = Math.max(this.shake, 0.28 + closeness * 0.5);
                    this.multPulseT = 1;
                    audio_1.sfx.whoosh(0.25 + closeness * 0.75);
                    if (closeness > 0.45) {
                        this.parts.burst(pcx + (sp.cx - pcx) * 0.18, pcy, pcz, 10, 9, 1.6, 1, 0.85, 0.5, 0.5);
                    }
                }
            }
        }
    }
    // ---------------- particles fx ----------------
    emitFx(dt) {
        const p = this.player;
        if (p.alive) {
            // engine trail
            this.trailAcc += dt * (60 + p.speedNorm * 160);
            while (this.trailAcc > 1) {
                this.trailAcc -= 1;
                const bx = p.pos.x - p.vx * 0.06 + (Math.random() - 0.5) * 0.7;
                const by = p.pos.y - 0.3 + (Math.random() - 0.5) * 0.7;
                const bz = p.pos.z + 3.2;
                const boost = p.boosting;
                this.parts.spawn(bx, by, bz, (Math.random() - 0.5) * 3, -1 - Math.random() * 3, 4 + Math.random() * 6, boost ? 0.5 : 0.32, boost ? 3.0 : 2.0, boost ? 0.45 : 1.0, boost ? 0.85 : 0.62, boost ? 1.0 : 0.3, 0.6, 0);
            }
            // dust / debris streaking past
            this.dustAcc += dt * (8 + p.speedNorm * 26);
            while (this.dustAcc > 1) {
                this.dustAcc -= 1;
                const side = Math.random() < 0.5 ? -1 : 1;
                const dx = p.pos.x + side * (10 + Math.random() * 26);
                const dy = p.pos.y - 6 + Math.random() * 16;
                const dz = p.pos.z + 6 + Math.random() * 18;
                this.parts.spawn(dx, dy, dz, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, -p.vz * 0.55, 0.9, 2.6 + Math.random() * 3.4, 0.75, 0.45, 0.38, 0, 0);
            }
        }
    }
    // ---------------- HUD ----------------
    hud() {
        const e = this.energy / 100;
        const fill = $("energy-fill");
        fill.style.width = `${Math.max(0, this.energy)}%`;
        fill.style.filter = `hue-rotate(${(0, types_1.lerp)(0, -50, this.energy < 30 ? (30 - this.energy) / 30 : 0)}deg)`;
        $("energy").classList.toggle("low", this.energy < 28);
        const kmh = Math.round(this.player.vz * 2.2);
        $("speedo-fill").style.height = `${(0, types_1.clamp)((this.player.vz - 26) / (types_1.CFG.BOOST_SPEED - 26), 0, 1) * 100}%`;
        $("speedo-num").textContent = kmh.toString().padStart(3, "0");
        $("mult-n").textContent = Math.max(1, this.stats.mult).toString();
        $("mult-x").textContent = "x";
        $("score").textContent = fmt(this.stats.score);
        if (this.multPulseT > 0) {
            this.multPulseT -= 0.016;
            const m = $("mult");
            if (this.multPulseT > 0.9)
                m.classList.add("pulse", "hot");
            else
                m.classList.remove("pulse", "hot");
        }
        if (this.multPulseT <= 0 && this.multPulseT > -0.1) {
            $("mult").classList.remove("pulse", "hot");
        }
        const t = this.turbo / 100;
        $("turbo-fill").style.width = `${this.turbo}%`;
        $("turbo-wrap").classList.toggle("ready", this.turbo >= 100 || this.boostOn);
        void t;
        // near badge
        if (this.multPulseT > 0.4) {
            const b = $("near-badge");
            if (!b.classList.contains("show")) {
                b.classList.remove("show");
                void b.offsetWidth;
                b.classList.add("show");
            }
        }
        // tints
        const hurt = this.player.wallFlash * 0.85 + (this.energy < 22 ? (22 - this.energy) / 22 * 0.5 : 0);
        $("tint").style.opacity = (0, types_1.clamp)(hurt, 0, 0.9).toFixed(3);
        const boostGlow = this.player.boosting ? 0.55 + Math.sin(this.time * 20) * 0.1 : 0;
        $("boostTint").style.opacity = boostGlow.toFixed(3);
        // letterbox
        const h = (0, types_1.lerp)(6, 16, this.player.speedNorm);
        $("bar-top").style.height = `${h}vh`;
        $("bar-bottom").style.height = `${h}vh`;
        // orb pulse ring (visual flourish: energy bar flashes on pickup)
        if (this.orbPulse > 0)
            this.orbPulse = Math.max(0, this.orbPulse - 0.04);
    }
    // ---------------- loop ----------------
    onResize() {
        const w = window.innerWidth, h = window.innerHeight;
        this.H.camera.aspect = w / h;
        this.H.camera.updateProjectionMatrix();
        this.H.renderer.setSize(w, h);
    }
    tick() {
        const now = performance.now();
        let dt = (now - this.lastT) / 1000;
        this.lastT = now;
        dt = (0, types_1.clamp)(dt, 0, 0.05);
        this.time += dt;
        const { ix, iy } = this.inputAxis();
        this.boostHeld = !!(this.keys["shift"] || this.keys[" "]);
        const cruise = (0, types_1.cruiseTarget)(this.stats.score);
        if (this.phase === "playing") {
            const p = this.player;
            // boost logic
            const wantBoost = this.boostHeld && this.energy > types_1.CFG.BOOST_MIN_ENERGY && p.vz > cruise * 0.5;
            if (wantBoost && !this.boostOn) {
                this.boostOn = true;
                audio_1.sfx.turbo(true);
            }
            if ((!wantBoost || this.energy <= types_1.CFG.BOOST_MIN_ENERGY) && this.boostOn) {
                this.boostOn = false;
                audio_1.sfx.turbo(false);
            }
            if (this.boostOn)
                this.energy = Math.max(0, this.energy - types_1.CFG.BOOST_DRAIN * dt);
            p.update(dt, ix, iy, cruise, this.boostOn, this.energy);
            // energy decay — running dry ends the run
            this.energy = Math.max(0, this.energy - types_1.CFG.ENERGY_DECAY * dt);
            if (this.energy <= 0) {
                this.die();
            }
            // turbo meter
            if (Math.abs(ix) < 0.15 && Math.abs(iy) < 0.15)
                this.turbo = Math.min(100, this.turbo + dt * 3.2);
            else
                this.turbo = Math.max(0, this.turbo - dt * 1.6);
            // scoring by speed
            this.stats.score += this.player.vz * dt * types_1.CFG.SCORE_SPEED * this.stats.mult;
            this.stats.distance += this.player.vz * dt;
            this.stats.time += dt;
            this.stats.maxSpeed = Math.max(this.stats.maxSpeed, this.player.vz * 2.2);
            // world gen + cull
            this.world.score = this.stats.score;
            this.world.ensure(p.pos.z, types_1.CFG.SPAWN_AHEAD);
            this.world.cull(p.pos.z);
            // collisions
            const inv = this.collectInv();
            this.handleCollisions(inv);
            if (this.phase === "playing")
                this.emitFx(dt);
            // camera + light rig
            const shake = this.shake + (p.boosting ? 0.06 : 0);
            p.updateCamera(this.H.camera, dt, shake, 70, 92, this.time);
            this.shake *= Math.pow(0.02, dt);
            const H = this.H;
            H.sun.position.set(-180, 130, p.pos.z + 20);
            H.sun.target.position.set(0, 0, p.pos.z - 260);
            H.sunDisc.position.z = p.pos.z + types_1.CFG.SUN_Z;
            H.corona.position.z = p.pos.z + types_1.CFG.SUN_Z + 100;
            // orb spin + pulse
            const om = this.world.orbMat;
            om.emissiveIntensity = 1.5 + Math.sin(this.time * 5) * 0.5 + this.orbPulse * 2;
            for (const c of this.world.chunks) {
                for (const sp of c.spheres) {
                    if (sp.type === "orb" && sp.mesh && sp.mesh.visible) {
                        sp.mesh.rotation.y += dt * 2;
                        sp.mesh.rotation.x += dt * 1.3;
                    }
                }
            }
        }
        else if (this.phase === "menu") {
            // idle fly-through
            this.menuFly(dt);
        }
        else if (this.phase === "dead" && this.player.deadT < 6) {
            this.player.update(dt, 0, 0, 0, false, 0);
            this.player.updateCamera(this.H.camera, dt, this.shake, 70, 92, this.time);
            this.shake *= Math.pow(0.02, dt);
        }
        // audio + particles always
        audio_1.sfx.setEngine(this.player.speedNorm, this.player.boosting, this.phase === "playing");
        this.parts.update(dt);
        this.hud();
        this.H.renderer.render(this.H.scene, this.H.camera);
    }
    menuFly(dt) {
        const p = this.player;
        if (p.pos.z > -2600)
            p.pos.z -= 42 * dt;
        else
            p.pos.z = 0;
        p.pos.x = Math.sin(this.time * 0.32) * 12;
        p.pos.y = 3 + Math.sin(this.time * 0.5) * 3;
        p.vx = Math.cos(this.time * 0.32) * 12 * 0.32;
        p.vy = Math.cos(this.time * 0.5) * 1.5;
        p.vz = 42;
        p.speedNorm = 0.2;
        p.updateCamera(this.H.camera, dt, 0, 66, 72, this.time);
        this.world.score = 0;
        this.world.ensure(p.pos.z, types_1.CFG.SPAWN_AHEAD);
        this.world.cull(p.pos.z);
        const H = this.H;
        H.sun.position.set(-180, 130, p.pos.z + 20);
        H.sun.target.position.set(0, 0, p.pos.z - 260);
        H.sunDisc.position.z = p.pos.z + types_1.CFG.SUN_Z;
        H.corona.position.z = p.pos.z + types_1.CFG.SUN_Z + 100;
    }
}
exports.Game = Game;
function boot() {
    const g = new Game();
    g.setPhasePublic("menu");
    window.__game = g;
    const loop = () => {
        g.tick();
        requestAnimationFrame(loop);
    };
    requestAnimationFrame(loop);
    return g;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"78d09537-5866-4bd7-83cf-dc6b549dbcac","tokensIn":7465521,"tokensOut":231636,"tokensTotal":7697157,"cost":0,"turns":110,"toolCalls":160,"failedToolCalls":2,"timestamp":"2026-08-15T13:06:34.967Z"} -->