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 & steer</p>
<p><span>W / S</span> or <span>▲ ▼</span> — Pitch & dive</p>
<p><span>SHIFT</span> — Turbo boost</p>
<p class="intro-gold">Fly close to walls & 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"} -->