// 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); }, }; }