diff --git a/web/world/js/main.js b/web/world/js/main.js index 8c0ea6a..f988397 100644 --- a/web/world/js/main.js +++ b/web/world/js/main.js @@ -189,6 +189,10 @@ export async function boot(opts = {}) { // --- world & camera ----------------------------------------------------- const world = createWorld(scene, { wind }); + // Lane E's GLBs land over the graybox. Awaited here because it resolves + // world.shedTable onto E's baked pickup_anchor, and wireYardActions (below, + // inside rigSail) reads that position when it registers the spare pickup. + await world.dress(); const cameraRig = createCameraRig(canvas); cameraRig.setSolids(world.solids); cameraRig.setGround(world.heightAt); diff --git a/web/world/js/world.js b/web/world/js/world.js index 9eca335..48abd22 100644 --- a/web/world/js/world.js +++ b/web/world/js/world.js @@ -35,6 +35,11 @@ export function heightAt(x, z) { const GARDEN_BED = { x: 1, z: 2, w: 6, d: 4 }; +// Shed on the east side, table out in front of it. Lane D tested reachability +// against (9, 6), so the table stays there and the shed tucks in behind. +const SHED = { x: 11.8, z: 6.2, rotY: -Math.PI / 2 }; +const SHED_TABLE = { x: 9, z: 6, rotY: -Math.PI / 2 }; + // Sun: mid-afternoon, high and off the north-west shoulder. Elevation 55°. // Stored as the direction from the GROUND toward the SUN (see contracts.js). const SUN_ELEV = (55 * Math.PI) / 180; @@ -335,6 +340,69 @@ export function createWorld(scene, opts = {}) { root.add(fence); solids.push(fence); + // --- shed & spare table ------------------------------------------------ + // Where the spare hardware lives, which makes it where the §7 scenario + // starts: rig → carry a spare → repair mid-storm. Lane D's pickup radius is + // 1.5 m off this point and everything downstream of it is already wired, so + // this small thing gates the whole hand-played loop. + // + // The position is published SYNCHRONOUSLY, from constants, even though the + // meshes arrive later in dress(). createWorld() has to stay sync — a.test.js + // and the selftest build a yard without a server — and Lane D's + // wireYardActions reads world.shedTable at wiring time. dress() refines the + // point to Lane E's baked `pickup_anchor` if it's there. + const shedTable = { + pos: new THREE.Vector3(SHED_TABLE.x, heightAt(SHED_TABLE.x, SHED_TABLE.z) + 0.9, SHED_TABLE.z), + }; + + /** + * Swap Lane E's GLBs in over the graybox. Async and separate from + * createWorld() on purpose: the selftest builds a yard with no server, and a + * fetch in the constructor would either break it or make it slow and flaky. + * Every load is individually guarded — a missing GLB leaves its graybox + * standing rather than taking the boot down with it. + */ + async function dress() { + const { GLTFLoader } = await import('../vendor/addons/loaders/GLTFLoader.js'); + const loader = new GLTFLoader(); + + const load = async (name) => { + try { + const gltf = await loader.loadAsync(new URL(`../models/${name}.glb`, import.meta.url).href); + gltf.scene.traverse((o) => { + if (o.isMesh) { o.castShadow = true; o.receiveShadow = true; } + }); + return gltf.scene; + } catch (err) { + console.warn(`[world] ${name} unavailable, keeping graybox:`, err.message); + return null; + } + }; + + const [shed, table] = await Promise.all([load('shed_01_v1'), load('shed_table_v1')]); + + if (shed) { + shed.name = 'shed_01'; + shed.position.set(SHED.x, heightAt(SHED.x, SHED.z), SHED.z); + shed.rotation.y = SHED.rotY; + root.add(shed); + solids.push(shed); + } + if (table) { + table.name = 'shed_table'; + table.position.set(SHED_TABLE.x, heightAt(SHED_TABLE.x, SHED_TABLE.z), SHED_TABLE.z); + table.rotation.y = SHED_TABLE.rotY; + root.add(table); + // NOT in solids: you want to walk up to the table, not be fenced off it. + + // Prefer Lane E's baked anchor over my guess at where a table top is. + table.updateWorldMatrix(true, true); + const anchor = table.getObjectByName('pickup_anchor'); + if (anchor) shedTable.pos.setFromMatrixPosition(anchor.matrixWorld); + } + return { shed, table }; + } + // --- the world object -------------------------------------------------- return { anchors, @@ -343,6 +411,13 @@ export function createWorld(scene, opts = {}) { sunDir: SUN_DIR.clone(), solids, root, + /** + * Where a spare gets picked up. `{pos}` — Lane D registers a 1.5 m hold-E + * off this point. Present from construction; dress() may nudge it onto + * Lane E's `pickup_anchor`. + */ + shedTable, + dress, // Lane C's skyfx MODULATES these as the storm builds and hands them back // untouched on dispose() — it doesn't own them. That's why the yard exposes // its lights rather than keeping them private.