Land needsLadder on the work field, measured against E's real carport
SPRINT10 gate 1. Adopts SPRINT10's spelling — `work: 'bracket' | 'cloth'` — as
canonical over the boolean I proposed in Sprint 9: naming the mechanism
positively beats a negation, and it's the field A is emitting. Sprint 9's
`worksAtBracket` is still honoured so nothing that already sets it breaks, and
`type === 'house'` remains the fallback, so site_01 is a no-op (verified live:
no anchor there carries either field, and fascia/post/tree verdicts are
unchanged).
Keyed to E's SHIPPED carport_01_v1.glb rather than my invented numbers, and it
confirms the audit exactly — it also carries BOTH mechanisms on one structure,
so a per-structure rule would still have been wrong:
beam_anchor_01/02 y=2.36 anchor_type "carport" rating_hint 0.22
post_anchor_01/02 y=1.75 anchor_type "carport_post" rating_hint 0.30
The beam sits over a 1.72 m person's 2.20 m reach and needs the ladder; the post
doesn't. Neither is typed 'house', so un-declared they BOTH fall back to "ground
work" and canReach waves a 2.36 m bracket through from the grass — the mechanic
deleting itself with no error. That regression is now pinned by a test.
E: the beam is the double trap — your worst rating in the game AND the one that
costs a ladder trip. That reads as a rule, not a bug, which is what §Lane D asks
for. A: the mapping you want is beam→bracket, post→cloth.
63 Lane D asserts headless, 288/0/0 in the browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
63beec87a7
commit
13fc4bd14d
@ -36,23 +36,36 @@ export const LADDER_URL = './models/ladder_01_v1.glb';
|
||||
* at the bracket on the wall, while a blown post or tree corner is re-made on the cloth, which has
|
||||
* fallen to somewhere you can stand. Height is a coincidence of that; the bracket is the reason.
|
||||
*
|
||||
* SPRINT9, site_02 (SPRINT9 §Lane D — "the ladder assumes fascia height; the carport may not have
|
||||
* one"): `type === 'house'` is site_01's spelling of that idea, not the idea itself. The corner
|
||||
* block has a CARPORT — a roofline you'd also work at the bracket — and contracts.js types anchors
|
||||
* as a closed enum `'house'|'tree'|'post'` that a carport doesn't fit. Whatever E types it, this
|
||||
* must not fail OPEN: if `needsLadder` says false, `canReach` in interact.js returns true
|
||||
* unconditionally and the player re-rigs a 2.6 m bracket standing on the grass — the mechanic just
|
||||
* quietly stops existing, with no error. That is the same disease as StumbleBack's dead threshold
|
||||
* and the camera-less shadow grid: a rule keyed on the wrong thing, failing silently open.
|
||||
* SPRINT10 §Gate 1 — keyed on the MECHANISM, from site data. `type === 'house'` was site_01's
|
||||
* spelling of the idea, not the idea itself, and contracts.js types anchors as a closed enum
|
||||
* `'house'|'tree'|'post'` that site_02's carport does not fit. That mattered because it fails OPEN:
|
||||
* when `needsLadder` says false, `canReach` in interact.js returns true UNCONDITIONALLY, so the
|
||||
* player re-rigs a bracket well over their head while standing on the grass and the whole mechanic
|
||||
* quietly stops existing — no error, no red. Same disease as StumbleBack's dead threshold and the
|
||||
* camera-less shadow grid: a rule keyed on the wrong thing, failing silently open.
|
||||
*
|
||||
* So it reads DATA first, exactly like `anchor.ratingHint` already does (world.js pulls that from
|
||||
* the GLB's `userData.rating_hint`), and falls back to site_01's shape. Lane A/E: set
|
||||
* `anchor.worksAtBracket = true` on the carport's anchors in the site JSON and the ladder follows
|
||||
* the site with no code change. Nothing on site_01 carries the flag, so this is a no-op there.
|
||||
* Measured against E's shipped `carport_01_v1.glb`, which is exactly the case the audit predicted —
|
||||
* and note it carries BOTH mechanisms, so a per-STRUCTURE rule would still be wrong:
|
||||
* beam_anchor_01/02 y=2.36 anchor_type "carport" → over the 2.20 m reach: BRACKET
|
||||
* post_anchor_01/02 y=1.75 anchor_type "carport_post" → under it, reachable: CLOTH
|
||||
* Neither is typed 'house'. The beam is the double trap — E's worst rating in the game (0.22) AND
|
||||
* the one you need the ladder for.
|
||||
*
|
||||
* Reads DATA first, exactly like `anchor.ratingHint` already does from the GLB's `userData`:
|
||||
* · `work: 'bracket' | 'cloth'` — SPRINT10's spelling, and the canonical one. Positive about the
|
||||
* mechanism rather than a negation, which is why it beats the boolean I proposed in Sprint 9.
|
||||
* · `worksAtBracket: boolean` — my Sprint-9 spelling, still honoured so nothing that already
|
||||
* sets it breaks.
|
||||
* · else `type === 'house'` — site_01's shape. Nothing there carries either field, so this is
|
||||
* a no-op on backyard_01 and E's anchor tripwires stay green.
|
||||
* Lane A: emit `work` per anchor in the site JSON and the ladder follows the site with no code
|
||||
* change. The carport mapping you want is beam→bracket, post→cloth, per the heights above.
|
||||
*/
|
||||
export const needsLadder = (anchor) => {
|
||||
if (!anchor) return false;
|
||||
if (typeof anchor.worksAtBracket === 'boolean') return anchor.worksAtBracket; // site data wins
|
||||
if (anchor.work === 'bracket') return true; // SPRINT10 spelling
|
||||
if (anchor.work === 'cloth') return false;
|
||||
if (typeof anchor.worksAtBracket === 'boolean') return anchor.worksAtBracket; // Sprint-9 spelling
|
||||
return anchor.type === 'house'; // site_01's shape
|
||||
};
|
||||
|
||||
|
||||
@ -448,22 +448,46 @@ export default async function run(t) {
|
||||
'nor a tree limb — that is a strop you throw');
|
||||
});
|
||||
|
||||
t.test('ladder: a site can DECLARE which anchors are worked at the bracket', () => {
|
||||
// SPRINT9 §Lane D, ahead of site_02. The corner block has a CARPORT, and contracts.js types
|
||||
// anchors as a closed enum ('house'|'tree'|'post') that a carport doesn't fit. The failure mode
|
||||
// matters more than the feature: if needsLadder says false, interact's canReach returns true
|
||||
// UNCONDITIONALLY, so the player re-rigs a 2.6 m bracket from the grass and the mechanic
|
||||
// silently stops existing. Data first, site_01's type as the fallback.
|
||||
assert(realNeedsLadder({ type: 'carport', pos: { y: 2.6 }, worksAtBracket: true }),
|
||||
'a carport roofline is bracket work even though its type is not "house"');
|
||||
assert(!realNeedsLadder({ type: 'house', pos: { y: 2.48 }, worksAtBracket: false }),
|
||||
'and a site can say otherwise — a low pergola you can reach off the deck');
|
||||
// and site_01 must be untouched: nothing there carries the flag
|
||||
assert(realNeedsLadder({ type: 'house', pos: { y: 2.48 } }), 'no flag → site_01 behaviour');
|
||||
assert(!realNeedsLadder({ type: 'post', pos: { y: 3.95 } }), 'no flag → post is still ground work');
|
||||
t.test('ladder: keys on the MECHANISM a site declares, not on the anchor type', () => {
|
||||
// SPRINT10 gate 1. The failure mode is the point: when needsLadder says false, interact's
|
||||
// canReach returns TRUE UNCONDITIONALLY — so a mis-keyed anchor doesn't error, it silently
|
||||
// deletes the ladder mechanic. Fail-open is why this is data and not a type string.
|
||||
assert(realNeedsLadder({ type: 'carport', pos: { y: 2.36 }, work: 'bracket' }),
|
||||
'declared bracket work needs the ladder, whatever the type says');
|
||||
assert(!realNeedsLadder({ type: 'carport_post', pos: { y: 1.75 }, work: 'cloth' }),
|
||||
'declared cloth work does not — you re-make it on the fallen sail');
|
||||
assert(!realNeedsLadder({ type: 'house', pos: { y: 2.48 }, work: 'cloth' }),
|
||||
'and a site outranks the type in both directions (a low pergola off a deck)');
|
||||
|
||||
// Sprint-9's spelling still honoured, so anything already setting it keeps working
|
||||
assert(realNeedsLadder({ type: 'carport', pos: { y: 2.36 }, worksAtBracket: true }));
|
||||
assert(!realNeedsLadder({ type: 'house', pos: { y: 2.48 }, worksAtBracket: false }));
|
||||
|
||||
// site_01 must be untouched — nothing there carries either field
|
||||
assert(realNeedsLadder({ type: 'house', pos: { y: 2.48 } }), 'no field → site_01 behaviour');
|
||||
assert(!realNeedsLadder({ type: 'post', pos: { y: 3.95 } }), 'no field → post is still ground work');
|
||||
assert(!realNeedsLadder(null) && !realNeedsLadder(undefined), 'and it never throws on a gap');
|
||||
});
|
||||
|
||||
t.test('ladder: E\'s REAL carport resolves both ways round (site_02\'s trap)', () => {
|
||||
// Read off carport_01_v1.glb rather than invented: E ships BOTH mechanisms on one structure,
|
||||
// which is why a per-structure rule would still be wrong.
|
||||
// beam_anchor_01/02 y=2.36 anchor_type "carport" rating_hint 0.22
|
||||
// post_anchor_01/02 y=1.75 anchor_type "carport_post" rating_hint 0.30
|
||||
// The beam is the double trap: E's worst rating in the game AND over the 2.20 m reach.
|
||||
const beam = { id: 'c1', type: 'carport', pos: { y: 2.36 }, work: 'bracket', ratingHint: 0.22 };
|
||||
const post = { id: 'c3', type: 'carport_post', pos: { y: 1.75 }, work: 'cloth', ratingHint: 0.30 };
|
||||
assert(realNeedsLadder(beam), 'the beam bracket at 2.36 m is over a 1.72 m person\'s 2.20 m reach');
|
||||
assert(!realNeedsLadder(post), 'the post fitting at 1.75 m is not — you can just reach it');
|
||||
assert(beam.ratingHint < post.ratingHint, 'and the one needing the ladder is also the weaker lie');
|
||||
|
||||
// The regression that matters: WITHOUT the work field, E's types are not 'house', so the old
|
||||
// rule said "no ladder" for a 2.36 m bracket and canReach waved it through from the grass.
|
||||
assert(!realNeedsLadder({ type: 'carport', pos: { y: 2.36 } }),
|
||||
'un-declared, it falls back to the type and reads as ground work — which is exactly why '
|
||||
+ 'Lane A must emit `work` per anchor in the site JSON, and why this test exists');
|
||||
});
|
||||
|
||||
t.test('ladder: fascia re-rig is gated on being up it; post re-rig is not', () => {
|
||||
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
|
||||
const L = fakeLadder({ player: p });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user