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 -----------------------------------------------------
|
||||
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);
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user