Merge remote-tracking branch 'origin/lane/d'
# Conflicts: # THREADS.md
This commit is contained in:
commit
997ea0bab1
47
THREADS.md
47
THREADS.md
@ -1118,3 +1118,50 @@ Format: `[lane letter] YYYY-MM-DD — note`
|
||||
the player commits blind to the one number the whole game is about. If you want the cheap version of
|
||||
the same lesson instead, `riggingUI.summary.area` is already live and picking the obvious quad reads
|
||||
"191 m2" before you commit to it.
|
||||
[D] 2026-07-17 — ✅ **GATE 3 §7 LOOP CLOSED BY HAND, ON RECORD — 4/4 survival, real storm-induced break.**
|
||||
Drove the merged game through `SHADES.step` (no rAF), real key input, real hold-E. Full trace:
|
||||
prep: rig 3×rated + 1×carabiner@p1, tn 1.0, settle → `oooo`
|
||||
t=3.9 walk to shed table (9,6) → hold E → **carrying=spare** (PickUp→CarryIdle)
|
||||
t≈3.4 storm: carry the spare across the yard (clip **Carry**) toward p1
|
||||
**p1's carabiner blows on its own** under storm load → `oooX`, sail flogs loose over the garden
|
||||
prompt **`rerig_3` tracks the FLOGGING corner** as it swings (live cornerPos — the feature working)
|
||||
hold E 2.5 s at the loose corner (clip **Crank**, state busy) → `oooo`, **spare consumed**,
|
||||
**p1 re-rigged carabiner→shackle (an UPGRADE**, exactly B's documented behaviour)
|
||||
ride out to t=90 → **ends 4/4, coverage 1.0, no further breaks** → aftermath, garden intact.
|
||||
4 screenshots (shed pickup · flogging sail + player w/ spare · re-tensioned sail · calm aftermath)
|
||||
in this session's transcript. **This is the §7 thesis working: mixed rig with a weak link → the
|
||||
weak link fails → one repair upgrades it → the now-uniform rig survives.** Definition of done met,
|
||||
and the break was the storm's, not induced.
|
||||
|
||||
[D] 2026-07-17 — 🔧 **RETUNED player thresholds against the REAL storm_02 (my pre-merge tune was against
|
||||
a mock).** Committed ahead of the run. In the actual storm, gusts-over-baseline peak at **12.1 m/s**,
|
||||
so my old `stumbleGust:17` was above every gust in the game — **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. Fixed: stumbleGust 17→9, shoveGustMin 8→4. A wild night now reads shoved (26 s of 90) →
|
||||
stumbling (4) → floored (2); a calm day stays 0/0. **d.test.js now loads the real storm JSON
|
||||
(`weather.loadStorm`) and asserts every threshold is REACHABLE and correctly ordered** — decision 8
|
||||
reweights the downdraft this sprint, and this guard fails loudly if that silently kills a mechanic
|
||||
again. 40 Lane D asserts, 0 fail.
|
||||
|
||||
[D] 2026-07-17 — 📊 **FOR B+C (decision 8) — measured the sail's failure envelope, and it's a CLIFF not a
|
||||
slope.** Driving `rig.step` against the real wild wind, 3×rated+1×carabiner, settled, per-corner peak
|
||||
loads:
|
||||
tn ≤ 1.03 → **4/4 survives**, peaks ~875–1240 N (carabiner rides just under its 1200 N rating)
|
||||
tn 1.04 → **instant double cascade**: h1 and p2 spike to **~10,300 N at t=1.1** and blow → 1/4
|
||||
A 0.01 tension step takes peak load from ~1.2 kN to ~10 kN — that's the cloth going **unstable**, not
|
||||
gradual overload. Consequence for §7: in a HIGH-tension rig the cascade is faster than any hold-E
|
||||
(neighbours gone <1 s after the first break), so a naturally-broken corner there is **not**
|
||||
hand-repairable. BUT a survivable mixed rig (my run) IS — the weak link blows while the rated corners
|
||||
keep margin and don't cascade, so the repair lands. **Net: good rigs survive, flat/drum-tight rigs
|
||||
cascade — the thesis holds.** The decision-8 downdraft-semantic (fraction of total vs gust) is really
|
||||
about loading a FLAT roof *steadily*; it won't fix the cliff, which is a cloth-stability ceiling near
|
||||
tn 1.04. Worth a stability clamp on peak per-face force if you want a gentler failure curve.
|
||||
|
||||
[D] 2026-07-17 — 🪜 **LADDER STRETCH GOAL — DEFERRED, flagging early as asked.** It's bigger than what's
|
||||
left of this sprint: carry-ladder is a *second* carry type (can't also carry a spare — hands-full),
|
||||
placement needs a valid-surface test + a snap to fascia anchors, and ClimbLadder needs vertical
|
||||
root motion the rig's rotation-only clips don't provide (same class of problem as the knockdown —
|
||||
I'd drive the climb height in code and play the clip on top). That's a whole interaction sub-system,
|
||||
not a polish item, and the §7 loop (the actual gate) is closed without it. Recommend it as a
|
||||
Sprint-4 item once A's anchor rework (decision 2) lands the fascia anchors it targets. The clip is
|
||||
baked and waiting (`ClimbLadder` is in the pack), so it's not blocked on assets.
|
||||
|
||||
@ -62,7 +62,11 @@ export const TUNE = {
|
||||
// prototype: push = ws*ws*0.55 — "wind pressure goes with speed², gusts have teeth"; and shove
|
||||
// only applied while wind.gust > 8, never from the base wind.
|
||||
shoveK: 0.0035, // shove accel (m/s²) = shoveK · ws² → ~3.2 m/s² in a 30 m/s gust
|
||||
shoveGustMin: 8, // m/s of gust (over baseline) before the wind can push you at all
|
||||
// Gate RETUNED against the real storm_02 (see the header note): the prototype's literal 8 was a
|
||||
// low bar in ITS units (gusts ran 12–38, so 8 was "almost always"); ported straight across it
|
||||
// became a high bar in ours (gusts peak at 12.1) and threw away most of the shove. 4 restores the
|
||||
// prototype's intent — you're being pushed for ~26 s of a 90 s storm, and a calm day is still calm.
|
||||
shoveGustMin: 4, // m/s of gust (over baseline) before the wind can push you at all
|
||||
shoveDamp: 2.5, // 1/s foot-friction bleed → terminal drift ≈ shoveK·ws²/shoveDamp
|
||||
|
||||
// Baseline tracker: contracts.js exposes wind.sample() (total) and wind.gustTelegraph() (before
|
||||
@ -80,7 +84,12 @@ export const TUNE = {
|
||||
|
||||
// A gust that can't floor you can still break your stride. Sits BELOW knockWind on purpose, so a
|
||||
// storm reads as: shoved → stumbling → floored, rather than fine-fine-fine-flat-on-your-back.
|
||||
stumbleGust: 17, // m/s over baseline → you lose your footing (but not your feet)
|
||||
// RETUNED against the real storm_02: 17 was set against a mock that injected +18 gusts, but the
|
||||
// real storm's gusts-over-baseline peak at 12.1, so StumbleBack was unreachable — dead code in the
|
||||
// shipped game. 9 catches the top third of the 12 gusts in a wild night: 4 stumbles to 2
|
||||
// knockdowns, which is the intended rhythm (stumble is the beat, knockdown the punchline).
|
||||
// d.test.js pins this to the actual storm JSON so a reweighted downdraft can't quietly kill it again.
|
||||
stumbleGust: 9, // m/s over baseline → you lose your footing (but not your feet)
|
||||
stumbleCooldown: 3, // s — punctuation, not a stutter: one gust hold must not stumble you twice
|
||||
|
||||
// Shelter (hold C): brace and the wind stops owning you. This is the storm's real answer to "the
|
||||
|
||||
@ -15,6 +15,7 @@ import { PlayerSim, STATES, TUNE, clipFor } from '../player.sim.js';
|
||||
import { Interact, wireYardActions } from '../interact.js';
|
||||
import { assert, assertEq, assertClose, assertLess, fixedLoop } from '../testkit.js';
|
||||
import { FIXED_DT } from '../contracts.js';
|
||||
import { loadStorm, createWind } from '../weather.js';
|
||||
|
||||
const DT = FIXED_DT;
|
||||
|
||||
@ -25,8 +26,31 @@ const windX = (speed) => ({ x: speed, y: 0, z: 0 });
|
||||
const drive = (sim, secs, input = {}, wind = null, t0 = 0) =>
|
||||
fixedLoop(secs, DT, (dt, t) => sim.step(dt, t0 + t, input, wind));
|
||||
|
||||
/** Count what a real storm actually does to a person standing mid-yard. */
|
||||
function stormProfile(wind, tune, secs = 90) {
|
||||
const p = new PlayerSim({ start: { x: 0, y: 0, z: 4 }, tune });
|
||||
let stumbles = 0, knocks = 0, maxGust = 0, maxWind = 0, shovedFor = 0;
|
||||
fixedLoop(secs, DT, (dt, t) => {
|
||||
p.step(dt, t, {}, wind);
|
||||
maxGust = Math.max(maxGust, p.gust);
|
||||
maxWind = Math.max(maxWind, p.windSpeed);
|
||||
if (Math.hypot(p.shove.x, p.shove.z) > 0.15) shovedFor += dt;
|
||||
for (const e of p.events) if (e.type === 'state') {
|
||||
if (e.state === 'stumble') stumbles++;
|
||||
if (e.state === 'knocked') knocks++;
|
||||
}
|
||||
p.events.length = 0;
|
||||
});
|
||||
return { stumbles, knocks, maxGust, maxWind, shovedFor };
|
||||
}
|
||||
|
||||
/** @param {import('../testkit.js').Suite} t */
|
||||
export default function run(t) {
|
||||
export default async function run(t) {
|
||||
// The REAL storms, not a mock. Lane D's thresholds are meaningless except against the data they
|
||||
// fire on, and this sprint's decision 8 has B+C changing the downdraft semantics underneath us —
|
||||
// exactly the kind of edit that silently made StumbleBack unreachable the first time.
|
||||
const wild = createWind(await loadStorm('storm_02_wildnight'));
|
||||
const calm = createWind(await loadStorm('storm_01_gentle'));
|
||||
// ---------------------------------------------------------------- state machine table
|
||||
// The 17 clips actually in player_anims.glb (integrator baked the M3 pack; names logged in THREADS).
|
||||
// Verified against the real GLB in-browser: SHADES.player.view.clipNames matches this exactly.
|
||||
@ -205,12 +229,27 @@ export default function run(t) {
|
||||
assertEq(s.state, 'knocked', 'a big enough gust still takes you off your feet, braced or not');
|
||||
});
|
||||
|
||||
t.test('shelter: releasing the key always frees you, even mid-gust', () => {
|
||||
t.test('shelter: releasing the key always frees you — you are never stuck braced', () => {
|
||||
// steady wind (already learned as baseline, so no apparent gust): straight back to idle
|
||||
const s = new PlayerSim();
|
||||
drive(s, 30, {}, windX(20));
|
||||
drive(s, 1, { shelter: true }, windX(20));
|
||||
assertEq(s.state, 'shelter', 'braced');
|
||||
drive(s, 0.5, {}, windX(20)); // let go, wind still blowing
|
||||
assert(!s.busy && s.state === 'idle', 'released');
|
||||
drive(s, 0.5, {}, windX(20));
|
||||
assertEq(s.state, 'idle', 'let go in steady wind → idle');
|
||||
assert(!s.busy, 'and free to move');
|
||||
});
|
||||
|
||||
t.test('shelter: unbracing INTO the gust you were hiding from costs you your footing', () => {
|
||||
// Emergent, and worth pinning: bracing is a commitment. Let go early and the gust takes you.
|
||||
// This is what makes "wait for the lull" a decision rather than a formality.
|
||||
const g = new PlayerSim();
|
||||
drive(g, 30, {}, windX(4)); // learn a calm baseline
|
||||
drive(g, 1, { shelter: true }, windX(4 + TUNE.stumbleGust + 6), 30);
|
||||
assertEq(g.state, 'shelter', 'braced through the gust');
|
||||
drive(g, 0.2, {}, windX(4 + TUNE.stumbleGust + 6), 31);
|
||||
assert(g.state !== 'shelter', 'releasing always leaves shelter');
|
||||
assertEq(g.state, 'stumble', 'and straight into the gust → you stumble');
|
||||
});
|
||||
|
||||
t.test('shelter: cannot brace from your back', () => {
|
||||
@ -259,6 +298,51 @@ export default function run(t) {
|
||||
assertEq(s.state, 'shelter', 'braced, the gust does not stumble you');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- tuned against the REAL storms
|
||||
// These are the guard rails the pre-merge tuning didn't have. Every threshold below is only
|
||||
// meaningful relative to the storm JSON it fires on, so assert against the JSON.
|
||||
t.test('storm_02: every player threshold is actually REACHABLE in the real storm', () => {
|
||||
const p = stormProfile(wild, undefined);
|
||||
assert(p.maxGust > TUNE.stumbleGust,
|
||||
`StumbleBack is dead code: storm_02 gusts peak at ${p.maxGust.toFixed(1)} m/s over baseline, `
|
||||
+ `but stumbleGust is ${TUNE.stumbleGust}. Lower it or ask Lane C for angrier gusts.`);
|
||||
assert(p.maxGust > TUNE.shoveGustMin, `gust shove unreachable: peak ${p.maxGust.toFixed(1)}`);
|
||||
assert(p.maxWind > TUNE.knockWind,
|
||||
`knockdown unreachable: storm_02 peaks at ${p.maxWind.toFixed(1)} m/s, knockWind is ${TUNE.knockWind}`);
|
||||
});
|
||||
|
||||
t.test('storm_02: reads as shoved → stumbling → floored, in that order of frequency', () => {
|
||||
const p = stormProfile(wild, undefined);
|
||||
assert(p.stumbles >= 2, `a wild night should break your stride more than twice, got ${p.stumbles}`);
|
||||
assert(p.knocks >= 1, `and floor you at least once, got ${p.knocks}`);
|
||||
assert(p.stumbles > p.knocks,
|
||||
`stumble is the beat and knockdown the punchline — got ${p.stumbles} stumbles vs ${p.knocks} knocks`);
|
||||
assert(p.shovedFor > 10, `you should feel pushed for a real slice of the storm, got ${p.shovedFor.toFixed(1)}s`);
|
||||
assertLess(p.knocks, 8, `${p.knocks} knockdowns in 90 s is unplayable, not dramatic`);
|
||||
});
|
||||
|
||||
t.test('storm_01: a calm day leaves you completely alone', () => {
|
||||
const p = stormProfile(calm, undefined);
|
||||
assertEq(p.stumbles, 0, 'no stumbles on a gentle day');
|
||||
assertEq(p.knocks, 0, 'no knockdowns on a gentle day');
|
||||
assertLess(p.shovedFor, 1, 'and essentially no shove');
|
||||
});
|
||||
|
||||
t.test('storm_02: bracing is what makes the worst of it survivable', () => {
|
||||
const exposed = new PlayerSim({ start: { x: 0, y: 0, z: 4 } });
|
||||
const braced = new PlayerSim({ start: { x: 0, y: 0, z: 4 } });
|
||||
let exposedKnocks = 0, bracedKnocks = 0;
|
||||
fixedLoop(90, DT, (dt, tt) => {
|
||||
exposed.step(dt, tt, {}, wild);
|
||||
braced.step(dt, tt, { shelter: true }, wild);
|
||||
for (const e of exposed.events) if (e.state === 'knocked') exposedKnocks++;
|
||||
for (const e of braced.events) if (e.state === 'knocked') bracedKnocks++;
|
||||
exposed.events.length = 0; braced.events.length = 0;
|
||||
});
|
||||
assert(exposedKnocks > 0, 'storm_02 floors you if you just stand in it');
|
||||
assertLess(bracedKnocks, exposedKnocks, 'and bracing through it is strictly better');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- solids collision
|
||||
t.test('collision: solids stop you, and the pushout slides you along them', () => {
|
||||
// one box: the yard's north wall, x -8..8, z -16..-10, waist high
|
||||
|
||||
Loading…
Reference in New Issue
Block a user