guts/web/js/boot.js
jing 4ccf5edcc0 [lane F] Round 1 integration + ROUND2 instructions: the game is a game
Wired B's player/combat + event pump, D's assets, C's level pick into boot.
Fixed qa ESM gate (node --check no-op, found by B) + added spline selfcheck.
TECH: world contract FROZEN v1.1, bus events ratified, colorspace + crest-speed
laws (CREST_FACTOR 1.6 — B proved surfing lost to throttle at 1.0). Stub complies.
Verified integrated build via 60s stepped sim: 9 draws, 0 errors, 0 asset misses;
one known gap (fiction-id spawn resolve) confirmed and assigned as B's top item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:39:02 +10:00

150 lines
6.0 KiB
JavaScript

// boot.js (Lane F) — the shell. Creates renderer/scene/loop, picks the world (real or
// stub), constructs assets + player + combat, pumps level events, hosts DBG.
// Lanes: request wiring via NOTES, don't edit here. Round 2: A/B/C/D snippets integrated.
import * as THREE from 'three';
import { parseFlags } from './core/flags.js';
import { createRng } from './core/rng.js';
import { createBus } from './core/bus.js';
import { createAssets } from './core/assets.js';
import { createWorld as createStubWorld, STUB_LEVEL } from './stub/world_stub.js';
import { createPlayer } from './flight/player.js';
import { createCombat } from './combat/index.js';
const flags = parseFlags();
const bus = createBus();
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.getElementById('game').appendChild(renderer.domElement);
const assets = await createAssets({ flags, renderer });
async function loadLevel(id) {
try {
const mod = await import('./levels/index.js'); // Lane C's registry
return mod.getLevel(id); // default level lives there
} catch (e) {
console.info('[boot] Lane C registry not available —', e.message);
return null;
}
}
async function pickWorld() {
const level = (await loadLevel(flags.lvl)) ?? STUB_LEVEL;
const rng = createRng(flags.seed ?? level.seed);
if (!flags.stub) {
try {
const mod = await import('./world/index.js'); // Lane A's world
return mod.createWorld(level, { rng, assets });
} catch (e) {
console.info('[boot] Lane A world not available, using stub —', e.message);
}
}
return createStubWorld(level, { rng, assets }); // ?stub=1&lvl=<id> composes (beat 1 only — stub is single-segment)
}
const world = await pickWorld();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);
scene.add(world.group);
scene.background = new THREE.Color(world.biomeAt(0).palette.void);
// --- player + combat (Lane B). ?fly=1 keeps the noclip/drift camera instead. ---
const rng = createRng(flags.seed ?? world.level?.seed ?? 0);
const player = flags.fly ? null
: createPlayer({ scene, world, bus, rng, flags, camera, dom: renderer.domElement });
const combat = player ? createCombat({ scene, world, bus, rng, flags, player }) : null;
// --- level-event pump (owned by boot per round-2 ruling): emits C's events as the
// player crosses their s. Checkpoint/respawn replay semantics: revisit with B in round 2.
let _evts = [...(world.level?.events ?? [])].sort((a, b) => a.s - b.s), _ei = 0;
const pumpTo = (s) => { while (_ei < _evts.length && s >= _evts[_ei].s) bus.emit('level:event', _evts[_ei++]); };
// --- debug drift/fly camera (?fly=1, or fallback when B's player is absent) ---
let sCam = 5, look = { yaw: 0, pitch: 0 }, dragging = false, speed = world.biomeAt(0).flow * 0.6;
const keys = new Set();
addEventListener('keydown', (e) => keys.add(e.code));
addEventListener('keyup', (e) => keys.delete(e.code));
addEventListener('mousedown', () => (dragging = true));
addEventListener('mouseup', () => (dragging = false));
addEventListener('mousemove', (e) => {
if (!dragging || player) return;
look.yaw -= e.movementX * 0.003;
look.pitch = THREE.MathUtils.clamp(look.pitch - e.movementY * 0.003, -1.2, 1.2);
});
function updateCamera(dt) {
if (flags.fly) {
if (keys.has('KeyW')) sCam += speed * 2 * dt;
if (keys.has('KeyS')) sCam -= speed * 2 * dt;
} else {
sCam += speed * dt; // title-screen auto-drift
}
sCam = ((sCam % world.length) + world.length) % world.length;
const f = world.sample(sCam), ahead = world.sample(Math.min(sCam + 12, world.length));
camera.position.copy(f.pos).addScaledVector(f.nor, -f.radius * 0.25);
const target = ahead.pos.clone()
.addScaledVector(f.bin, Math.sin(look.yaw) * 8)
.addScaledVector(f.nor, Math.sin(look.pitch) * 8);
camera.up.copy(f.nor);
camera.lookAt(target);
}
// --- DBG (?dbg=1) — cite these numbers in NOTES; DBG.shot('name') saves a frame ---
const dbgEl = document.getElementById('dbg');
let frames = 0, fpsT = 0, fps = 0;
renderer.info.autoReset = false; // three resets info at render end — cache per-frame instead
const stats = { draws: 0, tris: 0 };
window.DBG = {
get draws() { return stats.draws; },
get tris() { return stats.tris; },
get fps() { return fps; },
world, player, combat, assets,
shot(name = 'shot') {
const a = document.createElement('a');
a.download = `${name}.png`;
a.href = renderer.domElement.toDataURL('image/png');
a.click();
},
};
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let last = performance.now();
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.05);
last = now;
if (player) {
player.update(dt); // must run first: combat hit-tests this frame's ship position
pumpTo(player.state.s);
combat.update(dt);
world.update(dt, player.state.s);
} else {
updateCamera(dt); // ?fly=1 noclip / title drift
world.update(dt, sCam);
}
renderer.render(scene, camera);
stats.draws = renderer.info.render.calls;
stats.tris = renderer.info.render.triangles;
renderer.info.reset();
frames++; fpsT += dt;
if (fpsT >= 0.5) {
fps = Math.round(frames / fpsT); frames = 0; fpsT = 0;
if (flags.dbg && !flags.shots) {
const s = player?.state.s ?? sCam;
dbgEl.textContent = `${fps} fps · ${DBG.draws} draws · ${(DBG.tris / 1000) | 0}k tris · s=${s.toFixed(0)} · ${world.level?.id ?? '?'} ${world.hash?.() ?? ''}`;
}
}
requestAnimationFrame(frame);
}
if (flags.dbg && !flags.shots) dbgEl.style.display = 'block';
requestAnimationFrame(frame);
export { scene, camera, renderer, bus, flags, world, player, combat, assets };