SPRINT14 gate 3.1. The editor is about to offer these GLBs to an author by
name, which turns every silent field in them into a trap for whoever places
one. Audited the placeable set, fixed the gaps in the factory (nothing
hand-edited), and added the two props D needs to author a yard worth playing.
THE AUDIT, and what it found:
- Four `*_anchor` nodes carried no rating_hint at all — door_anchor,
pickup_anchor, grip_anchor, window_light_anchor. adoptAnchor does
`rating_hint ?? 1`, so silence there does not mean "unrated", it means
RATED PERFECT: a shed door skin outranking a concreted post, conjured out
of a missing field. They now deny it explicitly (`tie_off: false`).
- The carport's price resolved only by coincidence: collateralFor() matches a
STRUCTURE whose site id equals the anchors' collateral string, and site_02
happens to name its structure "carport". The editor will generate
"carport_2" for the second one, and the trap would have gone free — the
gutter bug reborn in the sprint meant to bury it. The GLB now carries
`collateral_key`, like the house does for "gutter". Lane A: the runtime
half is yours (collateralFor could fall back to it).
- The trampoline is unpriced, and now says so rather than omitting it.
Three new rules make the audit permanent, and each is proven able to fail:
no silent anchors, no collateral string nobody prices, no baked anchor_type
outside the checked ANCHOR_TYPE enum. Rule 2 found the carport gap on its
first run — an unfaked negative control.
THE PROPS:
- swing_set_01 (+ wrecked) — the palette's honest middle. It had a ceiling
(gum fork 1.0) and a trap (carport beam 0.22) and nothing in between. Two
apex anchors at 0.45, typed `swing_frame` (NOT `post`: the type string is
what the player reads before committing, and "post" promises concrete),
and a crossbar that is the most anchor-looking object in the game and is
not one. Priced at $140 — proposal, reasoning baked beside it, A rules the
number: gnome 25 < gutter 90 < swing 140 < carport 180.
- tree_jacaranda_01 — a second species whose LADDER is the feature.
Gum: 1.00/0.88/0.76, climb freely, pay 24%.
Jacaranda: 0.95/0.52/0.40, climb at all, pay 58%.
That makes tree choice a real decision instead of "is there a tree".
Unpriced by ruling, same as the bike: no limb-failure event exists to see.
Both wreck/orientation claims are measured in browser coords, not reasoned —
`wreck_falls_toward: "+Z"` is baked AND asserted against the mesh.
Selftest 376/0/0. Two consecutive full factory runs: 44 files byte-identical.
The fascia anchors have said collateral:"gutter" since Sprint 6 and nobody
priced one — collateralFor('gutter') returns null, backyard_01's house is a
free failure. Proposal rides in the asset like the carport's did: 90, between
the gnome (25) and the carport (180), with the band reasoning next to the
constant. A rules the number.
house_yardside_wrecked_v1: same origin/footprint/facade (shared _house_facade
helper — intact house BIN chunk byte-identical to the committed GLB, only the
three new price extras differ), fascia torn through the anchored span, left
run hanging by its last bracket, right run kinked ON the grass, downpipe out
of plumb, offcuts flat on the lawn. No fascia_anchor_* empties survive — you
cannot re-tie to a ripped eave.
collateral_key is new and explicit: unlike the carport, the structure and the
thing you take have different names, so the key names which collateral string
the value prices.
All 33 GLBs byte-identical across two full factory runs; the house pair also
byte-identical between --only and full runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hide_render does not survive glTF export — verified in three r175, every node
arrives visible:true. So garden_bed has been drawing plants_full, tattered AND
dead superimposed since Sprint 1, and my THREADS note told Lane A the opposite.
The asserts missed it by checking the nodes existed rather than that only one
was on. The flag now rides in extras, which does export, and consumers apply it
in one line; e.test.js pins the flag instead of the node.
Night dressing: house_yardside gains window_glow, an unlit emissive pane marked
hidden_by_default, plus window_light_anchor. A warm window means someone is
inside, which is why the player is out in the dark keeping the sail on. Plus a
moon sprite whose halo, not its disc, is the part that survives cloud.
End cards are rendered from the game's own props as a diptych: identical camera
and yard, only the night's outcome differs. The left third is left empty for
Lane A's text.
Cards are opt-in behind --cards because EEVEE's shadow pass is not
byte-reproducible across processes in 5.1 — proven with a minimal repro, not
PYTHONHASHSEED, and present even with a hard sun. The dawn shadow is the art, so
the cards become committed art rather than build output. Everything the game
loads stays byte-identical: 38/38 verified.
Selftest 241/0/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifying the GLBs in three.js (not just Blender) showed four assets reporting
inflated bounds: tramp_01 came back 3.29 x 1.27 m against a true 2.96 x 0.78.
Box3.setFromObject expands each mesh's LOCAL box by the world matrix, so a node
carrying a rotation over-reports — the same trap Blender's obj.bound_box sets,
and what three uses for frustum culling. Joined nodes inherited parts[0]'s
rotation, which for an arc is half a segment step off-axis.
Applying rotation at join time makes every local box axis-aligned, so the
default Box3 path is now correct for consumers and culling is tight. World
geometry is unchanged; the exported dims are identical.
Adds tools/assetcheck/, the three.js harness that caught this. It's the only
check that can: a Blender round-trip exports Z-up->Y-up and imports Y-up->Z-up,
so a broken export_yup flips back and passes green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 generated assets plus the grass billboard atlas and the four debris models
copied verbatim from the 3D-STORE library (copies rule, §0). All meter-scale,
Y-up, and far under the 15k tri budget — garden_bed is heaviest at 2,580.
Regeneration is byte-deterministic: two consecutive runs produce identical
files, so re-running the factory causes no churn.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>