Canyon Run
Qwen 3.8 27B GGUF · typescript
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 73,530 bytes · SHA-256 c6badb1be9aa
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SUNDRIFT — High-Speed Procedural Canyon Racer</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap">
<style>
/* ── css: base ── */
:root {
--ink: #f6e7ff;
--gold: #ffb86b;
--ember: #ff6b3d;
--violet: #b04dff;
--ice: #8be9ff;
--danger: #ff4d6d;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #14091f; }
body {
font-family: "Orbitron", "Segoe UI", sans-serif;
color: var(--ink);
-webkit-font-smoothing: antialiased;
user-select: none;
}
#app { position: fixed; inset: 0; }
#gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#hud, #overlay, #paused { position: absolute; inset: 0; pointer-events: none; }
/* ---------- speed / letterbox bars (boost drama) ---------- */
.bar {
position: absolute; left: 0; right: 0; height: 7vh;
background: #0a0512;
z-index: 5; pointer-events: none;
transition: height 0.35s cubic-bezier(.2,.8,.2,1);
}
#bar-top { top: 0; border-bottom: 1px solid rgba(255,150,80,.15); }
#bar-bottom { bottom: 0; border-top: 1px solid rgba(255,150,80,.15); }
/* ---------- energy (bottom-left) ---------- */
#energy {
position: absolute; left: 34px; bottom: 34px; width: 250px; height: 16px;
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,180,120,.35);
clip-path: polygon(10px 0, 100% 0, calc(100% - 10px) 100%, 0 100%);
}
#energy-fill {
position: absolute; inset: 0; width: 100%;
background: linear-gradient(90deg, #ff8a3d, #ffc46b, #8be9ff);
box-shadow: 0 0 18px rgba(255,170,90,.55);
transform-origin: left center;
}
#energy-ticks {
position: absolute; inset: 0;
background-image: repeating-linear-gradient(90deg, transparent 0 24px, rgba(10,5,18,.85) 24px 26px);
}
#energy.low #energy-fill { animation: pulseLow .5s infinite alternate; }
@keyframes pulseLow { from { filter: brightness(.6); } to { filter: brightness(1.5); } }
/* ---------- turbo (bottom-right) ---------- */
#turbo-wrap { position: absolute; right: 34px; bottom: 34px; width: 250px; text-align: right; }
#turbo-label {
font-size: 10px; letter-spacing: .3em; color: rgba(246,231,255,.55);
display: block; margin-bottom: 6px;
}
#turbo-track {
position: relative; height: 8px; width: 100%;
background: rgba(255,255,255,.06);
border: 1px solid rgba(139,233,255,.35);
clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 100%, 10px 100%);
}
#turbo-fill {
position: absolute; inset: 0; width: 0%;
background: linear-gradient(90deg, #5d7cff, #8be9ff, #ffffff);
box-shadow: 0 0 14px rgba(139,233,255,.8);
transform-origin: left center;
}
#turbo-wrap.ready #turbo-label { color: #8be9ff; animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: .35; } }
/* ---------- score block (top-right) ---------- */
#near { position: absolute; top: 30px; right: 36px; text-align: right; }
#mult { display: flex; justify-content: flex-end; align-items: baseline; gap: 4px; }
#mult-x { font-size: 26px; font-weight: 700; color: rgba(255,184,107,.8); }
#mult-n {
font-size: 72px; font-weight: 900; line-height: 1;
color: #fff; text-shadow: 0 0 24px rgba(255,150,60,.8);
}
#mult.hot { color: #ffd27a; }
#mult.hot #mult-x { color: #ffd27a; }
#mult.pulse { animation: multPulse .45s ease-out; }
@keyframes multPulse {
0% { transform: scale(1); }
30% { transform: scale(1.28) rotate(-2deg); text-shadow: 0 0 40px rgba(255,220,150,1); }
100% { transform: scale(1); }
}
#near-badge {
display: block; font-size: 11px; letter-spacing: .5em; font-weight: 700;
color: #ffd27a; opacity: 0; margin-bottom: 2px;
}
#near-badge.show { animation: badgePop .6s ease-out forwards; }
@keyframes badgePop {
0% { opacity: 0; transform: translateY(8px) scale(1.4); }
25% { opacity: 1; transform: translateY(0) scale(1); }
80% { opacity: 1; }
100% { opacity: 0; }
}
#score {
margin-top: 6px; font-size: 20px; font-weight: 500; letter-spacing: .25em;
color: rgba(246,231,255,.85);
}
#score::before { content: "SCORE "; font-size: 10px; letter-spacing: .4em; color: rgba(246,231,255,.4); }
/* ---------- speedometer (left edge, vertical) ---------- */
#speedo {
position: absolute; left: 34px; top: 50%; transform: translateY(-50%);
display: flex; align-items: center; gap: 10px; height: 46vh;
}
#speedo-track {
position: relative; width: 4px; height: 100%;
background: rgba(255,255,255,.1);
}
#speedo-fill {
position: absolute; left: 0; right: 0; bottom: 0; height: 20%;
background: linear-gradient(0deg, #ff6b3d, #ffb86b 55%, #8be9ff);
box-shadow: 0 0 12px rgba(255,150,80,.7);
}
#speedo-num { font-size: 15px; font-weight: 700; letter-spacing: .15em; color: rgba(246,231,255,.9); }
#speedo-num::after { content: " KM/H"; font-size: 8px; color: rgba(246,231,255,.4); letter-spacing: .3em; }
/* ---------- pause ---------- */
#paused { display: flex; align-items: center; justify-content: center; background: rgba(10,5,18,.45); z-index: 8; }
#paused[hidden] { display: none; }
.pa-box { text-align: center; border: 1px solid rgba(255,184,107,.4); padding: 26px 54px; background: rgba(20,9,31,.8); }
.pa-box span { font-size: 30px; font-weight: 900; letter-spacing: .5em; }
.pa-box small { display: block; margin-top: 10px; font-size: 10px; letter-spacing: .3em; color: rgba(246,231,255,.5); }
/* ---------- overlay ---------- */
#overlay {
display: flex; align-items: center; justify-content: center;
background: radial-gradient(ellipse at 50% 120%, rgba(120,40,90,.35), rgba(10,5,18,.82) 65%);
z-index: 10; transition: opacity .5s;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
#panel {
width: min(560px, 86vw); text-align: center;
border: 1px solid rgba(255,184,107,.25);
background: linear-gradient(180deg, rgba(24,11,38,.92), rgba(16,8,28,.92));
padding: 46px 44px 40px;
box-shadow: 0 0 80px rgba(255,120,60,.12), inset 0 0 60px rgba(120,60,180,.10);
}
.p-kicker { font-size: 10px; letter-spacing: .55em; color: rgba(255,184,107,.75); margin-bottom: 14px; }
.p-title {
font-size: 64px; font-weight: 900; letter-spacing: .12em; line-height: 1;
color: #fff; text-shadow: 0 0 34px rgba(255,140,60,.55);
}
.p-title span { color: var(--gold); }
.p-sub { margin: 16px auto 26px; max-width: 400px; font-size: 12px; line-height: 1.8; letter-spacing: .08em; color: rgba(246,231,255,.65); font-weight: 500; }
.p-keys { display: inline-block; text-align: left; margin-bottom: 28px; }
.k-row { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 8px 0; min-width: 320px; }
.k-row span { font-size: 9px; letter-spacing: .35em; color: rgba(246,231,255,.5); }
.kbd { font-family: inherit; }
kbd {
display: inline-block; min-width: 26px; padding: 4px 8px; margin-left: 6px;
border: 1px solid rgba(255,184,107,.4); font-size: 10px; letter-spacing: .1em;
color: var(--gold); background: rgba(255,150,80,.06);
}
.p-btn {
pointer-events: auto; cursor: pointer;
font-family: inherit; font-size: 15px; font-weight: 700; letter-spacing: .4em;
color: #1c0b22; background: linear-gradient(90deg, #ffb86b, #ff8a3d);
border: none; padding: 16px 46px 16px 50px;
clip-path: polygon(14px 0, 100% 0, calc(100% - 14px) 100%, 0 100%);
box-shadow: 0 0 34px rgba(255,140,60,.45);
transition: transform .15s, box-shadow .15s, filter .15s;
}
.p-btn em { font-style: normal; margin-left: 12px; }
.p-btn:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 0 54px rgba(255,150,60,.75); filter: brightness(1.08); }
.p-btn:active { transform: translateY(0) scale(.98); }
.p-hint { margin-top: 22px; font-size: 9px; letter-spacing: .28em; color: rgba(246,231,255,.4); line-height: 2; }
/* ---------- game over ---------- */
#overlay.death .p-kicker { color: rgba(255,77,109,.85); }
#overlay.death .p-title { text-shadow: 0 0 34px rgba(255,77,109,.5); }
.stat-grid { display: flex; justify-content: center; gap: 44px; margin: 6px 0 26px; }
.stat { text-align: center; }
.stat b { display: block; font-size: 34px; font-weight: 900; color: #fff; text-shadow: 0 0 20px rgba(255,150,60,.6); }
.stat i { display: block; font-style: normal; margin-top: 8px; font-size: 9px; letter-spacing: .35em; color: rgba(246,231,255,.45); }
#overlay.death #startBtn { background: linear-gradient(90deg, #ff6b8a, #ff6b3d); }
/* ---------- hurt / boost tints ---------- */
#tint {
position: absolute; inset: 0; pointer-events: none; z-index: 4; opacity: 0;
background: radial-gradient(ellipse at center, transparent 42%, rgba(255,30,50,.55) 100%);
}
#boostTint {
position: absolute; inset: 0; pointer-events: none; z-index: 4; opacity: 0;
background: radial-gradient(ellipse at center, rgba(139,233,255,.05) 50%, rgba(90,140,255,.18) 100%);
}
</style>
</head>
<body>
<div id="app">
<canvas id="gl"></canvas><div id="bar-top" class="bar"></div>
<div id="bar-bottom" class="bar"></div>
<div id="tint"></div>
<div id="boostTint"></div>
<div id="hud">
<div id="energy" title="ENERGY"><div id="energy-fill"></div><div id="energy-ticks"></div></div>
<div id="turbo-wrap"><div id="turbo-track"><div id="turbo-fill"></div></div><span id="turbo-label">TURBO · press SHIFT</span></div>
<div id="near">
<span id="near-badge">NEAR MISS</span>
<div id="mult"><span id="mult-x">x</span><span id="mult-n">1</span></div>
<div id="score">0000000</div>
</div>
<div id="speedo"><div id="speedo-fill"></div><span id="speedo-num">000</span></div>
</div>
<div id="overlay">
<div id="panel">
<div class="p-kicker">high-speed procedural canyon racer</div>
<h1 class="p-title">SUN<span>DRIFT</span></h1>
<p class="p-sub">Carve the endless dusk canyon. Every wall you kiss pays. Run out of energy and the sky closes in.</p>
<div class="p-keys">
<div class="k-row"><span>STEER · BANK</span><kbd>A D</kbd><kbd>◄ ►</kbd></div>
<div class="k-row"><span>PITCH · BRAKE / BURN</span><kbd>S W</kbd><kbd>▼ ▲</kbd></div>
<div class="k-row"><span>TURBO BURN</span><kbd>SHIFT</kbd></div>
<div class="k-row"><span>RESTART</span><kbd>R</kbd></div>
</div>
<button id="startBtn" class="p-btn">LAUNCH <em>▶</em></button>
<div class="p-hint">near-misses stack the multiplier · orbs refill energy · turbo builds on straightaways</div>
</div>
</div>
<div id="paused" hidden=""><div class="pa-box"><span>PAUSED</span><small>ESC to resume</small></div></div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts"},"src/worldgen.ts":{"./types":"src/types.ts"},"src/player.ts":{"./types":"src/types.ts"},"src/scene.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./scene":"src/scene.ts","./worldgen":"src/worldgen.ts","./player":"src/player.ts","./particles":"src/particles.ts","./audio":"src/audio.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// Entry point: boot the SUNDRIFT game.
const game_1 = require("./game");
(0, game_1.boot)();
};
// ── types: src/globals.d.ts ──
__mods["src/globals.d.ts"] = function (exports) {};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// Shared types, tuning constants and palette for SUNDRIFT.
Object.defineProperty(exports, "__esModule", { value: true });
exports.smooth = exports.rand = exports.lerp = exports.clamp = exports.PAL = exports.CFG = void 0;
exports.difficultyOf = difficultyOf;
exports.maxSpeedFor = maxSpeedFor;
exports.cruiseTarget = cruiseTarget;
exports.gapHalfW = gapHalfW;
exports.wallThickness = wallThickness;
exports.wallHeight = wallHeight;
exports.monolithDensity = monolithDensity;
exports.gapYRange = gapYRange;
exports.CFG = {
// player
BASE_SPEED: 52,
MAX_SPEED: 240,
BOOST_SPEED: 340,
SPEED_ACCEL: 26, // units/s^2 toward cruise
PITCH_ACCEL: 55, // speed gain from pitch up
PITCH_BRAKE: 42, // speed loss from pitch down
BOOST_MULT: 1.45,
BOOST_MIN_ENERGY: 18,
BOOST_DRAIN: 26, // energy/s while boosting
DRAG_XY: 2.2, // lateral damping factor
MAX_LAT_SPEED: 33,
GRAVITY: 30,
ENERGY_DECAY: 2.4, // energy/s
ORB_ENERGY: 26,
// world
CH_LEN: 130,
N_AHEAD: 5,
GEN_SPACING: 118,
SPAWN_AHEAD: 640,
SUN_Z: -3800,
GATE_HALF_W: 27,
GATE_HALF_H: 15,
GATE_MIN: 11,
GATE_MAX: 17,
WALL_H: 95,
WALL_BASE_HZ: 9,
WALL_MAX_HZ: 15,
// near miss
NM_GRACE: 4.8,
NM_CREDIT: 9.0,
NM_WINDOW: 3.2,
HIT_GRACE: 0.9,
HIT_R: 1.9,
HIT_E_LOSS: 12,
HIT_MULT_KEEP: 0.5,
// scoring
SCORE_SPEED: 0.2,
SCORE_NM: 120,
SCORE_ORB: 25,
};
exports.PAL = {
skyTop: 0x1a0f38,
skyMid: 0x6b2d6e,
skyLow: 0xff8a4d,
horizon: 0xffd9a0,
sun: 0xffe6b0,
sunCore: 0xfff3d6,
fog: 0xd96b3f,
rockA: 0x8a3a48,
rockB: 0x5e2c56,
rockC: 0x3a2150,
wallEdge: 0xff8a5c,
monolith: 0x46326e,
monolithEdge: 0xc07cff,
frame: 0x2a1a44,
frameEdge: 0xffb36b,
orb: 0x8be9ff,
orbCore: 0xeaffff,
engine: 0xffb347,
ship: 0xcfd6e6,
};
// difficulty 0..1 (score-driven)
function difficultyOf(score) {
return Math.min(1, score / 30000);
}
function maxSpeedFor(score) {
const d = difficultyOf(score);
return exports.CFG.BASE_SPEED + (exports.CFG.MAX_SPEED - exports.CFG.BASE_SPEED) * d;
}
function cruiseTarget(score) {
return exports.CFG.BASE_SPEED + (exports.CFG.MAX_SPEED * 0.55 - exports.CFG.BASE_SPEED) * Math.min(1, score / 12000);
}
function gapHalfW(score) {
const d = difficultyOf(score);
return exports.CFG.GATE_HALF_W - d * 7.5;
}
function wallThickness(score) {
const d = difficultyOf(score);
return exports.CFG.WALL_BASE_HZ + d * (exports.CFG.WALL_MAX_HZ - exports.CFG.WALL_BASE_HZ);
}
function wallHeight(score) {
const d = difficultyOf(score);
return exports.CFG.WALL_H + d * 45;
}
function monolithDensity(score) {
return 2 + Math.floor(difficultyOf(score) * 4); // per 2 chunks
}
function gapYRange(score) {
return exports.CFG.GATE_HALF_H - difficultyOf(score) * 3;
}
const clamp = (v, a, b) => (v < a ? a : v > b ? b : v);
exports.clamp = clamp;
const lerp = (a, b, t) => a + (b - a) * t;
exports.lerp = lerp;
const rand = (a, b) => a + Math.random() * (b - a);
exports.rand = rand;
const smooth = (t) => t * t * (3 - 2 * t);
exports.smooth = smooth;
};
// ── module: src/worldgen.ts ──
__mods["src/worldgen.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.World = void 0;
// Chunk-based procedural canyon generation with shared geometry and clean disposal.
const types_1 = require("./types");
const L = types_1.CFG.CH_LEN;
class World {
constructor(scene, sunLight) {
this.gates = new Map();
this.chunks = [];
this.maxIndex = 0;
this.score = 0;
this.scene = scene;
this.sunLight = sunLight;
this.slabMat = new THREE.MeshLambertMaterial({ vertexColors: true, side: THREE.DoubleSide });
this.monoMat = new THREE.MeshLambertMaterial({ color: types_1.PAL.monolith, emissive: 0x241340, emissiveIntensity: 0.9 });
this.frameMat = new THREE.MeshLambertMaterial({ color: types_1.PAL.frame, emissive: 0x3a1a10, emissiveIntensity: 0.8 });
this.orbMat = new THREE.MeshLambertMaterial({ color: 0x06202a, emissive: types_1.PAL.orb, emissiveIntensity: 1.6 });
this.tipMat = new THREE.MeshBasicMaterial({ color: types_1.PAL.monolithEdge });
this.boxGeo = new THREE.BoxGeometry(1, 1, 1);
this.orbGeo = new THREE.IcosahedronGeometry(1, 0);
this.tipGeo = new THREE.OctahedronGeometry(1, 0);
}
// ---------------- gate path ----------------
gate(i) {
let g = this.gates.get(i);
if (g)
return g;
const base = i === 0 ? { x: 0, y: 2, hw: 22 } : this.gate(i - 1);
const d = this.score > 0 ? Math.min(1, this.score / 30000) : 0;
const x = (0, types_1.clamp)(base.x + (0, types_1.rand)(-1, 1) * (15 + 20 * d), -44, 44);
const y = (0, types_1.clamp)(base.y + (0, types_1.rand)(-1, 1) * (5 + 9 * d), -22, 24);
let hw = (0, types_1.clamp)((0, types_1.gapHalfW)(this.score) + (0, types_1.rand)(-1.5, 1.5), 9, 21);
if (i < 4)
hw = Math.max(hw, 19); // gentle intro
g = { x, y, hw };
this.gates.set(i, g);
return g;
}
// path point sampled along chunk i, t in [0,1] (0 = far end, 1 = near end)
pathAt(i, t) {
const tS = (0, types_1.smooth)(t);
const a = this.gate(i + 1);
const b = this.gate(i);
return {
x: (0, types_1.lerp)(a.x, b.x, tS),
y: (0, types_1.lerp)(a.y, b.y, tS),
hw: (0, types_1.lerp)(a.hw, b.hw, tS),
z: -(i + 1 - t) * L,
};
}
// ---------------- chunk building ----------------
ensure(playerZ, lookahead) {
while ((this.maxIndex + 1) * L < -playerZ + lookahead) {
this.buildChunk(this.maxIndex);
this.maxIndex++;
}
}
disposeAll() {
for (const c of this.chunks) {
for (const m of c.meshes)
if (m.parent)
m.parent.remove(m);
c.dispose();
}
this.chunks.length = 0;
this.gates.clear();
this.maxIndex = 0;
this.score = 0;
}
cull(playerZ) {
// flight is toward -z: a chunk is only reclaimable once its FAR
// (most-negative) end z0 is behind the player (+ margin), so every
// collider ahead of the player survives.
const behind = playerZ + 30;
for (let i = this.chunks.length - 1; i >= 0; i--) {
const c = this.chunks[i];
if (c.z0 > behind) {
for (const m of c.meshes)
if (m.parent)
m.parent.remove(m);
c.dispose();
this.chunks.splice(i, 1);
}
}
}
buildChunkPublic(i) {
this.buildChunk(i);
}
buildChunk(i) {
const z0 = -(i + 1) * L; // far
const z1 = -i * L; // near
const meshes = [];
const boxes = [];
const spheres = [];
const disposes = [];
// ---- canyon wall slabs (left + right) ----
const H = (0, types_1.wallHeight)(0) + 14;
const hz = (0, types_1.wallThickness)(this.score);
const baseY = -13;
for (const side of [-1, 1]) {
const N = 8;
const pos = [];
const col = [];
const idx = [];
const cDeep = new THREE.Color(0x1d0f2e);
const cWarm = new THREE.Color(types_1.PAL.rockA);
const cTop = new THREE.Color(0xff9a5e);
const cTmp = new THREE.Color();
for (let s = 0; s <= N; s++) {
const t = s / N;
const p = this.pathAt(i, t);
const jag = Math.sin(Math.PI * t) * (4 + 7 * Math.abs(Math.sin(t * 7.3 + i * 2.1))) * (0.6 + 0.4 * Math.sin(t * 13.7 + i));
const xIn = p.x + side * p.hw;
const xOut = p.x + side * (p.hw + hz + Math.max(2, jag));
const topIn = baseY + H - Math.sin(Math.PI * t) * 10 * (0.5 + 0.5 * Math.sin(t * 9.1 + i * 3.7));
const topOut = baseY + H + 8 - Math.sin(Math.PI * t) * (14 + 8 * Math.sin(t * 5.3 + i * 1.9));
const v = (x, y, warm, inner) => {
pos.push(x, y, p.z);
cTmp.copy(cDeep).lerp(cWarm, (0, types_1.clamp)((y - baseY) / H, 0, 1) * 0.85).lerp(cTop, warm);
if (inner)
cTmp.multiplyScalar(0.62); // shadowed inner face
col.push(cTmp.r, cTmp.g, cTmp.b);
};
v(xOut, baseY, 0, false); // 0
v(xOut, topOut, 0.55, false); // 1
v(xIn, baseY, 0, true); // 2
v(xIn, topIn, 0.35, true); // 3
}
const quad = (a, b, c2, d) => {
idx.push(a, b, c2, a, c2, d);
};
for (let s = 0; s < N; s++) {
const o = s * 4;
quad(o + 0, o + 2, o + 6, o + 4); // bottom
quad(o + 1, o + 3, o + 7, o + 5); // top
quad(o + 2, o + 6, o + 7, o + 3); // inner face
quad(o + 0, o + 4, o + 5, o + 1); // outer face
}
const geo = new THREE.BufferGeometry();
geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(pos), 3));
geo.setAttribute("color", new THREE.BufferAttribute(new Float32Array(col), 3));
geo.setIndex(idx);
geo.computeVertexNormals();
const mesh = new THREE.Mesh(geo, this.slabMat);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.scene.add(mesh);
meshes.push(mesh);
disposes.push(() => geo.dispose());
// wall near-miss spheres along each edge
for (let s = 0; s < 3; s++) {
const t = (s + 0.5) / 3;
const p = this.pathAt(i, t);
spheres.push({ cx: p.x + side * (p.hw + hz * 0.5), cy: 4, cz: p.z, r: hz * 0.5 + 3.2, type: "wall" });
}
// wall hit boxes (split in thirds)
for (let s = 0; s < 3; s++) {
const ta = s / 3, tb = (s + 1) / 3;
const pa = this.pathAt(i, ta), pb = this.pathAt(i, tb);
const zx = (pa.z + pb.z) / 2;
const zlen = L / 3 + 1;
const xEdge = (pa.x + pb.x) / 2 + side * ((pa.hw + pb.hw) / 2);
boxes.push({ cx: xEdge + side * (hz / 2 + 2), cy: 2, cz: zx, hx: hz / 2 + 2, hy: 60, hz: zlen / 2 });
}
}
// ---- monoliths ----
const d = this.score > 0 ? Math.min(1, this.score / 30000) : 0;
if (i > 1 && Math.random() < 0.55 + 0.35 * d) {
const n = Math.random() < 0.35 + 0.45 * d ? 2 : 1;
for (let k = 0; k < n; k++) {
const t = (0, types_1.rand)(0.18, 0.82);
const p = this.pathAt(i, t);
const r = (0, types_1.rand)(3.2, 6 + 3 * d);
const h = (0, types_1.rand)(18, 34);
const ox = (0, types_1.rand)(-0.5, 0.5) * (p.hw - r - 2);
const oy = p.y + (0, types_1.rand)(-0.35, 0.35) * 2 * (0, types_1.gapYRange)(this.score);
const cx = p.x + ox, cy = oy, cz = p.z;
const body = new THREE.Mesh(this.boxGeo, this.monoMat);
body.position.set(cx, cy, cz);
body.scale.set(r * 2, h, r * 2);
body.rotation.y = (0, types_1.rand)(0, Math.PI) * 0.25;
body.castShadow = true;
body.receiveShadow = true;
const tip = new THREE.Mesh(this.tipGeo, this.tipMat);
tip.position.set(cx, cy + h / 2 + 1.6, cz);
tip.scale.setScalar(0.9);
this.scene.add(body, tip);
meshes.push(body, tip);
boxes.push({ cx, cy, cz, hx: r, hy: h / 2, hz: r });
spheres.push({ cx, cy, cz, r: r + 2.0, type: "mono" });
}
}
// ---- squeeze frames ----
if (i > 2 && i % 2 === 0 && Math.random() < 0.22 + 0.45 * d) {
const t = 0.5;
const p = this.pathAt(i, t);
const ghSq = 7.5 + (0, types_1.rand)(0, 3.5) - d * 1.5; // vertical half gap
const hwSq = p.hw - 2 - (0, types_1.rand)(0, 2.5);
const bw = 2.4; // beam width
const z = p.z;
const xL = p.x - hwSq, xR = p.x + hwSq;
const yB = p.y - ghSq, yT = p.y + ghSq;
const span = (w, cx, cy, cx2, cy2) => {
const dx = cx2 - cx, dy = cy2 - cy;
const m = new THREE.Mesh(this.boxGeo, this.frameMat);
m.position.set((cx + cx2) / 2, (cy + cy2) / 2, z);
m.scale.set(w + Math.abs(dx), w + Math.abs(dy), bw);
m.castShadow = true;
this.scene.add(m);
meshes.push(m);
};
span(bw, xL, yT, xR, yT); // top beam
span(bw, xL, yB, xR, yB); // bottom beam
span(bw, xL, yB, xL, yT); // left post
span(bw, xR, yB, xR, yT); // right post
boxes.push({ cx: p.x, cy: yT + bw / 2, cz: z, hx: (xR - xL) / 2 + bw / 2, hy: bw / 2, hz: bw / 2 }, { cx: p.x, cy: yB - bw / 2, cz: z, hx: (xR - xL) / 2 + bw / 2, hy: bw / 2, hz: bw / 2 }, { cx: xL - bw / 2, cy: p.y, cz: z, hx: bw / 2, hy: ghSq + bw / 2, hz: bw / 2 }, { cx: xR + bw / 2, cy: p.y, cz: z, hx: bw / 2, hy: ghSq + bw / 2, hz: bw / 2 });
for (const [sx, sy] of [[-1, -1], [1, -1], [-1, 1], [1, 1]]) {
spheres.push({ cx: p.x + sx * hwSq, cy: p.y + sy * ghSq, cz: z, r: 3.4, type: "squeeze" });
}
}
// ---- energy orbs (placed on the risky edge: near a wall or monolith) ----
if (i > 0 && Math.random() < 0.85) {
const n = 3 + Math.floor((0, types_1.rand)(0, 2) + d * 1.6);
const side = Math.random() < 0.5 ? -1 : 1;
const t0 = (0, types_1.rand)(0.12, 0.55);
for (let k = 0; k < n; k++) {
const t = (0, types_1.clamp)(t0 + k * 0.11, 0.05, 0.95);
const p = this.pathAt(i, t);
const risky = Math.random() < 0.65;
const edge = p.hw * (risky ? 0.62 + Math.random() * 0.2 : 0.25 + Math.random() * 0.15);
const x = p.x + side * edge;
const y = (0, types_1.clamp)(p.y + (Math.random() - 0.5) * 5, p.y - (0, types_1.gapYRange)(this.score) + 2, p.y + (0, types_1.gapYRange)(this.score) - 2);
const orb = new THREE.Mesh(this.orbGeo, this.orbMat);
orb.position.set(x, y, p.z);
orb.scale.setScalar(1.5);
this.scene.add(orb);
meshes.push(orb);
spheres.push({ cx: x, cy: y, cz: p.z, r: 3.4, type: "orb", mesh: orb });
}
}
this.chunks.push({
type: "wall",
z0, z1,
meshes,
boxes,
spheres,
dispose: () => { for (const fn of disposes)
fn(); },
});
}
}
exports.World = World;
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sfx = void 0;
// Procedural Web Audio: engine hum, orb pings, near-miss whooshes, crash, turbo.
class Sfx {
constructor() {
this.ctx = null;
this.master = null;
this.engineOsc = null;
this.engineSub = null;
this.engineGain = null;
this.engineLfo = null;
this.noiseBuf = null;
this.muted = false;
this._filt = null;
}
init() {
if (this.ctx) {
void this.ctx.resume();
return;
}
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC)
return;
const ctx = new AC();
this.ctx = ctx;
this.master = ctx.createGain();
this.master.gain.value = 0.55;
this.master.connect(ctx.destination);
// noise buffer (shared)
const len = ctx.sampleRate * 1;
const buf = ctx.createBuffer(1, len, ctx.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < len; i++)
d[i] = Math.random() * 2 - 1;
this.noiseBuf = buf;
// engine drone: detuned saw + sub sine, filtered
const g = ctx.createGain();
g.gain.value = 0.0;
const filt = ctx.createBiquadFilter();
filt.type = "lowpass";
filt.frequency.value = 380;
filt.Q.value = 6;
const o1 = ctx.createOscillator();
o1.type = "sawtooth";
o1.frequency.value = 55;
const o2 = ctx.createOscillator();
o2.type = "sawtooth";
o2.frequency.value = 55.7;
const sub = ctx.createOscillator();
sub.type = "sine";
sub.frequency.value = 28;
const subG = ctx.createGain();
subG.gain.value = 0.5;
o1.connect(filt);
o2.connect(filt);
filt.connect(g);
sub.connect(subG);
subG.connect(g);
g.connect(this.master);
const lfo = ctx.createOscillator();
lfo.type = "sine";
lfo.frequency.value = 11;
const lfoG = ctx.createGain();
lfoG.gain.value = 22;
lfo.connect(lfoG);
lfoG.connect(o1.frequency);
o1.start();
o2.start();
sub.start();
lfo.start();
this.engineOsc = o1;
this.engineSub = sub;
this.engineGain = g;
this.engineLfo = lfo;
filt.__sfx_filt = true;
this._filt = filt;
}
setEngine(speedNorm, boost, alive) {
if (!this.ctx || !this.engineOsc || !this.engineGain)
return;
const t = this.ctx.currentTime;
const on = alive && !this.muted;
const base = on ? (24 + speedNorm * 40 + (boost ? 26 : 0)) : 0;
this.engineOsc.frequency.setTargetAtTime(55 + speedNorm * 70 + (boost ? 40 : 0), t, 0.08);
if (this.engineSub)
this.engineSub.frequency.setTargetAtTime(28 + speedNorm * 44, t, 0.08);
if (this._filt)
this._filt.frequency.setTargetAtTime(300 + speedNorm * 1600 + (boost ? 900 : 0), t, 0.1);
this.engineGain.gain.setTargetAtTime(on ? 0.05 + speedNorm * 0.07 + (boost ? 0.045 : 0) : 0.0, t, 0.15);
void base;
}
env(g, t, a, peak, dec) {
g.gain.cancelScheduledValues(t);
g.gain.setValueAtTime(0.0001, t);
g.gain.linearRampToValueAtTime(peak, t + a);
g.gain.exponentialRampToValueAtTime(0.0001, t + a + dec);
}
orb(step) {
if (!this.ctx || !this.master)
return;
const t = this.ctx.currentTime;
const o = this.ctx.createOscillator();
o.type = "sine";
const f = 660 * Math.pow(1.06, step % 12);
o.frequency.value = f;
const g = this.ctx.createGain();
this.env(g, t, 0.008, 0.22, 0.35);
o.connect(g);
g.connect(this.master);
o.start(t);
o.stop(t + 0.5);
// sparkle octave
const o2 = this.ctx.createOscillator();
o2.type = "triangle";
o2.frequency.value = f * 2;
const g2 = this.ctx.createGain();
this.env(g2, t + 0.02, 0.008, 0.1, 0.2);
o2.connect(g2);
g2.connect(this.master);
o2.start(t + 0.02);
o2.stop(t + 0.4);
}
whoosh(intensity) {
if (!this.ctx || !this.master || !this.noiseBuf)
return;
const t = this.ctx.currentTime;
const src = this.ctx.createBufferSource();
src.buffer = this.noiseBuf;
src.loop = true;
src.playbackRate.value = 0.7 + intensity * 0.8;
const bp = this.ctx.createBiquadFilter();
bp.type = "bandpass";
bp.Q.value = 1.4;
const g = this.ctx.createGain();
const dur = 0.45;
bp.frequency.setValueAtTime(160, t);
bp.frequency.exponentialRampToValueAtTime(2400 + 2400 * intensity, t + dur * 0.5);
bp.frequency.exponentialRampToValueAtTime(300, t + dur);
this.env(g, t, 0.04, 0.16 + intensity * 0.3, dur);
src.connect(bp);
bp.connect(g);
g.connect(this.master);
src.start(t);
src.stop(t + dur + 0.1);
}
crash() {
if (!this.ctx || !this.master || !this.noiseBuf)
return;
const t = this.ctx.currentTime;
const src = this.ctx.createBufferSource();
src.buffer = this.noiseBuf;
const lp = this.ctx.createBiquadFilter();
lp.type = "lowpass";
lp.frequency.setValueAtTime(2600, t);
lp.frequency.exponentialRampToValueAtTime(80, t + 0.9);
const g = this.ctx.createGain();
this.env(g, t, 0.005, 0.9, 0.9);
src.connect(lp);
lp.connect(g);
g.connect(this.master);
src.start(t);
src.stop(t + 1.1);
const o = this.ctx.createOscillator();
o.type = "sine";
o.frequency.setValueAtTime(110, t);
o.frequency.exponentialRampToValueAtTime(32, t + 0.5);
const g2 = this.ctx.createGain();
this.env(g2, t, 0.005, 0.5, 0.5);
o.connect(g2);
g2.connect(this.master);
o.start(t);
o.stop(t + 0.7);
}
turbo(on) {
if (!this.ctx || !this.master || !this.noiseBuf)
return;
const t = this.ctx.currentTime;
const src = this.ctx.createBufferSource();
src.buffer = this.noiseBuf;
src.loop = true;
const bp = this.ctx.createBiquadFilter();
bp.type = "bandpass";
bp.Q.value = 0.8;
bp.frequency.value = 900;
const g = this.ctx.createGain();
if (on) {
this.env(g, t, 0.25, 0.22, 0.3);
}
else {
g.gain.cancelScheduledValues(t);
g.gain.setTargetAtTime(0.0001, t, 0.2);
}
src.connect(bp);
bp.connect(g);
g.connect(this.master);
src.start(t);
src.stop(t + (on ? 0.6 : 0.6));
}
uiClick() {
if (!this.ctx || !this.master)
return;
const t = this.ctx.currentTime;
const o = this.ctx.createOscillator();
o.type = "square";
o.frequency.value = 880;
const g = this.ctx.createGain();
this.env(g, t, 0.004, 0.08, 0.08);
o.connect(g);
g.connect(this.master);
o.start(t);
o.stop(t + 0.12);
}
setMuted(m) {
this.muted = m;
if (this.master && this.ctx)
this.master.gain.setTargetAtTime(m ? 0 : 0.55, this.ctx.currentTime, 0.05);
}
isMuted() { return this.muted; }
}
exports.sfx = new Sfx();
};
// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Particles = void 0;
exports.makeGlowCanvas = makeGlowCanvas;
// Pooled GPU point particles: engine trail, dust streaks, debris, burst sparks.
const MAX = 2400;
class Particles {
constructor(scene, makeCanvas) {
this.pool = [];
this.cursor = 0;
this.scene = scene;
this.pos = new Float32Array(MAX * 3);
this.col = new Float32Array(MAX * 3);
for (let i = 0; i < MAX; i++) {
this.pos[i * 3 + 1] = -9999;
this.pool.push({
x: 0, y: -9999, z: 0, vx: 0, vy: 0, vz: 0,
life: 0, maxLife: 1, size: 1, r: 1, g: 1, b: 1,
active: false, drag: 0, grav: 0,
});
}
this.geo = new THREE.BufferGeometry();
this.geo.setAttribute("position", new THREE.BufferAttribute(this.pos, 3));
this.geo.setAttribute("color", new THREE.BufferAttribute(this.col, 3));
const tex = new THREE.CanvasTexture(makeCanvas(64));
this.mat = new THREE.PointsMaterial({
size: 1.6,
map: tex,
vertexColors: true,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
sizeAttenuation: true,
});
this.pts = new THREE.Points(this.geo, this.mat);
this.pts.frustumCulled = false;
scene.add(this.pts);
}
grab() {
// ring-find a slot, overwriting the oldest if full
for (let i = 0; i < MAX; i++) {
const idx = (this.cursor + i) % MAX;
const p = this.pool[idx];
if (!p.active) {
this.cursor = (idx + 1) % MAX;
return p;
}
}
const p = this.pool[this.cursor];
this.cursor = (this.cursor + 1) % MAX;
p.active = true;
return p;
}
spawn(x, y, z, vx, vy, vz, life, size, r, g, b, drag, grav) {
const p = this.grab();
p.x = x;
p.y = y;
p.z = z;
p.vx = vx;
p.vy = vy;
p.vz = vz;
p.life = life;
p.maxLife = life;
p.size = size;
p.r = r;
p.g = g;
p.b = b;
p.active = true;
p.drag = drag;
p.grav = grav;
}
burst(x, y, z, n, speed, size, r, g, b, life, grav = 0) {
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2;
const b = Math.acos(Math.random() * 2 - 1);
const s = speed * (0.3 + Math.random() * 0.7);
this.spawn(x, y, z, Math.sin(b) * Math.cos(a) * s, Math.cos(b) * s, Math.sin(b) * Math.sin(a) * s, life * (0.5 + Math.random() * 0.5), size * (0.6 + Math.random() * 0.8), r, g, b, 0.4, grav);
}
}
update(dt) {
for (let i = 0; i < MAX; i++) {
const p = this.pool[i];
if (!p.active)
continue;
p.life -= dt;
if (p.life <= 0) {
p.active = false;
this.pos[i * 3 + 1] = -9999;
continue;
}
const k = Math.max(0, 1 - dt * p.drag);
p.vx *= k;
p.vy *= k;
p.vz *= k;
p.vy -= p.grav * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.z += p.vz * dt;
const j = i * 3;
this.pos[j] = p.x;
this.pos[j + 1] = p.y;
this.pos[j + 2] = p.z;
const f = p.life / p.maxLife;
const bright = p.size > 2 ? 1 : f;
this.col[j] = p.r * bright;
this.col[j + 1] = p.g * bright;
this.col[j + 2] = p.b * bright;
}
this.geo.getAttribute("position").needsUpdate = true;
this.geo.getAttribute("color").needsUpdate = true;
}
}
exports.Particles = Particles;
function makeGlowCanvas(size) {
const c = document.createElement("canvas");
c.width = size;
c.height = size;
const ctx = c.getContext("2d");
const g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
g.addColorStop(0, "rgba(255,255,255,1)");
g.addColorStop(0.3, "rgba(255,255,255,.7)");
g.addColorStop(1, "rgba(255,255,255,0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, size, size);
return c;
}
};
// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Player = void 0;
// Player craft: low-poly mesh, flight physics (momentum / bank / pitch accel), chase camera.
const types_1 = require("./types");
class Player {
constructor(scene) {
this.pos = { x: 0, y: 2, z: 0 };
this.vx = 0;
this.vy = 0;
this.vz = 0;
this.pitch = 0;
this.roll = 0;
this.yaw = 0;
this.alive = true;
this.deadT = 0;
this.boosting = false;
this.wallFlash = 0;
this.hitCd = 0; // post-hit invulnerability
this.speedNorm = 0; // 0..1 for HUD/audio
this.group = new THREE.Group();
this.bodyMat = new THREE.MeshLambertMaterial({ color: 0xd8dfeb, emissive: 0x1a2030, emissiveIntensity: 0.8 });
this.glowMat = new THREE.MeshBasicMaterial({ color: 0xffc36b });
const nose = new THREE.Mesh(new THREE.ConeGeometry(1.15, 5.4, 5), this.bodyMat);
nose.rotation.x = -Math.PI / 2;
nose.position.z = -2.6;
const hull = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.8, 3.4), this.bodyMat);
hull.position.z = 0.6;
const wing = new THREE.Mesh(new THREE.BoxGeometry(5.6, 0.22, 2.1), this.bodyMat);
wing.position.set(0, -0.1, 1.4);
const finL = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.1, 1.5), this.bodyMat);
finL.position.set(-0.8, 0.55, 1.6);
const finR = finL.clone();
finR.position.x = 0.8;
finR.material = this.bodyMat;
const engine = new THREE.Mesh(new THREE.CylinderGeometry(0.6, 0.85, 1.1, 6), this.bodyMat);
engine.rotation.x = Math.PI / 2;
engine.position.set(0, 0.1, 2.35);
const engineGlow = new THREE.Mesh(new THREE.SphereGeometry(0.55, 6, 6), this.glowMat);
engineGlow.position.set(0, 0.1, 2.9);
this.engine = engine;
this.engineGlow = engineGlow;
for (const m of [nose, hull, wing, finL, finR, engine, engineGlow]) {
m.castShadow = true;
this.group.add(m);
}
this.group.position.set(this.pos.x, this.pos.y, this.pos.z);
scene.add(this.group);
}
reset() {
this.pos.x = 0;
this.pos.y = 2;
this.pos.z = 0;
this.vx = 0;
this.vy = 0;
this.vz = types_1.CFG.BASE_SPEED;
this.pitch = 0;
this.roll = 0;
this.yaw = 0;
this.alive = true;
this.deadT = 0;
this.boosting = false;
this.wallFlash = 0;
this.hitCd = 0;
this.group.position.set(0, 2, 0);
this.group.quaternion.identity();
}
// input: ix -1..1 (steer), iy -1..1 (pitch: up positive)
update(dt, ix, iy, cruise, boost, energy) {
if (!this.alive) {
this.deadT += dt;
// tumble down after crash
this.vy -= 40 * dt;
this.pos.x += this.vx * dt * 0.3;
this.pos.y += this.vy * dt;
this.pos.z += this.vz * dt * 0.4;
this.pitch += dt * 3.2;
this.roll += dt * 2.1;
this.applyVisual(1);
return;
}
const maxSpd = boost && energy > types_1.CFG.BOOST_MIN_ENERGY ? types_1.CFG.BOOST_SPEED : cruise;
this.boosting = boost && energy > types_1.CFG.BOOST_MIN_ENERGY && this.vz > cruise * 0.6;
// pitch-based acceleration
let target = cruise;
if (iy > 0.05)
target += types_1.CFG.PITCH_ACCEL * iy * 0.9;
else if (iy < -0.05)
target -= types_1.CFG.PITCH_BRAKE * (-iy) * 0.9;
if (this.boosting)
target = Math.max(target, types_1.CFG.BOOST_SPEED);
const acc = this.boosting ? 140 : (this.vz < target ? types_1.CFG.SPEED_ACCEL : 34);
this.vz += (0, types_1.clamp)(target - this.vz, -acc * dt, acc * dt) * (this.vz < target ? 1 : 1);
this.vz = (0, types_1.clamp)(this.vz, 26, types_1.CFG.BOOST_SPEED);
// lateral momentum toward input direction
const latTarget = ix * types_1.CFG.MAX_LAT_SPEED;
this.vx += (latTarget - this.vx) * Math.min(1, dt * 3.4);
// pitch drives vertical (throttle-like, with slight weight bias)
this.vy += (iy * 30 - 2.0 - this.vy) * Math.min(1, dt * 3.0);
this.vx *= Math.max(0, 1 - types_1.CFG.DRAG_XY * dt * 0.35);
this.pos.x += this.vx * dt;
this.pos.y += this.vy * dt;
this.pos.z -= this.vz * dt;
// soft altitude bounds (canyons don't have floors — but the sky does)
if (this.pos.y < -42) {
this.pos.y = -42;
this.vy = Math.abs(this.vy) * 0.35;
this.wallFlash = Math.max(this.wallFlash, 0.5);
}
if (this.pos.y > 48) {
this.pos.y = 48;
this.vy = -Math.abs(this.vy) * 0.35;
}
if (Math.abs(this.pos.x) > 120) {
this.pos.x = Math.sign(this.pos.x) * 120;
this.vx *= -0.4;
}
// visual attitude
const targetRoll = -ix * 0.85;
const targetPitch = (0, types_1.clamp)(iy * -0.45 + (this.vy / 40) * -0.5, -0.7, 0.7);
this.roll += (targetRoll - this.roll) * Math.min(1, dt * 6);
this.pitch += (targetPitch - this.pitch) * Math.min(1, dt * 5);
this.yaw = Math.atan2(this.vx, this.vz) * -1 * -1; // heading of travel
this.speedNorm = (0, types_1.clamp)((this.vz - 30) / (types_1.CFG.BOOST_SPEED - 30), 0, 1);
this.wallFlash = Math.max(0, this.wallFlash - dt * 2.5);
this.hitCd = Math.max(0, this.hitCd - dt);
this.applyVisual(dt);
// engine glow pulse
const gl = this.boosting ? 1.35 + Math.sin(performance.now() * 0.04) * 0.25 : 0.7 + this.speedNorm * 0.5;
this.engineGlow.scale.setScalar(gl);
this.engineGlow.material.color.setHex(this.boosting ? 0x9be9ff : 0xffc36b);
}
applyVisual(dt) {
this.group.position.set(this.pos.x, this.pos.y, this.pos.z);
const q = new THREE.Quaternion();
q.setFromEuler(new THREE.Euler(this.pitch, this.yaw, this.roll, "YXZ"));
this.group.quaternion.slerp(q, Math.min(1, dt * 10));
}
// chase camera with speed FOV + shake
updateCamera(cam, dt, shake, fovBase, fovBoost, time) {
const back = 9.5 + this.speedNorm * 4.5;
const up = 3.4 + this.speedNorm * 1.4;
const px = this.pos.x - this.vx * 0.12;
const py = this.pos.y + up;
const pz = this.pos.z + back;
const k = Math.min(1, dt * 5.5);
cam.position.x += (px - cam.position.x) * k;
cam.position.y += (py - cam.position.y) * k;
cam.position.z += (pz - cam.position.z) * k;
if (shake > 0.001) {
cam.position.x += (Math.random() - 0.5) * shake;
cam.position.y += (Math.random() - 0.5) * shake * 0.7;
cam.position.z += (Math.random() - 0.5) * shake * 0.5;
}
cam.lookAt(this.pos.x + this.vx * 0.35, this.pos.y + this.vy * 0.3 + 1.2, this.pos.z - 14);
const targetFov = (0, types_1.lerp)(fovBase, fovBoost, this.speedNorm) + Math.sin(time * 13) * shake * 0.6;
cam.fov += (targetFov - cam.fov) * Math.min(1, dt * 4);
cam.updateProjectionMatrix();
}
}
exports.Player = Player;
};
// ── module: src/scene.ts ──
__mods["src/scene.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeSkyTexture = makeSkyTexture;
exports.makeRadialTexture = makeRadialTexture;
exports.makeStarTexture = makeStarTexture;
exports.buildScene = buildScene;
// Scene construction: renderer, sky dome, sun, stars, lights with long shadows.
const types_1 = require("./types");
function makeSkyTexture() {
const c = document.createElement("canvas");
c.width = 4;
c.height = 512;
const ctx = c.getContext("2d");
const g = ctx.createLinearGradient(0, 0, 0, 512);
g.addColorStop(0.0, "#120a2c");
g.addColorStop(0.42, "#3c1b52");
g.addColorStop(0.62, "#8a3568");
g.addColorStop(0.78, "#e06a45");
g.addColorStop(0.9, "#ff9a55");
g.addColorStop(1.0, "#ffc98a");
ctx.fillStyle = g;
ctx.fillRect(0, 0, 4, 512);
return c;
}
function makeRadialTexture(stops) {
const c = document.createElement("canvas");
c.width = c.height = 128;
const ctx = c.getContext("2d");
const g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
for (const [o, col] of stops)
g.addColorStop(o, col);
ctx.fillStyle = g;
ctx.fillRect(0, 0, 128, 128);
return c;
}
function makeStarTexture() {
const c = document.createElement("canvas");
c.width = c.height = 64;
const ctx = c.getContext("2d");
const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
g.addColorStop(0, "rgba(255,240,255,1)");
g.addColorStop(0.25, "rgba(255,230,255,.5)");
g.addColorStop(1, "rgba(255,220,255,0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, 64, 64);
return c;
}
function buildScene(canvas) {
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: "high-performance" });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.6));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(types_1.PAL.fog, 90, 820);
const camera = new THREE.PerspectiveCamera(72, window.innerWidth / window.innerHeight, 0.1, 5000);
camera.position.set(0, 6, 16);
// sky dome
const skyTex = new THREE.CanvasTexture(makeSkyTexture());
const sky = new THREE.Mesh(new THREE.SphereGeometry(4200, 24, 16), new THREE.MeshBasicMaterial({ map: skyTex, side: THREE.BackSide, depthWrite: false, fog: false }));
sky.position.set(0, 0, -1800);
scene.add(sky);
// sun disc + corona (far on the path)
const sunDisc = new THREE.Mesh(new THREE.SphereGeometry(150, 24, 16), new THREE.MeshBasicMaterial({ color: types_1.PAL.sunCore, fog: false }));
sunDisc.position.set(0, 160, types_1.CFG.SUN_Z);
scene.add(sunDisc);
const corona = new THREE.Sprite(new THREE.SpriteMaterial({
map: new THREE.CanvasTexture(makeRadialTexture([
[0, "rgba(255,235,190,.9)"],
[0.25, "rgba(255,180,110,.45)"],
[0.6, "rgba(255,120,80,.12)"],
[1, "rgba(255,100,80,0)"],
])),
blending: THREE.AdditiveBlending,
depthWrite: false,
fog: false,
transparent: true,
}));
corona.scale.setScalar(2600);
corona.position.set(0, 220, types_1.CFG.SUN_Z + 100);
scene.add(corona);
// stars
const starCount = 320;
const sp = new Float32Array(starCount * 3);
const scl = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const a = Math.random() * Math.PI * 2;
const r = Math.random();
const y = 400 + Math.random() * 2600;
sp[i * 3] = Math.cos(a) * 2600 * (0.3 + r * 0.7);
sp[i * 3 + 1] = y;
sp[i * 3 + 2] = -1200 - Math.random() * 2600;
const b = 0.35 + Math.random() * 0.65;
scl[i * 3] = b;
scl[i * 3 + 1] = b * 0.9;
scl[i * 3 + 2] = b * 1.1;
}
const starGeo = new THREE.BufferGeometry();
starGeo.setAttribute("position", new THREE.BufferAttribute(sp, 3));
starGeo.setAttribute("color", new THREE.BufferAttribute(scl, 3));
const stars = new THREE.Points(starGeo, new THREE.PointsMaterial({
size: 7,
map: new THREE.CanvasTexture(makeStarTexture()),
vertexColors: true,
transparent: true,
depthWrite: false,
sizeAttenuation: true,
fog: false,
}));
stars.frustumCulled = false;
scene.add(stars);
// lights
scene.add(new THREE.AmbientLight(0x6a4478, 0.95));
const hemi = new THREE.HemisphereLight(0xffb070, 0x3a1a4a, 0.65);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffc98a, 1.5);
sun.position.set(-180, 130, 20);
sun.castShadow = true;
sun.shadow.mapSize.x = 1024;
sun.shadow.mapSize.y = 1024;
const cam = sun.shadow.camera;
cam.left = -150;
cam.right = 150;
cam.top = 120;
cam.bottom = -120;
cam.near = 1;
cam.far = 700;
sun.shadow.bias = -0.0004;
const target = new THREE.Object3D();
target.position.set(0, 0, -260);
scene.add(target);
sun.target = target;
scene.add(sun);
return { renderer, scene, camera, sun, sunDisc, corona, stars };
}
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
exports.boot = boot;
// SUNDRIFT — game state machine, loop, collisions, scoring, HUD wiring.
const types_1 = require("./types");
const scene_1 = require("./scene");
const worldgen_1 = require("./worldgen");
const player_1 = require("./player");
const particles_1 = require("./particles");
const audio_1 = require("./audio");
const $ = (id) => document.getElementById(id);
const fmt = (n) => Math.floor(n).toString().padStart(7, "0");
const MENU_PANEL = `
<div class="p-kicker">high-speed procedural canyon racer</div>
<h1 class="p-title">SUN<span>DRIFT</span></h1>
<p class="p-sub">Carve the endless dusk canyon. Every wall you kiss pays. Run out of energy and the sky closes in.</p>
<div class="p-keys">
<div class="k-row"><span>STEER · BANK</span><kbd>A D</kbd><kbd>◄ ►</kbd></div>
<div class="k-row"><span>PITCH · BRAKE / BURN</span><kbd>S W</kbd><kbd>▼ ▲</kbd></div>
<div class="k-row"><span>TURBO BURN</span><kbd>SHIFT</kbd></div>
<div class="k-row"><span>RESTART</span><kbd>R</kbd></div>
</div>
<button id="startBtn" class="p-btn">LAUNCH <em>▶</em></button>
<div class="p-hint">near-misses stack the multiplier · orbs refill energy · turbo builds on straightaways</div>`;
class Game {
constructor() {
this.phase = "menu";
this.stats = { score: 0, mult: 1, maxSpeed: 0, distance: 0, nearMisses: 0, orbs: 0, time: 0, topMult: 1 };
this.energy = 100;
this.turbo = 0;
this.boostHeld = false;
this.keys = {};
this.time = 0;
this.shake = 0;
this.multPulseT = 0;
this.orbPulse = 0;
this.nmCredit = new Set();
this.lastMultUp = 1;
this.trailAcc = 0;
this.dustAcc = 0;
this.boostOn = false;
this.overAt = 0;
this.lastT = performance.now();
const canvas = $("gl");
this.H = (0, scene_1.buildScene)(canvas);
this.world = new worldgen_1.World(this.H.scene, this.H.sun);
this.player = new player_1.Player(this.H.scene);
this.parts = new particles_1.Particles(this.H.scene, particles_1.makeGlowCanvas);
this.world.score = 0;
for (let i = 0; i < 12; i++)
this.world.buildChunkPublic(i);
window.addEventListener("keydown", (e) => this.onKey(e, true));
window.addEventListener("keyup", (e) => this.onKey(e, false));
window.addEventListener("blur", () => { this.keys = {}; if (this.phase === "playing")
this.setPhase("paused"); });
window.addEventListener("resize", () => this.onResize());
$("startBtn").addEventListener("click", () => this.start());
}
// ---------------- input ----------------
onKey(e, down) {
const k = e.key.toLowerCase();
if (["arrowup", "arrowdown", "arrowleft", "arrowright", " "].includes(k))
e.preventDefault();
this.keys[k] = down;
if (!down)
return;
if (k === " " && (this.phase === "menu" || (this.phase === "dead" && this.time - this.overAt > 1.2)))
this.start();
if (k === "r" && this.phase !== "menu")
this.start();
if (k === "escape") {
if (this.phase === "playing")
this.setPhase("paused");
else if (this.phase === "paused")
this.setPhase("playing");
}
if (k === "m")
audio_1.sfx.setMuted(!audio_1.sfx.isMuted());
}
inputAxis() {
const k = this.keys;
const ix = (k["d"] || k["arrowright"] ? 1 : 0) - (k["a"] || k["arrowleft"] ? 1 : 0);
const iy = (k["w"] || k["arrowup"] ? 1 : 0) - (k["s"] || k["arrowdown"] ? 1 : 0);
return { ix, iy };
}
// ---------------- flow ----------------
start() {
audio_1.sfx.init();
audio_1.sfx.uiClick();
this.world.disposeAll();
this.world.score = 0;
for (let i = 0; i < 12; i++)
this.world.buildChunkPublic(i);
this.player.reset();
this.stats = { score: 0, mult: 1, maxSpeed: 0, distance: 0, nearMisses: 0, orbs: 0, time: 0, topMult: 1 };
this.energy = 100;
this.turbo = 0;
this.nmCredit.clear();
this.lastMultUp = 1;
this.time = 0;
this.shake = 0;
this.lastT = performance.now();
const ov = $("overlay");
ov.classList.remove("hidden");
this.setPhase("playing");
}
setPhasePublic(p) {
this.setPhase(p);
}
startPublic() {
this.start();
}
setPhase(p) {
this.phase = p;
const ov = $("overlay");
const panel = $("panel");
if (p === "dead") {
const reason = this.energy <= 0 ? "ENERGY DEPLETED" : "HULL IMPACT";
panel.innerHTML = `
<div class="p-kicker">run terminated — ${reason}</div>
<h1 class="p-title">GAME <span>OVER</span></h1>
<div class="stat-grid">
<div class="stat"><b>${fmt(this.stats.score)}</b><i>FINAL SCORE</i></div>
<div class="stat"><b>${Math.round(this.stats.maxSpeed)}<small style="font-size:14px"> km/h</small></b><i>MAX SPEED</i></div>
<div class="stat"><b>x${Math.max(1, this.stats.topMult)}</b><i>BEST MULTIPLIER</i></div>
</div>
<button id="startBtn" class="p-btn">RESTART <em>⟳</em></button>
<div class="p-hint">near-misses stack the multiplier · orbs refill energy · turbo builds on straightaways</div>`;
document.getElementById("startBtn").addEventListener("click", () => this.start());
ov.classList.remove("hidden");
ov.classList.add("death");
}
else if (p === "menu") {
ov.classList.remove("hidden", "death");
panel.innerHTML = MENU_PANEL;
document.getElementById("startBtn").addEventListener("click", () => this.start());
}
if (p === "playing" || p === "paused")
ov.classList.add("hidden");
if (p === "paused")
$("paused").hidden = false;
else
$("paused").hidden = true;
}
die() {
if (this.phase !== "playing")
return;
this.player.alive = false;
audio_1.sfx.crash();
audio_1.sfx.turbo(false);
this.boostOn = false;
this.shake = 1.4;
const p = this.player.pos;
this.parts.burst(p.x, p.y, p.z, 90, 26, 2.6, 1.0, 0.45, 0.15, 1.4, 20);
this.parts.burst(p.x, p.y, p.z, 40, 12, 3.4, 0.4, 0.9, 1.0, 1.0);
this.overAt = this.time;
setTimeout(() => this.setPhase("dead"), 900);
}
// ---------------- collision + near-miss ----------------
collectInv() {
const s = [];
const b = [];
const pz = this.player.pos.z;
for (let gi = 0; gi < this.world.chunks.length; gi++) {
const c = this.world.chunks[gi];
// include chunks overlapping [pz-130, pz+60] (ahead is more negative z)
if (c.z0 > pz + 60 || c.z1 < pz - 130)
continue;
for (let i = 0; i < c.spheres.length; i++) {
const sp = c.spheres[i];
s.push({ cx: sp.cx, cy: sp.cy, cz: sp.cz, r: sp.r, type: sp.type, g: gi, si: i });
}
for (let i = 0; i < c.boxes.length; i++) {
const bx = c.boxes[i];
b.push({ cx: bx.cx, cy: bx.cy, cz: bx.cz, hx: bx.hx, hy: bx.hy, hz: bx.hz, g: gi });
}
}
return { s, b };
}
handleCollisions(inv) {
const pl = this.player;
const px = pl.pos.x, py = pl.pos.y, pz = pl.pos.z;
// hits (AABB vs point) — with a short post-hit invulnerability window
const hr = types_1.CFG.HIT_R;
if (pl.hitCd <= 0) {
for (let i = 0; i < inv.b.length; i++) {
const bx = inv.b[i];
if (Math.abs(px - bx.cx) < bx.hx + hr &&
Math.abs(py - bx.cy) < bx.hy + hr &&
Math.abs(pz - bx.cz) < bx.hz + hr) {
this.energy -= types_1.CFG.HIT_E_LOSS;
pl.vz *= 0.68;
pl.vx = -Math.sign(px - bx.cx || 1) * 20;
pl.vy += 5;
pl.hitCd = 1.25;
pl.wallFlash = 1;
this.shake = Math.max(this.shake, 1.0);
this.stats.mult = Math.max(1, Math.round(this.stats.mult * types_1.CFG.HIT_MULT_KEEP));
// surviving a clip counts as a near-miss: the wall forgives once
this.stats.nearMisses++;
this.stats.score += types_1.CFG.SCORE_NM * 0.4 * this.stats.mult;
this.multPulseT = 1;
audio_1.sfx.whoosh(0.9);
this.parts.burst(px, py, pz, 16, 14, 2, 1, 0.5, 0.2, 0.7);
if (this.energy <= 0) {
this.energy = 0;
this.die();
return;
}
break;
}
}
}
// orbs
for (let i = 0; i < inv.s.length; i++) {
const sp = inv.s[i];
if (sp.type !== "orb")
continue;
const dx = px - sp.cx, dy = py - sp.cy, dz = pz - sp.cz;
if (dx * dx + dy * dy + dz * dz < 3.6 * 3.6) {
this.energy = Math.min(100, this.energy + types_1.CFG.ORB_ENERGY);
this.stats.orbs++;
this.stats.score += types_1.CFG.SCORE_ORB * this.stats.mult;
this.turbo = Math.min(100, this.turbo + 7);
this.orbPulse = 1;
this.nmCredit.delete(i);
audio_1.sfx.orb(this.stats.orbs);
const m = this.world.chunks[sp.g]?.spheres[sp.si];
if (m && m.mesh) {
m.mesh.scale.setScalar(0.001);
m.mesh.visible = false;
}
}
}
// near-miss scoring
const pcx = this.player.pos.x, pcy = this.player.pos.y, pcz = this.player.pos.z;
for (let i = 0; i < inv.s.length; i++) {
const sp = inv.s[i];
if (sp.type === "orb")
continue;
const dx = pcx - sp.cx, dy = pcy - sp.cy, dz = pcz - sp.cz;
const d2 = dx * dx + dy * dy + dz * dz;
const R = sp.r + types_1.CFG.NM_GRACE;
if (d2 < R * R) {
if (!this.nmCredit.has(i)) {
this.nmCredit.add(i);
const d = Math.sqrt(d2);
const closeness = 1 - d / R;
const bonus = 1 + closeness * 3 + (sp.type === "squeeze" ? 0.4 : 0);
const multBefore = this.stats.mult;
this.stats.mult = Math.min(99, 1 + Math.round((this.stats.mult - 1) * 0.35 + 1 * bonus));
if (this.stats.mult > multBefore)
this.lastMultUp = this.stats.mult;
this.stats.mult = Math.max(this.stats.mult, 1);
this.stats.score += types_1.CFG.SCORE_NM * closeness * this.stats.mult;
this.stats.nearMisses++;
this.stats.topMult = Math.max(this.stats.topMult, this.stats.mult);
this.shake = Math.max(this.shake, 0.28 + closeness * 0.5);
this.multPulseT = 1;
audio_1.sfx.whoosh(0.25 + closeness * 0.75);
if (closeness > 0.45) {
this.parts.burst(pcx + (sp.cx - pcx) * 0.18, pcy, pcz, 10, 9, 1.6, 1, 0.85, 0.5, 0.5);
}
}
}
}
}
// ---------------- particles fx ----------------
emitFx(dt) {
const p = this.player;
if (p.alive) {
// engine trail
this.trailAcc += dt * (60 + p.speedNorm * 160);
while (this.trailAcc > 1) {
this.trailAcc -= 1;
const bx = p.pos.x - p.vx * 0.06 + (Math.random() - 0.5) * 0.7;
const by = p.pos.y - 0.3 + (Math.random() - 0.5) * 0.7;
const bz = p.pos.z + 3.2;
const boost = p.boosting;
this.parts.spawn(bx, by, bz, (Math.random() - 0.5) * 3, -1 - Math.random() * 3, 4 + Math.random() * 6, boost ? 0.5 : 0.32, boost ? 3.0 : 2.0, boost ? 0.45 : 1.0, boost ? 0.85 : 0.62, boost ? 1.0 : 0.3, 0.6, 0);
}
// dust / debris streaking past
this.dustAcc += dt * (8 + p.speedNorm * 26);
while (this.dustAcc > 1) {
this.dustAcc -= 1;
const side = Math.random() < 0.5 ? -1 : 1;
const dx = p.pos.x + side * (10 + Math.random() * 26);
const dy = p.pos.y - 6 + Math.random() * 16;
const dz = p.pos.z + 6 + Math.random() * 18;
this.parts.spawn(dx, dy, dz, (Math.random() - 0.5) * 4, (Math.random() - 0.5) * 4, -p.vz * 0.55, 0.9, 2.6 + Math.random() * 3.4, 0.75, 0.45, 0.38, 0, 0);
}
}
}
// ---------------- HUD ----------------
hud() {
const e = this.energy / 100;
const fill = $("energy-fill");
fill.style.width = `${Math.max(0, this.energy)}%`;
fill.style.filter = `hue-rotate(${(0, types_1.lerp)(0, -50, this.energy < 30 ? (30 - this.energy) / 30 : 0)}deg)`;
$("energy").classList.toggle("low", this.energy < 28);
const kmh = Math.round(this.player.vz * 2.2);
$("speedo-fill").style.height = `${(0, types_1.clamp)((this.player.vz - 26) / (types_1.CFG.BOOST_SPEED - 26), 0, 1) * 100}%`;
$("speedo-num").textContent = kmh.toString().padStart(3, "0");
$("mult-n").textContent = Math.max(1, this.stats.mult).toString();
$("mult-x").textContent = "x";
$("score").textContent = fmt(this.stats.score);
if (this.multPulseT > 0) {
this.multPulseT -= 0.016;
const m = $("mult");
if (this.multPulseT > 0.9)
m.classList.add("pulse", "hot");
else
m.classList.remove("pulse", "hot");
}
if (this.multPulseT <= 0 && this.multPulseT > -0.1) {
$("mult").classList.remove("pulse", "hot");
}
const t = this.turbo / 100;
$("turbo-fill").style.width = `${this.turbo}%`;
$("turbo-wrap").classList.toggle("ready", this.turbo >= 100 || this.boostOn);
void t;
// near badge
if (this.multPulseT > 0.4) {
const b = $("near-badge");
if (!b.classList.contains("show")) {
b.classList.remove("show");
void b.offsetWidth;
b.classList.add("show");
}
}
// tints
const hurt = this.player.wallFlash * 0.85 + (this.energy < 22 ? (22 - this.energy) / 22 * 0.5 : 0);
$("tint").style.opacity = (0, types_1.clamp)(hurt, 0, 0.9).toFixed(3);
const boostGlow = this.player.boosting ? 0.55 + Math.sin(this.time * 20) * 0.1 : 0;
$("boostTint").style.opacity = boostGlow.toFixed(3);
// letterbox
const h = (0, types_1.lerp)(6, 16, this.player.speedNorm);
$("bar-top").style.height = `${h}vh`;
$("bar-bottom").style.height = `${h}vh`;
// orb pulse ring (visual flourish: energy bar flashes on pickup)
if (this.orbPulse > 0)
this.orbPulse = Math.max(0, this.orbPulse - 0.04);
}
// ---------------- loop ----------------
onResize() {
const w = window.innerWidth, h = window.innerHeight;
this.H.camera.aspect = w / h;
this.H.camera.updateProjectionMatrix();
this.H.renderer.setSize(w, h);
}
tick() {
const now = performance.now();
let dt = (now - this.lastT) / 1000;
this.lastT = now;
dt = (0, types_1.clamp)(dt, 0, 0.05);
this.time += dt;
const { ix, iy } = this.inputAxis();
this.boostHeld = !!(this.keys["shift"] || this.keys[" "]);
const cruise = (0, types_1.cruiseTarget)(this.stats.score);
if (this.phase === "playing") {
const p = this.player;
// boost logic
const wantBoost = this.boostHeld && this.energy > types_1.CFG.BOOST_MIN_ENERGY && p.vz > cruise * 0.5;
if (wantBoost && !this.boostOn) {
this.boostOn = true;
audio_1.sfx.turbo(true);
}
if ((!wantBoost || this.energy <= types_1.CFG.BOOST_MIN_ENERGY) && this.boostOn) {
this.boostOn = false;
audio_1.sfx.turbo(false);
}
if (this.boostOn)
this.energy = Math.max(0, this.energy - types_1.CFG.BOOST_DRAIN * dt);
p.update(dt, ix, iy, cruise, this.boostOn, this.energy);
// energy decay — running dry ends the run
this.energy = Math.max(0, this.energy - types_1.CFG.ENERGY_DECAY * dt);
if (this.energy <= 0) {
this.die();
}
// turbo meter
if (Math.abs(ix) < 0.15 && Math.abs(iy) < 0.15)
this.turbo = Math.min(100, this.turbo + dt * 3.2);
else
this.turbo = Math.max(0, this.turbo - dt * 1.6);
// scoring by speed
this.stats.score += this.player.vz * dt * types_1.CFG.SCORE_SPEED * this.stats.mult;
this.stats.distance += this.player.vz * dt;
this.stats.time += dt;
this.stats.maxSpeed = Math.max(this.stats.maxSpeed, this.player.vz * 2.2);
// world gen + cull
this.world.score = this.stats.score;
this.world.ensure(p.pos.z, types_1.CFG.SPAWN_AHEAD);
this.world.cull(p.pos.z);
// collisions
const inv = this.collectInv();
this.handleCollisions(inv);
if (this.phase === "playing")
this.emitFx(dt);
// camera + light rig
const shake = this.shake + (p.boosting ? 0.06 : 0);
p.updateCamera(this.H.camera, dt, shake, 70, 92, this.time);
this.shake *= Math.pow(0.02, dt);
const H = this.H;
H.sun.position.set(-180, 130, p.pos.z + 20);
H.sun.target.position.set(0, 0, p.pos.z - 260);
H.sunDisc.position.z = p.pos.z + types_1.CFG.SUN_Z;
H.corona.position.z = p.pos.z + types_1.CFG.SUN_Z + 100;
// orb spin + pulse
const om = this.world.orbMat;
om.emissiveIntensity = 1.5 + Math.sin(this.time * 5) * 0.5 + this.orbPulse * 2;
for (const c of this.world.chunks) {
for (const sp of c.spheres) {
if (sp.type === "orb" && sp.mesh && sp.mesh.visible) {
sp.mesh.rotation.y += dt * 2;
sp.mesh.rotation.x += dt * 1.3;
}
}
}
}
else if (this.phase === "menu") {
// idle fly-through
this.menuFly(dt);
}
else if (this.phase === "dead" && this.player.deadT < 6) {
this.player.update(dt, 0, 0, 0, false, 0);
this.player.updateCamera(this.H.camera, dt, this.shake, 70, 92, this.time);
this.shake *= Math.pow(0.02, dt);
}
// audio + particles always
audio_1.sfx.setEngine(this.player.speedNorm, this.player.boosting, this.phase === "playing");
this.parts.update(dt);
this.hud();
this.H.renderer.render(this.H.scene, this.H.camera);
}
menuFly(dt) {
const p = this.player;
if (p.pos.z > -2600)
p.pos.z -= 42 * dt;
else
p.pos.z = 0;
p.pos.x = Math.sin(this.time * 0.32) * 12;
p.pos.y = 3 + Math.sin(this.time * 0.5) * 3;
p.vx = Math.cos(this.time * 0.32) * 12 * 0.32;
p.vy = Math.cos(this.time * 0.5) * 1.5;
p.vz = 42;
p.speedNorm = 0.2;
p.updateCamera(this.H.camera, dt, 0, 66, 72, this.time);
this.world.score = 0;
this.world.ensure(p.pos.z, types_1.CFG.SPAWN_AHEAD);
this.world.cull(p.pos.z);
const H = this.H;
H.sun.position.set(-180, 130, p.pos.z + 20);
H.sun.target.position.set(0, 0, p.pos.z - 260);
H.sunDisc.position.z = p.pos.z + types_1.CFG.SUN_Z;
H.corona.position.z = p.pos.z + types_1.CFG.SUN_Z + 100;
}
}
exports.Game = Game;
function boot() {
const g = new Game();
g.setPhasePublic("menu");
window.__game = g;
const loop = () => {
g.tick();
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
return g;
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"78d09537-5866-4bd7-83cf-dc6b549dbcac","tokensIn":7465521,"tokensOut":231636,"tokensTotal":7697157,"cost":0,"turns":110,"toolCalls":160,"failedToolCalls":2,"timestamp":"2026-08-15T13:06:34.967Z"} -->