Implements THE WORLD CONTRACT (TECH.md) in web/js/world/; drop-in for the stub. boot.js now runs this on C's L2_esophagus (3600u, hash cefc4f83), zero console errors. - spline.js: centreline + arclength LUTs + parallel-transport frames + radius law + peristalsis phase + project() + hash(). No three.js import, so qa can run its 14-assertion selfcheck headless. - curviness is solved from a curvature budget, not authored as an amplitude: the pinch guard caught the tube folding through itself on a wide+curvy join (turn radius 9.9 vs tube radius 17.2). C can no longer author a pinch. - peristalsis: global OMEGA, k(s) = OMEGA/flow(s), phase K(s) = integral k ds. A crest therefore travels at exactly the local flow speed, so "surf the crest" == "ride the current". Esophagus wave amp 0.9 -> 1.4 (measured): moves wallRho for Lane B. - tube.js: chunked streaming, seams aligned to biome joins, arenas own their s-span. - arena.js: displaced icosphere shells v0 (three's polyhedron detail is 20*(detail+1)^2, not 20*4^detail — 720 tris was far too coarse); fog sized to the room, since biome fog tuned for 100u corridors renders C's 360u acid sea as a black rectangle. - index.js: prefers assets.texture() over get() (get returns raw JSON a shader can't eat), plus an explicit slug map — D's wall_smallint_a vs biome id small_intestine would miss silently forever under the assets-optional law. Measured (C's real L2 + D's real pack, 1920x1080, renderer.info): 8 draws worst frame (budget 150), 74k tris (500k), no geometry leak across a full sweep, <120ms load, pinch ratio 3.32. fps not claimed — the screenshot pane runs tabs hidden, pausing rAF. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.9 KiB
JavaScript
71 lines
2.9 KiB
JavaScript
// world/flycam.js (Lane A) — `?fly=1` noclip inspection camera.
|
|
// Owed to the other lanes per LANE_A_WORLD.md: a way to look at the canal without Lane B's
|
|
// player existing. True noclip (free 6DOF, ignores world.collide) — it is a debug tool, not
|
|
// a preview of arena-mode flight; Lane B owns how the ship actually feels.
|
|
//
|
|
// Not wired into boot.js yet: boot owns the camera and `?fly=1` currently drives a rail cam.
|
|
// The exact patch is offered in LANE_A_NOTES.md §-> Lane F.
|
|
//
|
|
// W/S forward/back · A/D strafe · Q/E down/up · Shift boost · drag to look · R reset to start
|
|
|
|
import * as THREE from 'three';
|
|
|
|
export function createFlyCam({ camera, dom = window, world, speed = 35 }) {
|
|
const keys = new Set();
|
|
let yaw = 0, pitch = 0, dragging = false;
|
|
|
|
function reset() {
|
|
const f = world.sample(4);
|
|
camera.position.copy(f.pos).addScaledVector(f.tan, -6);
|
|
yaw = Math.atan2(-f.tan.x, -f.tan.z);
|
|
pitch = Math.asin(THREE.MathUtils.clamp(f.tan.y, -1, 1));
|
|
}
|
|
|
|
const onDown = (e) => { keys.add(e.code); if (e.code === 'KeyR') reset(); };
|
|
const onUp = (e) => keys.delete(e.code);
|
|
const onBlur = () => keys.clear();
|
|
const onMouseDown = () => { dragging = true; };
|
|
const onMouseUp = () => { dragging = false; };
|
|
const onMouseMove = (e) => {
|
|
if (!dragging) return;
|
|
yaw -= e.movementX * 0.003;
|
|
pitch = THREE.MathUtils.clamp(pitch - e.movementY * 0.003, -1.5, 1.5);
|
|
};
|
|
|
|
addEventListener('keydown', onDown);
|
|
addEventListener('keyup', onUp);
|
|
addEventListener('blur', onBlur);
|
|
dom.addEventListener('mousedown', onMouseDown);
|
|
addEventListener('mouseup', onMouseUp);
|
|
addEventListener('mousemove', onMouseMove);
|
|
reset();
|
|
|
|
const fwd = new THREE.Vector3(), right = new THREE.Vector3(), up = new THREE.Vector3(0, 1, 0);
|
|
|
|
return {
|
|
get position() { return camera.position; },
|
|
reset,
|
|
update(dt) {
|
|
camera.up.copy(up); // noclip stays world-up: no frame roll to fight
|
|
camera.rotation.set(pitch, yaw, 0, 'YXZ');
|
|
fwd.set(0, 0, -1).applyEuler(camera.rotation);
|
|
right.set(1, 0, 0).applyEuler(camera.rotation);
|
|
const v = speed * (keys.has('ShiftLeft') || keys.has('ShiftRight') ? 3 : 1) * dt;
|
|
if (keys.has('KeyW')) camera.position.addScaledVector(fwd, v);
|
|
if (keys.has('KeyS')) camera.position.addScaledVector(fwd, -v);
|
|
if (keys.has('KeyD')) camera.position.addScaledVector(right, v);
|
|
if (keys.has('KeyA')) camera.position.addScaledVector(right, -v);
|
|
if (keys.has('KeyE')) camera.position.addScaledVector(up, v);
|
|
if (keys.has('KeyQ')) camera.position.addScaledVector(up, -v);
|
|
},
|
|
dispose() {
|
|
removeEventListener('keydown', onDown);
|
|
removeEventListener('keyup', onUp);
|
|
removeEventListener('blur', onBlur);
|
|
dom.removeEventListener('mousedown', onMouseDown);
|
|
removeEventListener('mouseup', onMouseUp);
|
|
removeEventListener('mousemove', onMouseMove);
|
|
},
|
|
};
|
|
}
|