Place the shed and spare table; expose world.shedTable (unblocks Lane D)
SPRINT3 decision 9 / §Lane A.1. Lane D's §7 loop — rig, carry a spare, repair mid-storm — had nowhere to pick a spare up, so wireYardActions self-skipped the pickup and nothing in the game could put one in your hands. world.shedTable.pos is published synchronously from constants even though the meshes arrive later: createWorld() has to stay sync (a.test.js and the selftest build a yard with no server), and wireYardActions reads the position at wiring time. dress() then refines it onto Lane E's baked pickup_anchor — which moved it 5 cm off my guess at where a table top is, so it was worth reading. dress() is async and separate from createWorld for the same reason: a fetch in the constructor would make the selftest slow and flaky, or break it. Each load is guarded individually, so a missing GLB leaves its graybox standing instead of taking the boot down. Verified by hand rather than by registration check: walk up, hold E, spare in hand; a second hold says "hands full"; leaning on the table with E held for 6 s deals exactly one spare (D's latch works); nothing fires from across the yard. Selftest 169/0/0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
624a72e458
commit
a5ed4fcdb6
@ -189,6 +189,10 @@ export async function boot(opts = {}) {
|
|||||||
|
|
||||||
// --- world & camera -----------------------------------------------------
|
// --- world & camera -----------------------------------------------------
|
||||||
const world = createWorld(scene, { wind });
|
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);
|
const cameraRig = createCameraRig(canvas);
|
||||||
cameraRig.setSolids(world.solids);
|
cameraRig.setSolids(world.solids);
|
||||||
cameraRig.setGround(world.heightAt);
|
cameraRig.setGround(world.heightAt);
|
||||||
|
|||||||
@ -35,6 +35,11 @@ export function heightAt(x, z) {
|
|||||||
|
|
||||||
const GARDEN_BED = { x: 1, z: 2, w: 6, d: 4 };
|
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°.
|
// 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).
|
// Stored as the direction from the GROUND toward the SUN (see contracts.js).
|
||||||
const SUN_ELEV = (55 * Math.PI) / 180;
|
const SUN_ELEV = (55 * Math.PI) / 180;
|
||||||
@ -335,6 +340,69 @@ export function createWorld(scene, opts = {}) {
|
|||||||
root.add(fence);
|
root.add(fence);
|
||||||
solids.push(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 --------------------------------------------------
|
// --- the world object --------------------------------------------------
|
||||||
return {
|
return {
|
||||||
anchors,
|
anchors,
|
||||||
@ -343,6 +411,13 @@ export function createWorld(scene, opts = {}) {
|
|||||||
sunDir: SUN_DIR.clone(),
|
sunDir: SUN_DIR.clone(),
|
||||||
solids,
|
solids,
|
||||||
root,
|
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
|
// 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
|
// untouched on dispose() — it doesn't own them. That's why the yard exposes
|
||||||
// its lights rather than keeping them private.
|
// its lights rather than keeping them private.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user