GDD v1 (flow-locked hybrid movement, 5 biomes + secret, boss roster), TECH contracts (world API, level schema v0, bus events, manifest law), ART_BIBLE (synthetic scanner look), PIPELINE (MODELBEAST-first, fal gated), PROCESS (PROCITY lane/round law), charters A-F + ROUND1 instructions. Seed code: shell + boot + core (rng/bus/flags) + stub world (peristalsis shader tube, 1 draw / 102k tris, contract-complete) + qa.sh (GREEN). Verified in-browser; evidence docs/shots/laneF/round0_stub_tube.png. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
118 lines
4.4 KiB
JavaScript
118 lines
4.4 KiB
JavaScript
// boot.js (Lane F) — the shell. Creates renderer/scene/loop, picks the world (real or
|
|
// stub), hosts the debug camera + DBG. Lanes: request wiring via NOTES, don't edit here.
|
|
|
|
import * as THREE from 'three';
|
|
import { parseFlags } from './core/flags.js';
|
|
import { createRng } from './core/rng.js';
|
|
import { createBus } from './core/bus.js';
|
|
import { createWorld as createStubWorld, STUB_LEVEL } from './stub/world_stub.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 scene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);
|
|
|
|
async function pickWorld() {
|
|
if (!flags.stub) {
|
|
try {
|
|
const mod = await import('./world/index.js'); // Lane A's world, when it lands
|
|
const level = await loadLevel(flags.lvl);
|
|
return mod.createWorld(level, { rng: createRng(flags.seed ?? level.seed) });
|
|
} catch (e) {
|
|
console.info('[boot] Lane A world not available, using stub —', e.message);
|
|
}
|
|
}
|
|
return createStubWorld(STUB_LEVEL, { rng: createRng(flags.seed ?? STUB_LEVEL.seed) });
|
|
}
|
|
async function loadLevel(id) {
|
|
const mod = await import('./levels/index.js'); // Lane C's registry, when it lands
|
|
return mod.getLevel(id);
|
|
}
|
|
|
|
const world = await pickWorld();
|
|
scene.add(world.group);
|
|
scene.background = new THREE.Color(world.biomeAt(0).palette.void);
|
|
|
|
// --- debug drift/fly camera (placeholder until Lane B's player rig; ?fly=1 = manual) ---
|
|
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) 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,
|
|
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;
|
|
world.update(dt, sCam);
|
|
updateCamera(dt);
|
|
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)
|
|
dbgEl.textContent = `${fps} fps · ${DBG.draws} draws · ${(DBG.tris / 1000) | 0}k tris · s=${sCam.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 }; // read-only handles for lanes
|