The pre-merge thresholds were tuned against a mock that injected +18 gusts. In
the actual storm_02, gusts-over-baseline peak at 12.1 m/s — so stumbleGust:17
was above every gust in the game and StumbleBack was unreachable dead code, and
shoveGustMin:8 (a low bar in the prototype's 12-38 units) had become a high bar
in ours and threw away most of the shove.
Retuned against the real storm profile (12 gust events, peaks 12.1..6.1, 4
knockdowns, maxExposure 1.55): stumbleGust 17→9, shoveGustMin 8→4. A wild night
now reads shoved (26s of 90) → stumbling (4) → floored (2); a calm day stays 0/0.
d.test.js now loads the real storm JSON via weather.loadStorm and asserts every
threshold is REACHABLE and correctly ordered — the guard the first tuning lacked.
This matters because decision 8 (B+C) reweights the downdraft this sprint, which
is exactly the kind of change that silently killed StumbleBack the first time.
Also pinned the emergent 'unbrace into a gust → stumble' behaviour.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
At gate 1 the ped walked straight through the house. Collision now stops it
0.30 m off the wall face (expected -9.70, measured -9.70), off trunks, posts and
the fence, and slides along a wall hit at an angle. Injected as opts.collide the
way groundAt already was, so player.sim.js stays zero-import and node-runnable.
Two things the real yard taught, neither guessable from the plan:
- `fence` is a GROUP of 37 child meshes whose combined box is the entire 30x20 m
yard, so one box per solids entry is useless. Flattened to 43 leaf boxes.
- the house ROOF spans y 2.99-3.21 and reaches 0.4 m FURTHER into the yard than
the wall under it (eaves overhang). A flat footprint test would stop a 1.7 m
person dead at an invisible eave, so every box is filtered by vertical overlap
with the body and the roof drops out on its own.
Boxes are built once (solids are static) and distance-pruned — no per-frame
raycast, which is the thing Lane A measured as catastrophic on the terrain.
The M3 pack is wired: carrying swaps locomotion to Carry/CarryIdle; an
interaction names its own verb through a new `clip` field on the interact spec
(Crank at a turnbuckle, PickUp at the shed table); StumbleBack fires on a gust
that breaks your stride but can't floor you — below knockWind on purpose, so a
storm reads as shoved → stumbling → floored rather than fine-fine-fine-flat.
TakeCover (hold C) became a real mechanic rather than a pose: brace and knockWind
x2.0, shove x0.25. A 38 m/s gale floors you standing and doesn't while braced;
let go in the same gale and you're down in half a second. It raises the bar, it
does not remove it — a big enough gust still wins, braced or not. So the storm's
answer to "the gusts are too strong to cross the yard" is now wait one out and
move in the lull, which is the repair-window language DESIGN.md already uses.
wireYardActions now reads sailRig.corners[i] live by index instead of capturing
the corner object — per Lane A's warning that attach() replaces the array, a
captured corner is one the sim no longer steps and would gate forever on a
`broken` flag that can never change again.
35 Lane D asserts, 0 fail (was 20). Carry/shelter/knockdown verified against the
real 17-clip pack in the assembled game, not only in the harness.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
player.sim.js Deterministic core, zero imports: camera-relative movement, the
state-machine table, wind slow/gust-shove/knockdown. step(dt,t)
is the whole clock — no Date.now, no Math.random, no rAF — so a
storm fast-forwards identically in selftest and in the game.
player.js The view: rig load per the 90sDJsim DEVMANUAL rig rules
(SkeletonUtils.clone, height-normalise off the MEASURED head
bone, canonicalised bone namespace), clip retarget, and
createPlayer() satisfying the Player contract.
interact.js Hold-E with radial progress + wireYardActions (re-rig 2.5 s,
turnbuckle trim 1.2 s, carry-one-item), duck-typed so it no-ops
cleanly until Lane B lands sailRig.repair/trim.
d.test.js 20 asserts in Lane A's harness: 38 pass / 3 skip overall.
Ported from the 2D prototype's shape (game.js:250-252), retuned to m/s: the
slow curve, and shove gated to gusts only and scaling with speed² so gusts have
teeth. Knockdown needs 0.5 s of SUSTAINED overload — deliberately the same rule
as a sail corner letting go, so cloth and people speak one language.
Two decisions worth the review:
- The knockdown pitches the root in code rather than playing the Falling clip's
root. Shared clips must drop Hips.quaternion (a different-orientation source
lays the target flat), so a clip physically cannot lie the body down. Doing it
in code also lets the fall go DOWNWIND of the gust that caused it, which a
canned clip could never do, and keeps it deterministic.
- Gust magnitude is recovered from a slow EMA of local wind rather than widening
Lane C's contract: wind.sample() gives the total and gustTelegraph() only fires
BEFORE a gust, so nothing reports gust size during the hold. The EMA
self-calibrates to whatever storm JSON Lane C authors.
Verified in a real scene, not only in asserts: head bone 1.715 m at fig scale
0.983, all six clips bound, walk/run at the tuned speeds, the body lies down and
gets back up, hold-E fires exactly once per press.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>