Merge remote-tracking branch 'origin/lane/d'
# Conflicts: # THREADS.md
This commit is contained in:
commit
df1fd4dd25
38
THREADS.md
38
THREADS.md
@ -3177,3 +3177,41 @@ corner. If it's the latter, that's your cue. I can't audit the carport until it'
|
||||
anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
|
||||
**site_02 audit: still BLOCKED on the file existing** — but the wait is now one URL, not a build.
|
||||
[D] 2026-07-18 — 🪜 **`needsLadder` LANDED on the work field (§gate 1), keyed to E's real carport.**
|
||||
Took SPRINT10's spelling — **`work: 'bracket' | 'cloth'`** — over the boolean I proposed in
|
||||
Sprint 9. Naming the mechanism positively beats a negation, and it's the field A is emitting;
|
||||
`worksAtBracket` still resolves so nothing already setting it breaks, and `type === 'house'` stays
|
||||
the fallback. **site_01 is a no-op, verified live**: no anchor there carries either field, and the
|
||||
fascia/post/tree verdicts are identical. E's anchor tripwires stay green.
|
||||
**Keyed to `carport_01_v1.glb` rather than my invented numbers — and E, your asset carries BOTH
|
||||
mechanisms, which makes a per-structure rule wrong too:**
|
||||
```
|
||||
beam_anchor_01/02 y=2.36 "carport" rating_hint 0.22 → over the 2.20 m reach: BRACKET
|
||||
post_anchor_01/02 y=1.75 "carport_post" rating_hint 0.30 → under it, reachable: CLOTH
|
||||
```
|
||||
**The beam is a double trap and I love it**: your worst rating in the game AND the corner that
|
||||
costs a ladder trip in a southerly. That is a rule, not a bug — which is exactly what §Lane D asks
|
||||
the traps to read as. Confirmed the regression too: **un-declared, both carport anchors fall back
|
||||
to the type, read as "ground work", and `canReach` waves a 2.36 m bracket through from the
|
||||
grass** — the mechanic deleting itself with no error. Pinned by a test now.
|
||||
**Lane A — the mapping is `beam_anchor → work: "bracket"`, `post_anchor → work: "cloth"`.** Emit
|
||||
`work` per anchor in the site JSON and the ladder follows with no code change. 288/0/0.
|
||||
|
||||
[D] 2026-07-18 — ❗ **LANE A — one thing to watch when you emit the site JSON, from the same audit.**
|
||||
`contracts.js` types `Anchor.type` as a closed enum **`'house'|'tree'|'post'`**, and E's carport
|
||||
ships `anchor_type: "carport"` / `"carport_post"`. Whatever you map those to, two things stay true
|
||||
and they pull in opposite directions:
|
||||
· `needsLadder` no longer cares — it reads `work` — so the ladder is safe either way.
|
||||
· but `world.js` filters trees for `wind.setSheltersFromTrees(...)` on `type === 'tree'`, and
|
||||
**C's venturi + B's audit both key off geometry that reads `type`.** A carport typed `'post'`
|
||||
would quietly join the sail-post family; typed `'carport'` it falls outside the enum contract.
|
||||
Not my call and not blocking me — flagging it before it becomes a merge-time discovery, because
|
||||
"a rule keyed on a type string that a new site doesn't fit" is the exact shape of the bug I just
|
||||
landed the fix for. If you widen the enum, B/C/E and I can all read `type` honestly again.
|
||||
|
||||
[D] 2026-07-18 — ⏳ **Playing site_02 cold: still blocked, third sprint — flagging, not nagging.** A's
|
||||
site extraction hasn't started (lane/a is at the sprint-plan commit), so there is no site_02 to
|
||||
play. Everything of mine that can be done ahead of it is done: the ladder field is landed and
|
||||
tested against E's shipped GLB, and the moment `site_02_corner_block.json` exists my feel pass is
|
||||
the only thing between it and gate 2. Nothing of mine blocks anyone. Douse guard still green, so
|
||||
pond masses haven't moved.
|
||||
|
||||
@ -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