← Canyon Run results

Canyon Run

Deepseek v4 Flash 0731 · 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 52,486 bytes · SHA-256 4b18cc3b0ba9
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CANYON RUN — High-Speed Procedural Canyon Racer</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
<style>
/* ── css: base ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #12040f; }
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; color: #fff; }
#app { position: fixed; inset: 0; }
canvas { display: block; }
.hidden { display: none !important; }

#vignette {
  position: fixed; inset: 0; pointer-events: none; z-index: 5;
  background: radial-gradient(ellipse at center, rgba(0,0,0,0) 45%, rgba(10,0,8,0.55) 100%);
}

#hud { position: fixed; inset: 0; pointer-events: none; z-index: 10; }
/* ── css: hud ── */
#score-wrap {
  position: absolute; top: 20px; left: 26px;
  text-shadow: 0 0 18px rgba(255,150,60,0.5);
}
#score-label { font-size: 11px; letter-spacing: 3px; color: rgba(255,255,255,0.55); }
#score { font-size: 34px; font-weight: 700; letter-spacing: 1px; color: #ffe9c9;
  font-variant-numeric: tabular-nums; }

#mult-wrap {
  position: absolute; top: 18px; right: 30px; text-align: right;
}
#mult { font-size: 46px; font-weight: 800; color: #ffd23f;
  text-shadow: 0 0 22px rgba(255,180,40,0.85); transition: transform 0.12s;
  font-variant-numeric: tabular-nums; }
#mult.hot { transform: scale(1.45); color: #ff8a3d; text-shadow: 0 0 30px rgba(255,120,40,1); }
#mult-label { font-size: 10px; letter-spacing: 3px; color: rgba(255,255,255,0.5); }

#nearflash {
  position: absolute; left: 50%; top: 30%; transform: translate(-50%, -50%);
  font-size: 40px; font-weight: 800; letter-spacing: 6px; color: #ffd23f;
  text-shadow: 0 0 30px rgba(255,170,40,0.9), 0 0 8px rgba(255,60,40,0.7);
  opacity: 0; transition: opacity 0.1s;
}
#nearflash.show { opacity: 1; }

#crosshair { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); }
.ch-ring { width: 26px; height: 26px; border: 1px solid rgba(255,255,255,0.4); border-radius: 50%;
  position: absolute; left: -13px; top: -13px; opacity: 0.7; }
.ch-dot { width: 3px; height: 3px; background: rgba(255,255,255,0.7); border-radius: 50%;
  position: absolute; left: -1.5px; top: -1.5px; }

#bottom {
  position: absolute; left: 30px; right: 30px; bottom: 26px;
  display: flex; align-items: flex-end; justify-content: space-between; gap: 30px;
}
#energy-wrap { flex: 1; max-width: 380px; }
#speed-wrap { text-align: center; }
#turbo-wrap { flex: 1; max-width: 380px; text-align: right; }

#energy-label, #turbo-label { font-size: 9px; letter-spacing: 3px; color: rgba(255,255,255,0.45); margin-bottom: 4px; }
#speed-num { font-size: 20px; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums;
  text-shadow: 0 0 12px rgba(255,255,255,0.4); }

.bar { height: 6px; border-radius: 4px; background: rgba(255,255,255,0.09); overflow: hidden; }
#energy-bar { width: 100%; }
#turbo-bar { width: 100%; }
#speed-bar { width: 210px; margin: 0 auto; background: linear-gradient(90deg, rgba(120,60,255,0.2), rgba(255,255,255,0.12)); }

#energy-fill { height: 100%; width: 100%; border-radius: 4px;
  background: linear-gradient(90deg, #ff5a3d, #ffd23f, #aef066);
  box-shadow: 0 0 14px rgba(255,120,60,0.9); transition: width 0.15s linear; }
#energy-fill.low { background: linear-gradient(90deg, #ff2d2d, #ff6a3d); box-shadow: 0 0 16px rgba(255,40,40,1); }
#speed-fill { height: 100%; width: 20%; border-radius: 4px;
  background: linear-gradient(90deg, #3aa0ff, #7f7bff, #ff9af0); box-shadow: 0 0 12px rgba(120,140,255,0.8); }
#turbo-fill { height: 100%; width: 0%; border-radius: 4px;
  background: linear-gradient(90deg, #ff7b2d, #ffd23f); box-shadow: 0 0 14px rgba(255,150,40,0.9); }
#turbo-fill.full { background: linear-gradient(90deg, #ff4d4d, #ffd23f); animation: turboPulse 0.5s infinite alternate; }
@keyframes turboPulse { from { filter: brightness(1); } to { filter: brightness(1.8); } }
/* ── css: overlays ── */
#intro, #gameover {
  position: fixed; inset: 0; z-index: 20; pointer-events: auto;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  background: radial-gradient(ellipse at 50% 30%, rgba(20,6,26,0.85), rgba(6,1,8,0.97));
  backdrop-filter: blur(3px);
}
#intro-title {
  font-size: 74px; font-weight: 900; letter-spacing: 14px; color: #ffe9c9;
  text-shadow: 0 0 40px rgba(255,140,40,0.8), 0 0 90px rgba(255,80,60,0.5);
  margin-bottom: 4px;
}
#intro-sub { font-size: 13px; letter-spacing: 7px; color: rgba(255,255,255,0.55); margin-bottom: 42px; }
#intro-controls { text-align: center; color: rgba(255,255,255,0.75); font-size: 15px; margin-bottom: 44px; line-height: 1.7; }
#intro-controls p span { color: #ffd23f; font-weight: 700; }
.intro-gold { color: rgba(255,255,255,0.6); font-size: 13px; }
.orb { color: #aef066; text-shadow: 0 0 10px #aef066; }
@media (max-height: 700px) { #intro-title { font-size: 52px; } }

#start-btn, #restart-btn {
  padding: 16px 52px; font-size: 18px; font-weight: 800; letter-spacing: 4px; color: #16050f;
  background: linear-gradient(90deg, #ffd23f, #ff7b2d); border: none; cursor: pointer;
  border-radius: 6px; box-shadow: 0 0 30px rgba(255,140,40,0.6);
  transition: transform 0.12s;
}
#start-btn:hover, #restart-btn:hover { transform: scale(1.06); }
#best { margin-top: 22px; color: rgba(255,255,255,0.5); letter-spacing: 2px; font-size: 13px; }

#gameover { background: radial-gradient(ellipse at 50% 30%, rgba(20,4,10,0.9), rgba(5,0,6,0.98)); }
#go-title { font-size: 60px; font-weight: 900; letter-spacing: 10px; color: #ff5a3d;
  text-shadow: 0 0 30px rgba(255,60,40,0.8); margin-bottom: 30px; }
#go-stats { display: flex; gap: 46px; margin-bottom: 34px; text-align: center; }
#go-stats div span { display: block; font-size: 10px; letter-spacing: 3px; color: rgba(255,255,255,0.5); margin-bottom: 6px; }
#go-stats b { font-size: 40px; font-weight: 800; color: #ffe9c9; font-variant-numeric: tabular-nums; }
#go-best { color: rgba(255,255,255,0.55); letter-spacing: 2px; margin-bottom: 30px; font-size: 13px; }
</style>
</head>
<body>
<div id="app">
  <div id="hud">
    <div id="score-wrap">
      <div id="score-label">SCORE</div>
      <div id="score">0</div>
    </div>
    <div id="mult-wrap">
      <div id="mult">x1</div>
      <div id="mult-label">MULTIPLIER</div>
    </div>
    <div id="nearflash">NEAR MISS</div>
    <div id="crosshair"><div class="ch-ring"></div><div class="ch-dot"></div></div>
    <div id="bottom">
      <div id="energy-wrap" title="Energy">
        <div id="energy-label">ENERGY</div>
        <div id="energy-bar"><div id="energy-fill"></div></div>
      </div>
      <div id="speed-wrap" title="Speed">
        <div id="speed-num">000</div>
        <div id="speed-bar"><div id="speed-fill"></div></div>
      </div>
      <div id="turbo-wrap" title="Turbo [SHIFT to boost]">
        <div id="turbo-label">TURBO</div>
        <div id="turbo-bar"><div id="turbo-fill"></div></div>
      </div>
    </div>
  </div>

  <div id="intro">
    <div id="intro-title">CANYON RUN</div>
    <div id="intro-sub">HIGH-SPEED PROCEDURAL CANYON RACER</div>
    <div id="intro-controls">
      <p><span>A / D</span> or <span>◄ ►</span> — Bank &amp; steer</p>
      <p><span>W / S</span> or <span>▲ ▼</span> — Pitch &amp; dive</p>
      <p><span>SHIFT</span> — Turbo boost</p>
      <p class="intro-gold">Fly close to walls &amp; squeeze gaps for NEAR-MISS multipliers.<br>Collect <span class="orb">◆</span> orbs to stay fueled. Don't run out of energy.</p>
    </div>
    <div id="start-btn">START RUNNING</div>
    <div id="best"></div>
  </div>

  <div id="gameover" class="hidden">
    <div id="go-title">CRASHED</div>
    <div id="go-stats">
      <div><span>FINAL SCORE</span><b id="go-score">0</b></div>
      <div><span>MAX SPEED</span><b id="go-speed">000</b></div>
      <div><span>TIME</span><b id="go-time">0.0s</b></div>
    </div>
    <div id="go-best">Best: 0</div>
    <div id="restart-btn">RESTART</div>
  </div>

  <div id="vignette"></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./input":"src/input.ts","./audio":"src/audio.ts","./world":"src/world.ts","./player":"src/player.ts","./effects":"src/effects.ts","./hud":"src/hud.ts"},"src/world.ts":{"./state":"src/state.ts"},"src/player.ts":{"./state":"src/state.ts","./input":"src/input.ts"},"src/hud.ts":{"./state":"src/state.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";
// Entry: orchestrates scene, systems and the game loop.
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const input_1 = require("./input");
const audio_1 = require("./audio");
const world_1 = require("./world");
const player_1 = require("./player");
const effects_1 = require("./effects");
const hud_1 = require("./hud");
const app = document.getElementById('app');
const scene = new THREE.Scene();
(0, world_1.buildBackdrop)(scene);
const world = new world_1.World(scene);
world.update(0, 0.016); // seed a scenic canyon behind the intro screen
const player = new player_1.Player();
scene.add(player.group);
const effects = new effects_1.Effects(scene, app);
let D = 0;
let last = performance.now();
function start() {
    audio_1.audio.init();
    audio_1.audio.startEngine();
    (0, state_1.resetRun)();
    D = 0;
    world.reset();
    player.reset();
    state_1.S.running = true;
    document.getElementById('intro').classList.add('hidden');
    document.getElementById('gameover').classList.add('hidden');
    last = performance.now();
}
function gameOver() {
    state_1.S.running = false;
    state_1.S.boosting = false;
    audio_1.audio.crash();
    audio_1.audio.stopEngine();
    (0, hud_1.saveBest)();
    (0, hud_1.showGameOver)();
}
function onNearMiss() {
    state_1.S.multiplier = Math.min(10, state_1.S.multiplier + 1);
    state_1.S.multTimer = 3.0;
    state_1.S.turbo = Math.min(1, state_1.S.turbo + 0.14);
    audio_1.audio.whoosh();
}
function tick(now) {
    const dt = Math.min(0.05, (now - last) / 1000);
    last = now;
    const input = (0, input_1.getInput)();
    if (state_1.S.running) {
        state_1.S.time += dt;
        // difficulty ramp
        state_1.S.speedFactor = 1 + state_1.S.time * 0.011;
        const cap = 4.2;
        if (state_1.S.speedFactor > cap)
            state_1.S.speedFactor = cap;
        // boost state: once active, stays on until turbo drains; starts when full
        const wantBoost = state_1.S.boosting
            ? (input.boost && state_1.S.turbo > 0)
            : (input.boost && state_1.S.turbo >= 1);
        state_1.S.boosting = wantBoost;
        if (state_1.S.boosting) {
            state_1.S.turbo -= state_1.CFG.boostCost * dt / 100;
            if (state_1.S.turbo < 0)
                state_1.S.turbo = 0;
        }
        // velocity toward target
        const cruise = state_1.CFG.startSpeed * (0.7 + 0.32 * (state_1.S.speedFactor - 1));
        let target = cruise;
        if (state_1.S.boosting)
            target = Math.min(state_1.CFG.maxSpeed, cruise * 2.0);
        const boostLerp = state_1.S.boosting ? 3.2 : 1.4;
        state_1.S.velocity += (target - state_1.S.velocity) * Math.min(1, dt * boostLerp);
        state_1.S.maxSpeedReached = Math.max(state_1.S.maxSpeedReached, state_1.S.velocity);
        // travel + energy + score
        const travel = state_1.S.velocity * dt;
        D += travel;
        state_1.S.energy -= (state_1.CFG.energyDrain * (0.5 + state_1.S.speedFactor * 0.5) + (state_1.S.boosting ? 3.0 : 0)) * dt;
        (0, state_1.addScore)(state_1.S.velocity * state_1.S.multiplier * dt * 0.5);
        // multiplier decay
        if (state_1.S.multiplier > 1) {
            state_1.S.multTimer -= dt;
            if (state_1.S.multTimer <= 0) {
                state_1.S.multiplier = Math.max(1, state_1.S.multiplier - 1);
                state_1.S.multTimer = 2.2;
            }
        }
        // physics
        player.update(dt);
        // collisions + near miss
        const p = player.group.position;
        const res = world.collide(p.x, p.y, player.playerRadius, D);
        if (state_1.S.nearMissFlag)
            onNearMiss();
        // collect orbs
        const taken = world.collectOrbs(p.x, p.y, player.playerRadius, D);
        if (taken > 0) {
            state_1.S.energy = Math.min(100, state_1.S.energy + state_1.CFG.orbHeal * taken);
            state_1.S.turbo = Math.min(1, state_1.S.turbo + 0.08 * taken);
            audio_1.audio.ping(Math.floor(state_1.S.score / 500 + taken));
        }
        player.frameUpdate();
        // lose fuel or crash
        if (state_1.S.energy <= 0) {
            state_1.S.energy = 0;
            gameOver();
        }
        else if (res.crash) {
            gameOver();
        }
        if (state_1.S.running) {
            // camera shake: strongest when skimming obstacles
            const near01 = Math.max(0, Math.min(1, (state_1.CFG.nearMissDist - res.proximity) / state_1.CFG.nearMissDist));
            const shake = near01 * 1.5 + (state_1.S.boosting ? 0.5 : 0) + (0, hud_1.getShakeAdd)();
            const s01 = Math.min(1, state_1.S.velocity / state_1.CFG.maxSpeed);
            effects.update(dt, { px: p.x, py: p.y, speed01: s01, boost: state_1.S.boosting ? 1 : 0, shake });
            audio_1.audio.engineUpdate(s01, state_1.S.boosting ? 1 : 0);
            world.update(D, dt);
            (0, hud_1.updateHUD)(dt);
        }
    }
    effects.render();
    requestAnimationFrame(tick);
}
// wire restart + start, then show intro
(0, input_1.initInput)();
(0, hud_1.initHUD)(start, start);
(0, hud_1.showIntro)();
audio_1.audio.init();
// background render on intro (idle world)
const introCamIdle = () => {
    requestAnimationFrame(tick);
};
introCamIdle();
window.addEventListener('resize', () => effects.resize());
// test/debug handles
window.__S = state_1.S;
window.__world = world;
window.__player = player;
window.__D = () => D;
window.__effects = effects;
window.__start = start;
window.__gameover = gameOver;
window.__setInput = { boost: input_1._testSetBoost, x: input_1._testSetX, y: input_1._testSetY };
window.__getInput = input_1.getInput;
};

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

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// Central game state. Single source of truth for score, energy, multiplier, turbo, difficulty.
Object.defineProperty(exports, "__esModule", { value: true });
exports.S = exports.CFG = void 0;
exports.newRun = newRun;
exports.resetRun = resetRun;
exports.addScore = addScore;
exports.CFG = {
    startSpeed: 60, // forward world-units per second
    maxSpeed: 240,
    energyDrain: 4.2, // per second at 1x speed factor
    orbHeal: 26, // energy gained per orb
    boostCost: 20, // turbo drain per second while boosting
    turboGainNearMiss: 14,
    turboGainOrb: 10,
    turnRate: 34, // lateral steering units / sec
    pitchRate: 30,
    bankLean: 0.9,
    nearMissDist: 13, // closest allowed passthrough before touching = near miss
    playerRadius: 2.2,
};
function newRun() {
    return {
        running: false,
        score: 0,
        energy: 100,
        multiplier: 1,
        multTimer: 0,
        maxMult: 1,
        turbo: 0,
        boosting: false,
        nearMissFlag: false,
        speedFactor: 1,
        velocity: exports.CFG.startSpeed,
        maxSpeedReached: 0,
        time: 0,
        distTravelled: 0,
    };
}
exports.S = newRun();
/** Reset all run metrics (keeps best-score store in localStorage). */
function resetRun() {
    const s = newRun();
    Object.assign(exports.S, s);
}
function addScore(points) {
    exports.S.score += Math.round(points);
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// Procedural Web Audio: engine hum, orb ping, near-miss whoosh, turbo, crash.
Object.defineProperty(exports, "__esModule", { value: true });
exports.audio = void 0;
class AudioManager {
    constructor() {
        this.ctx = null;
        this.master = null;
        this.engineOsc = null;
        this.engineGain = null;
        this.engineFilter = null;
        this.noiseBuf = null;
        this.enabled = true;
    }
    init() {
        if (this.ctx) {
            this.ctx.resume();
            return;
        }
        try {
            const AC = window.AudioContext || window.webkitAudioContext;
            const ac = new AC();
            this.ctx = ac;
            const m = ac.createGain();
            this.master = m;
            m.gain.value = 0.6;
            m.connect(ac.destination);
            this.noiseBuf = this.makeNoise(1.5);
        }
        catch {
            this.ctx = null;
        }
    }
    resume() { this.ctx && this.ctx.resume(); }
    makeNoise(seconds) {
        const ctx = this.ctx;
        const len = Math.floor(ctx.sampleRate * seconds);
        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;
        return buf;
    }
    startEngine() {
        if (!this.ctx || !this.master || this.engineOsc)
            return;
        const ctx = this.ctx;
        this.engineOsc = ctx.createOscillator();
        this.engineOsc.type = 'sawtooth';
        this.engineOsc.frequency.value = 55;
        this.engineFilter = ctx.createBiquadFilter();
        this.engineFilter.type = 'lowpass';
        this.engineFilter.frequency.value = 320;
        this.engineGain = ctx.createGain();
        this.engineGain.gain.value = 0.0;
        this.engineOsc.connect(this.engineFilter).connect(this.engineGain).connect(this.master);
        this.engineOsc.start();
    }
    /** speed01 0..1, boost 0..1 */
    engineUpdate(speed01, boost) {
        if (!this.ctx || !this.engineOsc || !this.engineGain || !this.engineFilter)
            return;
        const t = this.ctx.currentTime;
        this.engineOsc.frequency.setTargetAtTime(48 + speed01 * 120 + boost * 90, t, 0.1);
        this.engineFilter.frequency.setTargetAtTime(260 + speed01 * 900 + boost * 2200, t, 0.12);
        this.engineGain.gain.setTargetAtTime(0.05 + speed01 * 0.05 + boost * 0.06, t, 0.1);
    }
    stopEngine() {
        if (!this.ctx || !this.engineOsc)
            return;
        try {
            this.engineOsc.stop();
        }
        catch { /* noop */ }
        this.engineOsc = null;
        this.engineGain = null;
        this.engineFilter = null;
    }
    blip(freq, dur, type, vol, bend = 0) {
        if (!this.ctx || !this.master)
            return;
        const ctx = this.ctx;
        const t = ctx.currentTime;
        const osc = ctx.createOscillator();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, t);
        if (bend)
            osc.frequency.exponentialRampToValueAtTime(Math.max(20, freq + bend), t + dur);
        const g = ctx.createGain();
        g.gain.setValueAtTime(0, t);
        g.gain.linearRampToValueAtTime(vol, t + 0.012);
        g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
        osc.connect(g).connect(this.master);
        osc.start(t);
        osc.stop(t + dur + 0.02);
    }
    ping(step) {
        const f = 620 + (step % 5) * 120;
        this.blip(f, 0.16, 'sine', 0.4, 240);
        this.blip(f * 1.5, 0.24, 'sine', 0.2, 300);
    }
    whoosh() {
        if (!this.ctx || !this.master || !this.noiseBuf)
            return;
        const ctx = this.ctx;
        const t = ctx.currentTime;
        const src = ctx.createBufferSource();
        src.buffer = this.noiseBuf;
        const bp = ctx.createBiquadFilter();
        bp.type = 'bandpass';
        bp.frequency.setValueAtTime(400, t);
        bp.frequency.exponentialRampToValueAtTime(3200, t + 0.18);
        bp.Q.value = 1.2;
        const g = ctx.createGain();
        g.gain.setValueAtTime(0.0001, t);
        g.gain.linearRampToValueAtTime(0.55, t + 0.05);
        g.gain.exponentialRampToValueAtTime(0.0001, t + 0.6);
        src.connect(bp).connect(g).connect(this.master);
        src.start(t);
        src.stop(t + 0.7);
    }
    boostStart() {
        this.whoosh();
        this.blip(220, 0.5, 'sawtooth', 0.18, 900);
    }
    boostEnd() {
        this.blip(500, 0.3, 'sine', 0.15, -300);
    }
    crash() {
        if (!this.ctx || !this.master || !this.noiseBuf)
            return;
        const ctx = this.ctx;
        const t = ctx.currentTime;
        const src = ctx.createBufferSource();
        src.buffer = this.noiseBuf;
        const lp = ctx.createBiquadFilter();
        lp.type = 'lowpass';
        lp.frequency.value = 900;
        const g = ctx.createGain();
        g.gain.setValueAtTime(0.9, t);
        g.gain.exponentialRampToValueAtTime(0.0001, t + 0.9);
        src.connect(lp).connect(g).connect(this.master);
        src.start(t);
        src.stop(t + 1.0);
        this.blip(90, 0.7, 'triangle', 0.5, -50);
    }
}
exports.audio = new AudioManager();
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// Keyboard input. Tracks directional axes and boost.
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.getInput = getInput;
exports._testSetBoost = _testSetBoost;
exports._testSetX = _testSetX;
exports._testSetY = _testSetY;
const inp = { x: 0, y: 0, boost: false };
const keys = new Set();
function codeToKey(e) {
    return e.code || '';
}
function updateAxes() {
    let x = 0, y = 0;
    if (keys.has('ArrowLeft') || keys.has('KeyA'))
        x -= 1;
    if (keys.has('ArrowRight') || keys.has('KeyD'))
        x += 1;
    if (keys.has('ArrowUp') || keys.has('KeyW'))
        y -= 1; // up = dive? invert handled in player
    if (keys.has('ArrowDown') || keys.has('KeyS'))
        y += 1;
    inp.x = x;
    inp.y = y;
    inp.boost = keys.has('ShiftLeft') || keys.has('ShiftRight');
}
function initInput() {
    window.addEventListener('keydown', (e) => {
        keys.add(codeToKey(e));
        if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Space'].includes(e.code))
            e.preventDefault();
        updateAxes();
    });
    window.addEventListener('keyup', (e) => {
        keys.delete(codeToKey(e));
        updateAxes();
    });
    window.addEventListener('blur', () => {
        keys.clear();
        updateAxes();
    });
}
function getInput() { return inp; }
function _testSetBoost(b) { inp.boost = b; }
function _testSetX(x) { inp.x = x; }
function _testSetY(y) { inp.y = y; }
};

// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
// Procedural chunk-based canyon generator with collision/orb querying.
// Coordinate model: player stays at z=0 and the world scrolls toward +z.
// D = total distance travelled. An object with fixed 'ahead' a sits at world z = a - D,
// so it reaches the player plane when D === a.
Object.defineProperty(exports, "__esModule", { value: true });
exports.World = exports.L = void 0;
exports.halfWidth = halfWidth;
exports.buildBackdrop = buildBackdrop;
const state_1 = require("./state");
exports.L = 150; // chunk depth (in ahead-units)
const HORIZON = 3.2; // chunks kept ahead of player
const FLOOR = -13;
const CEIL = 34;
const WALL_H = 60;
const WALL_COL = 0x3a2360;
const WALL_COL2 = 0x54296a;
const MONO_COL = 0xd97a2e;
const MONO_COL2 = 0xc26033;
const GATE_COL = 0x241a3e;
const ORB_COL = 0x8affb0;
function halfWidth(ahead) {
    // Continuous gentle corridor wander giving a "sweeping canyon" feel.
    const base = 40;
    return base
        + 7 * Math.sin(ahead * 0.0028)
        + 4 * Math.sin(ahead * 0.00082 + 1.7)
        + 2.5 * Math.sin(ahead * 0.0061);
}
function rand(a, b) { return a + Math.random() * (b - a); }
function pick(arr) { return arr[(Math.random() * arr.length) | 0]; }
class World {
    constructor(scene) {
        this.root = new THREE.Group();
        this.chunks = [];
        this.obstacles = [];
        this.orbs = [];
        this.nextI = 0;
        // shared materials / geometries
        this.wallMatL = new THREE.MeshLambertMaterial({ color: WALL_COL });
        this.wallMatR = new THREE.MeshLambertMaterial({ color: WALL_COL2 });
        this.monoMat = new THREE.MeshLambertMaterial({ color: MONO_COL });
        this.monoMat2 = new THREE.MeshLambertMaterial({ color: MONO_COL2 });
        this.gateMat = new THREE.MeshLambertMaterial({ color: GATE_COL });
        this.orbGeo = new THREE.OctahedronGeometry(1.05, 0);
        this.scene = scene;
        scene.add(this.root);
        this.orbMat = new THREE.MeshBasicMaterial({ color: ORB_COL });
        this.orbMat.transparent = true;
        this.orbMat.opacity = 0.95;
    }
    reset() {
        for (const c of this.chunks)
            this.root.remove(c.group);
        this.chunks = [];
        this.obstacles = [];
        this.orbs = [];
        this.nextI = 0;
    }
    /** Call each frame with the total distance travelled D. */
    update(D, dt) {
        // spawn chunks farther ahead than we have built
        while ((this.nextI + 1) * exports.L <= D + HORIZON * exports.L) {
            this.spawnChunk(this.nextI, D);
            this.nextI++;
        }
        // scroll + prune
        for (let c = this.chunks.length - 1; c >= 0; c--) {
            const ch = this.chunks[c];
            ch.group.position.z = (ch.i * exports.L + exports.L / 2) - D;
            if (ch.i * exports.L < D - 24) {
                this.root.remove(ch.group);
                this.chunks.splice(c, 1);
            }
        }
        // prune orphan data
        const minKeep = Math.max(-1, Math.floor((D - 30) / exports.L));
        this.obstacles = this.obstacles.filter(o => o.i >= minKeep);
        this.orbs = this.orbs.filter(o => o.i >= minKeep);
        // gentle orb idle spin
        for (const o of this.orbs)
            if (o.mat)
                o.mat.rotation.y += dt * 2;
    }
    spawnChunk(i, D) {
        const g = new THREE.Group();
        const diff = Math.max(0, state_1.S.speedFactor - 1); // 0..~3
        const gapW = Math.max(16, 30 - diff * 3.2); // gate gap narrows with difficulty
        this.buildWalls(g, i, D);
        let z = 24 + rand(0, 20);
        while (z < exports.L - 16) {
            const r = Math.random();
            if (r < 0.34 + diff * 0.06) {
                this.addMonolith(g, i, z, diff);
            }
            else if (r < 0.34 + diff * 0.06 + 0.24) {
                this.addGate(g, i, z, gapW);
            }
            else {
                this.addMonolith(g, i, z, diff * 0.6);
            }
            z += rand(26, 52) + diff * 6;
        }
        this.buildOrbs(g, i, D);
        this.root.add(g);
        this.chunks.push({ i, group: g });
    }
    buildWalls(g, i, _D) {
        const c = i * exports.L + exports.L / 2;
        const hl = halfWidth(i * exports.L);
        const hr = halfWidth((i + 1) * exports.L);
        const hMid = (hl + hr) / 2;
        const depth = exports.L + 4;
        const mk = (mat, x0) => {
            const box = new THREE.BoxGeometry(7, WALL_H, depth);
            const m = new THREE.Mesh(box, mat);
            m.position.set(x0, FLOOR + WALL_H / 2, 0);
            m.castShadow = true;
            m.receiveShadow = true;
            g.add(m);
            // jagged silhouette peaks
            for (let k = 0; k < 3; k++) {
                const w = 5 + rand(0, 8);
                const h = rand(8, 22);
                const pk = new THREE.Mesh(new THREE.BoxGeometry(w, h, rand(12, 22)), mat);
                pk.position.set(x0 + rand(-6, 6), FLOOR + WALL_H + h / 2, rand(-exports.L / 2, exports.L / 2));
                g.add(pk);
            }
        };
        mk(this.wallMatL, -(Math.min(hl, hr)) - 3.5);
        mk(this.wallMatR, (Math.min(hl, hr)) + 3.5);
        void hMid;
    }
    addMonolith(g, i, a, diff) {
        const hw = halfWidth(i * exports.L);
        const half = Math.max(8, hw - 6);
        let x = rand(-half, half);
        const w = rand(3, 5.5);
        const h = rand(9, 20);
        const y = rand(FLOOR + 3, CEIL - 3);
        if (Math.abs(x) < w + 5)
            x = (x < 0 ? -1 : 1) * (w + 6 + rand(0, 4));
        const mat = Math.random() < 0.5 ? this.monoMat : this.monoMat2;
        const geo = Math.random() < 0.7
            ? new THREE.BoxGeometry(w, h, w * rand(0.8, 1.4))
            : new THREE.OctahedronGeometry(rand(3, 6), 0);
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.set(x, y, a - exports.L / 2);
        mesh.rotation.set(rand(0, 0.4), rand(0, 3.1), rand(0, 0.4));
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        g.add(mesh);
        this.obstacles.push({ i, a, x, y, hx: w * 1.1, hy: h * 0.55, hz: w * 1.1, kind: 'mono', near: false });
        void diff;
    }
    addGate(g, i, a, gapW) {
        const hw = halfWidth(i * exports.L);
        const half = gapW / 2;
        const p = i * exports.L + a;
        const tall = 40;
        for (const side of [-1, 1]) {
            const px = side * (half + 3.5);
            const m = new THREE.Mesh(new THREE.BoxGeometry(7, tall, 3.5), this.gateMat);
            m.position.set(px, FLOOR + tall / 2, a - exports.L / 2);
            m.castShadow = true;
            m.receiveShadow = true;
            g.add(m);
            // glowing hoops at eye level
            const ring = new THREE.Mesh(new THREE.TorusGeometry(3.2, 0.35, 8, 20), new THREE.MeshBasicMaterial({ color: 0xffb158, transparent: true, opacity: 0.85 }));
            ring.position.set(px, rand(4, 10), a - exports.L / 2);
            ring.rotation.set(0, Math.PI / 2, 0);
            g.add(ring);
        }
        const yEye = rand(2, 12);
        this.obstacles.push({ i, a, x: half + 3.5, y: yEye, hx: 3.5, hy: 9, hz: 2, kind: 'gate', near: false });
        this.obstacles.push({ i, a, x: -(half + 3.5), y: yEye, hx: 3.5, hy: 9, hz: 2, kind: 'gate', near: false });
        // centre reward orb for threading the needle
        this.addOrbLocal(g, i, { i, a, x: 0, y: yEye + rand(0.5, 2), taken: false, mat: null });
        void p;
    }
    addOrbLocal(g, i, o) {
        const m = new THREE.Mesh(this.orbGeo, this.orbMat);
        m.position.set(o.x, o.y, o.a - exports.L / 2);
        g.add(m);
        o.mat = m;
        this.orbs.push(o);
    }
    buildOrbs(g, i, _D) {
        const hw = halfWidth(i * exports.L);
        const half = Math.max(6, hw - 8);
        const n = 8 + ((Math.random() * 6) | 0);
        let guard = 0;
        let placed = 0;
        while (placed < n && guard < 80) {
            guard++;
            const a = rand(8, exports.L - 8);
            const x = rand(-half, half);
            const y = rand(FLOOR + 6, CEIL - 6);
            if (this.overlapsObstacleAt(x, y, i, a, 2.6))
                continue;
            this.addOrbLocal(g, i, { i, a, x, y, taken: false, mat: null });
            placed++;
        }
    }
    overlapsObstacleAt(x, y, i, a, pad) {
        for (const o of this.obstacles) {
            if (o.i < i - 1 || o.i > i + 1)
                continue;
            const ox = o.x, oy = o.y;
            const oa = o.i * exports.L + o.a;
            const aThis = i * exports.L + a;
            if (Math.abs(oa - aThis) < o.hz + pad &&
                Math.abs(ox - x) < o.hx + pad &&
                Math.abs(oy - y) < o.hy + pad)
                return true;
        }
        return false;
    }
    // ---- collision / collection ----
    /**
     * Player sphere at (px,py) (pz=0), radius r.
     * Returns { crash, wallClear, proximity } where proximity = min distance to any obstacle
     * and wallClear = corridor clearance. Sets S.nearMissFlag if a near miss happens.
     */
    collide(px, py, r, D) {
        const hw = halfWidth(D);
        const wallClear = Math.abs(hw) - Math.abs(px); // distance to nearest wall
        let crash = false;
        let impossible = 1e9;
        if (wallClear < r)
            crash = true;
        if (py > CEIL - r)
            crash = true;
        if (py < FLOOR + r)
            crash = true;
        let proximity = 1e9;
        for (const o of this.obstacles) {
            if (o.i * exports.L + o.a < D - 40 || o.i * exports.L + o.a > D + HORIZON * exports.L)
                continue;
            const dist = sphereAABB(px, py, 0, r, o.x, o.y, o.i * exports.L + o.a - D, o.hx, o.hy, o.hz);
            proximity = Math.min(proximity, dist);
            if (dist < r) {
                crash = true;
                o.near = true;
            }
            else if (dist < r + state_1.CFG.nearMissDist && !o.near) {
                o.near = true;
                state_1.S.nearMissFlag = true;
            }
        }
        // wall near miss
        if (!crash && wallClear > r && wallClear < r + state_1.CFG.nearMissDist) {
            state_1.S.nearMissFlag = true;
        }
        void impossible;
        return { crash, proximity, wallClear };
    }
    /** Collect orbs within reach; returns count taken. */
    collectOrbs(px, py, r, D) {
        let taken = 0;
        for (const o of this.orbs) {
            if (o.taken)
                continue;
            if (o.i * exports.L + o.a < D - 15 || o.i * exports.L + o.a > D + 40)
                continue;
            const oz = o.i * exports.L + o.a - D;
            const dx = px - o.x, dy = py - o.y, dz = -oz;
            if (dx * dx + dy * dy + dz * dz < (r + 3.2) * (r + 3.2)) {
                o.taken = true;
                if (o.mat)
                    o.mat.visible = false;
                taken++;
            }
        }
        return taken;
    }
}
exports.World = World;
/** distance from a sphere to an AABB (returns negative when intersecting). */
function sphereAABB(px, py, pz, pr, cx, cy, cz, hx, hy, hz) {
    const dx = Math.max(0, Math.abs(px - cx) - hx);
    const dy = Math.max(0, Math.abs(py - cy) - hy);
    const dz = Math.max(0, Math.abs(pz - cz) - hz);
    const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
    // if inside, report negative penetration depth
    const inside = Math.abs(px - cx) < hx && Math.abs(py - cy) < hy && Math.abs(pz - cz) < hz;
    if (inside) {
        const pen = Math.min(hx - Math.abs(px - cx), hy - Math.abs(py - cy), hz - Math.abs(pz - cz));
        return -pen;
    }
    return dist;
}
function buildBackdrop(scene) {
    // ground plane
    const g = new THREE.Mesh(new THREE.PlaneGeometry(4000, 4000), new THREE.MeshLambertMaterial({ color: 0x2a1540 }));
    g.rotation.x = -Math.PI / 2;
    g.position.y = FLOOR;
    g.receiveShadow = true;
    scene.add(g);
    // distant sun disc glow (sprite-less simple fan of light)
    const sun = new THREE.Mesh(new THREE.CircleGeometry(26, 32), new THREE.MeshBasicMaterial({ color: 0xffca7a, transparent: true, opacity: 0.9 }));
    sun.position.set(180, 130, -520);
    sun.lookAt(0, 0, 0);
    scene.add(sun);
}
};

// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
// Player craft: low-poly mesh + weighty flight physics + banking visuals.
Object.defineProperty(exports, "__esModule", { value: true });
exports.Player = void 0;
const state_1 = require("./state");
const input_1 = require("./input");
class Player {
    constructor() {
        this.group = new THREE.Group();
        this.vx = 0;
        this.vy = 0;
        this.playerRadius = state_1.CFG.playerRadius;
        // smoothing accumulators for banking
        this.bank = 0;
        this.pitch = 0;
        this.yaw = 0;
        this.group = this.buildCraft();
        this.engineGlow = this.group.getObjectByName('engine');
        this.group.position.set(0, 6, 0);
    }
    buildCraft() {
        const g = new THREE.Group();
        const body = new THREE.MeshStandardMaterial({ color: 0xeaf2ff, metalness: 0.9, roughness: 0.28, emissive: 0x2a2e4a, emissiveIntensity: 0.55, flatShading: true });
        const accent = new THREE.MeshStandardMaterial({ color: 0xff7b2d, metalness: 0.4, roughness: 0.4, emissive: 0xff4a00, emissiveIntensity: 0.5, flatShading: true });
        const dark = new THREE.MeshStandardMaterial({ color: 0x23273a, metalness: 0.7, roughness: 0.4, flatShading: true });
        // fuselage
        const fuse = new THREE.Mesh(new THREE.BoxGeometry(1.5, 1.05, 4.0), body);
        fuse.castShadow = true;
        g.add(fuse);
        // nose cone (points +Z)
        const nose = new THREE.Mesh(new THREE.ConeGeometry(1.05, 3.0, 8), body);
        nose.rotation.x = Math.PI / 2; // cone apex toward +Z (forward)
        nose.position.set(0, 0, 3.2);
        nose.castShadow = true;
        g.add(nose);
        // cockpit strip
        const cockpit = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.5, 2.0), accent);
        cockpit.position.set(0, 0.35, 1.0);
        g.add(cockpit);
        // wings
        for (const s of [-1, 1]) {
            const wing = new THREE.Mesh(new THREE.BoxGeometry(3.6, 0.16, 1.6), accent);
            wing.position.set(s * 2.2, -0.1, -0.4);
            wing.rotation.z = s * -0.12;
            wing.castShadow = true;
            g.add(wing);
            const tip = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.9, 0.7), dark);
            tip.position.set(s * 3.9, -0.05, -0.4);
            tip.castShadow = true;
            g.add(tip);
        }
        // tail fin
        const fin = new THREE.Mesh(new THREE.BoxGeometry(0.14, 1.5, 1.5), dark);
        fin.position.set(0, 0.85, -1.9);
        fin.castShadow = true;
        g.add(fin);
        // engine glow (additive) at back
        const eg = new THREE.Mesh(new THREE.ConeGeometry(0.8, 3.0, 10), new THREE.MeshBasicMaterial({ color: 0xffb060, transparent: true, opacity: 0.95 }));
        eg.name = 'engine';
        eg.rotation.x = -Math.PI / 2; // cone apex toward -Z (behind)
        eg.position.set(0, 0, -3.4);
        eg.material.blending = THREE.AdditiveBlending;
        g.add(eg);
        return g;
    }
    /** Integrate flight physics. Returns nothing; reads keyboard via getInput. */
    update(dt) {
        const input = (0, input_1.getInput)();
        const boostMul = 1;
        const targetX = input.x * state_1.CFG.turnRate * boostMul;
        const targetY = input.y * state_1.CFG.pitchRate * boostMul;
        // weighty acceleration toward target
        this.vx += (targetX - this.vx) * Math.min(1, dt * 6.5);
        this.vy += (targetY - this.vy) * Math.min(1, dt * 6.5);
        this.group.position.x += this.vx * dt;
        this.group.position.y += this.vy * dt;
        const p = this.group.position;
        p.x = Math.max(-100, Math.min(100, p.x));
        p.y = Math.max(-20, Math.min(60, p.y));
        // banking/visuals
        this.bank += ((-input.x * 0.85) - this.bank) * Math.min(1, dt * 8);
        this.pitch += ((input.y * 0.5 + this.vy * 0.02) - this.pitch) * Math.min(1, dt * 8);
        this.yaw += ((-input.x * 0.25) - this.yaw) * Math.min(1, dt * 8);
        this.group.rotation.set(this.pitch, this.yaw, this.bank);
    }
    /** Call when collectible picked up: pulse engine briefly. */
    pulseEngine() {
        if (!this.engineGlow)
            return;
        const m = this.engineGlow.material;
        m.color.setHex(0xfff0c0);
        // decay handled in frameUpdate
    }
    frameUpdate() {
        // engine flicker
        if (!this.engineGlow)
            return;
        const m = this.engineGlow.material;
        const t = performance.now() / 1000;
        m.opacity = 0.75 + 0.25 * Math.sin(t * 40);
    }
    reset() {
        this.group.position.set(0, 6, 0);
        this.vx = 0;
        this.vy = 0;
        this.bank = 0;
        this.pitch = 0;
        this.yaw = 0;
        this.group.rotation.set(0, 0, 0);
    }
}
exports.Player = Player;
};

// ── module: src/effects.ts ──
__mods["src/effects.ts"] = function (exports, require, module) {
"use strict";
// Rendering: renderer, camera rig (shake + FOV), post-processing (bloom),
// lighting + fog, and pooled particle systems (exhaust flame, debris, speed streaks).
Object.defineProperty(exports, "__esModule", { value: true });
exports.Effects = void 0;
const BASE_FOV = 72;
class Effects {
    constructor(scene, container) {
        this.camX = 0;
        this.camY = 0;
        this.ex = 0;
        this.ey = 0;
        this.fov = BASE_FOV;
        this.pool = [];
        this.flameAcc = 0;
        this.debrisAcc = 0;
        this.scene = scene;
        const w = container.clientWidth || 1280;
        const h = container.clientHeight || 720;
        this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
        this.renderer.setSize(w, h);
        this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5));
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        this.renderer.setClearColor(0x160a24);
        container.appendChild(this.renderer.domElement);
        this.camera = new THREE.PerspectiveCamera(BASE_FOV, w / h, 0.5, 1600);
        this.camera.position.set(0, 9, -13);
        this.composer = new THREE.EffectComposer(this.renderer);
        this.composer.addPass(new THREE.RenderPass(scene, this.camera));
        this.bloom = new THREE.UnrealBloomPass(new THREE.Vector2(w, h), 0.55, 0.6, 0.1);
        this.composer.addPass(this.bloom);
        this.buildLights();
        this.buildPool();
    }
    buildLights() {
        const hemi = new THREE.HemisphereLight(0xffb37a, 0x2a1240, 1.05);
        this.scene.add(hemi);
        const amb = new THREE.AmbientLight(0x44204f, 0.5);
        this.scene.add(amb);
        const sun = new THREE.DirectionalLight(0xffa264, 2.1);
        sun.position.set(-70, 46, -110);
        sun.castShadow = true;
        sun.shadow.mapSize.width = 512;
        sun.shadow.mapSize.height = 512;
        const sc = 90;
        sun.shadow.camera.left = -sc;
        sun.shadow.camera.right = sc;
        sun.shadow.camera.top = sc;
        sun.shadow.camera.bottom = -sc;
        sun.shadow.camera.near = 1;
        sun.shadow.camera.far = 400;
        this.scene.add(sun);
        const warm = new THREE.PointLight(0xff7b2d, 0.6, 300);
        warm.position.set(0, 4, -20);
        this.scene.add(warm);
        this.scene.fog = new THREE.FogExp2(0x2a1544, 0.0052);
    }
    buildPool() {
        const mk = (n, geo, col, size) => {
            for (let i = 0; i < n; i++) {
                const mat = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 0 });
                mat.blending = THREE.AdditiveBlending;
                mat.depthWrite = false;
                const mesh = new THREE.Mesh(geo, mat);
                const s = size * (0.5 + Math.random() * 0.9);
                mesh.scale.set(s, s, s);
                mesh.visible = false;
                this.scene.add(mesh);
                this.pool.push({ mesh, mat, life: 0, max: 1, vx: 0, vy: 0, vz: 0, grow: 1, baseSize: s, col });
            }
        };
        mk(60, new THREE.OctahedronGeometry(1, 0), 0xffb060, 0.6); // exhaust flame
        mk(160, new THREE.BoxGeometry(0.7, 0.7, 1.4), 0xe0a05a, 0.8); // debris
        mk(60, new THREE.BoxGeometry(0.5, 0.5, 6), 0xffe0b0, 0.9); // speed streaks
    }
    resize() {
        const el = this.renderer.domElement.parentElement;
        const w = el.clientWidth || 1280, h = el.clientHeight || 720;
        this.renderer.setSize(w, h);
        this.camera.aspect = w / h;
        this.camera.updateProjectionMatrix();
        this.composer.setSize(w, h);
    }
    getP() {
        for (const p of this.pool)
            if (p.life <= 0)
                return p;
        return null;
    }
    update(dt, opts) {
        const { px, py, speed01, boost, shake } = opts;
        const sh = shake;
        this.camX += (px - this.camX) * Math.min(1, dt * 5);
        this.camY += (py - this.camY) * Math.min(1, dt * 5);
        this.ex += ((Math.random() * 2 - 1) * sh - this.ex) * Math.min(1, dt * 14);
        this.ey += ((Math.random() * 2 - 1) * sh - this.ey) * Math.min(1, dt * 14);
        this.camera.position.set(this.camX + this.ex, this.camY + 9 + this.ey, -13);
        const targetFov = BASE_FOV + boost * 22 + speed01 * 6;
        this.fov += (targetFov - this.fov) * Math.min(1, dt * 5);
        this.camera.fov = this.fov;
        this.camera.updateProjectionMatrix();
        this.camera.lookAt(px * 0.62 + this.ey * 0.2, py * 0.5 + 2.2, 52);
        this.bloom.strength = 0.45 + boost * 0.65 + speed01 * 0.15;
        this.bloom.threshold = 0.12;
        this.updateParticles(dt, px, py, speed01, boost);
    }
    updateParticles(dt, px, py, speed01, boost) {
        // exhaust plume (short-lived, near tail at z≈0)
        this.flameAcc += dt;
        if (this.flameAcc > 0.011) {
            this.flameAcc = 0;
            const p = this.getP();
            if (p) {
                p.mesh.visible = true;
                p.mat.color.setHex(p.col);
                p.mat.opacity = 0.55;
                p.mesh.position.set(px + (Math.random() - 0.5) * 0.6, py + (Math.random() - 0.5) * 0.6, -3.2 - Math.random() * 1.5);
                const s = (0.5 + Math.random() * 0.7) * (1 + speed01 * 0.6 + boost);
                p.mesh.scale.set(s, s, s * 1.4);
                p.vx = (Math.random() - 0.5) * 3;
                p.vy = (Math.random() - 0.5) * 3;
                p.vz = 0;
                p.grow = 5;
                p.life = 0.2;
                p.max = 0.2;
            }
        }
        // debris / dust streaming toward camera (+z) to sell speed
        this.debrisAcc += dt;
        const rate = 0.018 - speed01 * 0.008 - boost * 0.004;
        if (this.debrisAcc > rate) {
            this.debrisAcc = 0;
            const p = this.getP();
            if (p) {
                p.mesh.visible = true;
                p.mat.color.setHex(p.col);
                p.mat.opacity = 0;
                p.mesh.position.set(px + (Math.random() * 2 - 1) * 40, py + (Math.random() - 0.5) * 30 - 5, -80 - Math.random() * 40);
                p.vx = 0;
                p.vy = 0;
                p.vz = (70 + speed01 * 150 + boost * 170) * (1 + Math.random() * 0.6);
                p.grow = 1;
                p.life = 2.4;
                p.max = 2.4;
            }
        }
        for (const p of this.pool) {
            if (p.life <= 0)
                continue;
            p.life -= dt;
            const frac = Math.max(0, p.life / p.max);
            p.mesh.position.x += p.vx * dt;
            p.mesh.position.y += p.vy * dt;
            p.mesh.position.z += p.vz * dt;
            p.mat.opacity = Math.max(0, Math.min(1, frac * 1.6));
            const sc = p.grow > 1 ? p.baseSize * (0.6 + (1 - frac) * p.grow * 0.25) : p.baseSize * frac;
            p.mesh.scale.set(sc, sc, sc);
            if (p.life <= 0)
                p.mesh.visible = false;
        }
    }
    render() {
        this.composer.render();
    }
}
exports.Effects = Effects;
};

// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
// HUD + overlay rendering. Pure DOM manipulation.
Object.defineProperty(exports, "__esModule", { value: true });
exports.initHUD = initHUD;
exports.saveBest = saveBest;
exports.showIntro = showIntro;
exports.showGameOver = showGameOver;
exports.updateHUD = updateHUD;
exports.getShakeAdd = getShakeAdd;
const state_1 = require("./state");
const $ = (id) => document.getElementById(id);
let scoreEl, multEl, energyFill;
let speedNum, speedFill, turboFill, turboWrap;
let nearFlash, intro, gameover;
let goScore, goSpeed, goTime, goBest, bestEl;
let multTimer = 0;
let flashTimer = 0;
let shakeBar = 0;
function initHUD(onStart, onRestart) {
    scoreEl = $('score');
    multEl = $('mult');
    energyFill = $('energy-fill');
    speedNum = $('speed-num');
    speedFill = $('speed-fill');
    turboFill = $('turbo-fill');
    turboWrap = $('turbo-wrap');
    nearFlash = $('nearflash');
    intro = $('intro');
    gameover = $('gameover');
    goScore = $('go-score');
    goSpeed = $('go-speed');
    goTime = $('go-time');
    goBest = $('go-best');
    bestEl = $('best');
    $('start-btn').addEventListener('click', onStart);
    $('restart-btn').addEventListener('click', onRestart);
    $('best').textContent = bestScore() > 0 ? `BEST ${bestScore()}` : '';
    $('go-best').textContent = '';
}
function bestScore() {
    try {
        return +(localStorage.getItem('canyon_best') || '0');
    }
    catch {
        return 0;
    }
}
function saveBest() {
    try {
        const best = Math.max(bestScore(), Math.round(state_1.S.score));
        localStorage.setItem('canyon_best', String(best));
    }
    catch { /* ignore */ }
}
function showIntro() {
    intro.classList.remove('hidden');
    gameover.classList.add('hidden');
    bestEl.textContent = bestScore() > 0 ? `BEST ${bestScore()}` : '';
}
function showGameOver() {
    gameover.classList.remove('hidden');
    intro.classList.add('hidden');
    goScore.textContent = String(Math.round(state_1.S.score));
    goSpeed.textContent = String(Math.round(state_1.S.maxSpeedReached));
    goTime.textContent = state_1.S.time.toFixed(1) + 's';
    goBest.textContent = `Best: ${Math.max(bestScore(), Math.round(state_1.S.score))}`;
}
function updateHUD(dt) {
    scoreEl.textContent = String(Math.round(state_1.S.score));
    // multiplier pulse
    if (state_1.S.nearMissFlag) {
        multTimer = 0.35;
        flashTimer = 0.42;
    }
    state_1.S.nearMissFlag = false;
    multTimer -= dt;
    if (multTimer > 0)
        multEl.classList.add('hot');
    else
        multEl.classList.remove('hot');
    multEl.textContent = 'x' + state_1.S.multiplier.toFixed(1).replace(/\.0$/, '');
    // near-miss flash
    flashTimer -= dt;
    nearFlash.classList.toggle('show', flashTimer > 0);
    // energy bar
    const ef = state_1.S.energy;
    energyFill.style.width = Math.max(0, Math.min(100, ef)) + '%';
    energyFill.classList.toggle('low', state_1.S.energy < 30);
    if (state_1.S.energy < 18)
        shakeBar = 1;
    else
        shakeBar *= 0.9;
    // speed
    const s01 = Math.min(1, state_1.S.velocity / state_1.CFG.maxSpeed);
    speedFill.style.width = (s01 * 100) + '%';
    speedNum.textContent = String(Math.round(state_1.S.velocity)).padStart(3, '0');
    // turbo
    const t01 = state_1.S.turbo;
    turboFill.style.width = (t01 * 100) + '%';
    turboWrap.style.opacity = state_1.S.turbo >= 1 ? '1' : '0.55';
    turboFill.classList.toggle('full', state_1.S.turbo >= 1 && !state_1.S.boosting);
}
function getShakeAdd() {
    return shakeBar * 0.6;
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4-flash-0731","provider":"openrouter","persona":"typescript","sessionId":"0888c062-e8c8-4770-9234-32b09e8f6ca8","tokensIn":4207034,"tokensOut":68228,"tokensTotal":4275262,"cost":0.11483499599999998,"turns":77,"toolCalls":96,"failedToolCalls":2,"timestamp":"2026-08-01T16:26:40.454Z"} -->