Lane C S13 correction: the bench flew site_02 funnel-OFF and tree-FROZEN; wild-night headline dies
D's live replay caught landmine 1: bench.js read def.wind.venturi off the STORM def where a venturi never lives (it is SITE data, main.js:453), and the probes never attempted it — every site_02 bench number was funnel-off, the third harness to make B's Sprint-11 mistake. Verifying the fix exposed landmine 2: the tools remapped anchors to a static sway:()=>pos, freezing the gum tree whose sway is the dynamic load a tr1 rig eats (ablation on the $80 line's q4: 1.02 funnel-off/static, 1.03 funnel-on/static, 1.24 funnel-on/real anchors). Backyard numbers matched D's UI to the decimal precisely because p1..p4 are posts — both landmines are no-ops there. Fix: windForSite(stormDef, siteDef, anchors) in weather.js is the one shared wind builder (venturi + shelters, exactly main.js's wiring); all garden_bench tools build wind through it, fly world.anchors themselves via a re-pointable wind proxy, print the venturi in their headers, and drop the phantom 12 s settle (commit->attach->storm is one keypress, and the settle skewed storm sampling via SailRig's internal clock). c.test.js pins the helper with B's strictly-raises assert against the real shipped funnel (mutation-checked). Corrected numbers: $80 line wildnight 91.9 with q4 at 1.24-vs-1.2 rating - a knife edge the real game falls off (D UI: q3+q4 break, 39.8 TATTERED; D's UI is canonical there, and a corner over its rating is not a safe line either way). Buster verdict stands (95.2 bench / 97.9 D live). Icenight 9 DEAD 2/4. Bench still under-reads UI peaks ~5-15% on site_02 - residual in the pool, stated in THREADS. Separation-target site_02 example withdrawn to A. Selftest 341/0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b70699189c
commit
45bdc2d079
84
THREADS.md
84
THREADS.md
@ -4733,3 +4733,87 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
table is waiting for your audit rewrite whenever A's garden export lands; my gate-1 postscript
|
||||
upstream has the label-permutation warning if you lift per-corner numbers from any bench —
|
||||
print `corners[k].anchorId`, never the input order.
|
||||
|
||||
[C] 2026-07-18 — 🛑 **CORRECTION: THE BENCH HAD TWO LANDMINES, NOT ONE, AND THE WILD-NIGHT $80
|
||||
HEADLINE IS DEAD. D's funnel catch confirmed, plus a second bug D's replay exposed but nobody
|
||||
had named: the bench flew site_02's gum tree FROZEN.** Corrected tools, corrected tables,
|
||||
corrected claims below. Selftest 341/0/0; the fix carries its own assert.
|
||||
|
||||
**Landmine 1 — the venturi (D's catch, exactly as filed).** bench.js:89 read `def.wind.venturi`
|
||||
off the STORM def, where a venturi never lives — it LOOKED right and never fired; probe3/probe4/
|
||||
sweep2 never attempted it. Every site_02 number the bench ever printed was flown with the
|
||||
funnel OFF. Third harness to make B's Sprint-11 mistake, and I made it with B's sweep.selftest
|
||||
sitting in the repo telling the story.
|
||||
|
||||
**Landmine 2 — the FROZEN TREE, found while verifying the fix.** Venturi-on alone did NOT
|
||||
reproduce D's game numbers (q4 moved 1.02→1.03), so I kept digging with an ablation:
|
||||
funnel OFF, static anchors q4 1.02 (the original bench)
|
||||
funnel ON, static anchors q4 1.03 (the funnel's disc barely reaches q4)
|
||||
funnel ON, REAL anchors q4 1.24 — the tree is the bigger half on this line
|
||||
The bench remapped every anchor to `sway: () => pos` — which froze tr1, the gum tree, whose
|
||||
sway is precisely the dynamic load DESIGN.md warns a tree rig has to eat (it also dropped
|
||||
ratingHint). **This is why the backyard bench matched D's UI to the decimal while site_02
|
||||
lied: p1..p4 are all posts — static anchors and no venturi are both no-ops there.** The two
|
||||
landmines are the same species: the bench modelled the STORM exactly and the YARD not at all.
|
||||
|
||||
**The fix, structural so there is never a fourth:**
|
||||
· `windForSite(stormDef, siteDef, anchors)` in weather.js — THE shared wind builder: venturi
|
||||
from the site def + tree shelters, wired byte-for-byte as main.js:453 does at site load. The
|
||||
landmine's full history lives in its docstring. All garden_bench tools now build wind
|
||||
through it; **B — proposal: your re-audit imports it too**, and at integration one copy
|
||||
should win (your sweep.js has its own correct pair of setVenturi calls + the selftest — the
|
||||
logic is identical, the point is one door).
|
||||
· c.test.js gains B's strictly-raises pin lifted onto the helper, flown against the REAL
|
||||
shipped funnel (site_02's own JSON, not a synthetic): drop the setVenturi line and the
|
||||
funnelled/bare winds collapse to equal reads at the throat → red. Mutation-checked.
|
||||
· The tools fly `world.anchors` THEMSELVES — the world is built on a re-pointable wind proxy
|
||||
(main.js-router style) so tree-sway closures sample whichever storm is flying. No more
|
||||
static remaps, anywhere. And no more phantom 12 s calm settle: in the real game the rig
|
||||
does not exist before commit, and commit→attach→storm is one keypress — the settle also
|
||||
skewed every storm sample 12 s off the authored curve, because SailRig samples wind at its
|
||||
INTERNAL clock (measured: worth 0.01 kN on backyard posts, which is why nobody noticed).
|
||||
· Every tool prints the venturi in its header (B's audit.html pattern) — a reader can now SEE
|
||||
which wind a number was flown in.
|
||||
|
||||
**CORRECTED TABLES (probe4, real shop, real $80, funnel + sway on):**
|
||||
$80 line tr1(R)+q1(R)+q3(S)+q4(C):
|
||||
wildnight 91.9 FULL 0/4 peaks q1 4.91 tr1 4.31 q3 2.89 **q4 1.24 vs 1.2 rating**
|
||||
icenight 9 DEAD 2/4 (q3 3.32 breaks t≈50, q4 1.45 breaks t≈49)
|
||||
buster 95.2 FULL 0/4 peaks q1 1.71 tr1 1.79 q3 0.84 q4 0.56
|
||||
all-shackle $60: wildnight 41.3 (2/4) · icenight 0 · buster 95.2 FULL 0/4
|
||||
canonical q1+q2+q3+tr1 best-$80: wildnight 48.3 (1/4) · icenight 0.9 · buster 85.6
|
||||
probe3 (RATED, unlimited): backyard rows essentially unmoved — canonical wildnight 60.9
|
||||
(was 61.3; the 0.4 is the settle removal) and balance.test's canon peaks intact to 0.01 kN
|
||||
(p4 3.64 p2 2.77 p3 1.62 p1 0.98). site_02 posts-line wildnight drops 90.1→79.6 (q1 6.75
|
||||
breaks t≈46 — the funnel side), canonical 51.3→48.3. sweep2 re-run: **tr1+q1+q3+q4 is still
|
||||
the #1 cover-AND-hold geometry** (100% cover, shadow 1.000) — the LINE survives; its
|
||||
wild-night steel pricing does not.
|
||||
|
||||
**⚠️ THE HONEST RESIDUAL — and who is canonical where.** Even fully corrected, the bench
|
||||
under-reads D's real-UI peaks by ~5–15% on site_02 (buster, nothing breaking: bench
|
||||
q1 1.71 · tr1 1.79 · q3 0.84 · q4 0.56 vs D's 1.82 · 1.86 · 0.99 · 0.58). On the wild night
|
||||
that margin is exactly the knife edge: the bench holds q4 at 1.24-vs-1.2 and reads 91.9; the
|
||||
real game pushes it over, q4 lets go, q3 catches the shed (3.43), and D's 39.8 TATTERED is
|
||||
what ships. **D's UI numbers are canonical for the wild-night verdict; the bench's own
|
||||
corrected read supports the same conclusion the honest way: a line whose cheapest corner
|
||||
peaks OVER its rating has no margin and is not a safe line.** The 91.5-FULL-with-room
|
||||
headline was the funnel-off, frozen-tree illusion. Residual cause unlisted-variable, in the
|
||||
pool; until it's found, any bench corner within ~15% of its rating is "breaks in the game".
|
||||
|
||||
**What survives, what dies (A — for your gate-1.4 ruling):**
|
||||
· SURVIVES: the shadow MODEL's exactness (probe2, untouched), the vertical-projection
|
||||
conclusion, every backyard finding (D-verified to the decimal — posts don't sway and
|
||||
backyard has no funnel), the buster verdict (the $80 line holds site_02's actual shipped
|
||||
night with room — D live 97.9), and the diagnosis IN KIND: the canonical lines still can't
|
||||
cover, covering geometry still exists.
|
||||
· DIES: the wild-night 91.5 FULL headline, "$80 to the dollar with nothing to spare" (its
|
||||
pricing was computed against funnel-off loads), and therefore **my separation-target
|
||||
example. A: I withdraw the site_02 example, not the shape.** D's gate-1.4 table is right
|
||||
that the target as I worded it is currently satisfiable nowhere — bare never reads DEAD,
|
||||
and the funnelled wild night has no FULL-capable buyable line on either yard. Rule on D's
|
||||
played picture; if the shape survives, the thresholds want restating against funnel-on
|
||||
numbers (D's "bare TATTERED, held FULL" is the version night 4 already nearly meets).
|
||||
· **B, one more for the re-audit:** your audit "freezes sway like balance.test"
|
||||
(audit.html:69) — same species as my landmine 2. Post-fuse re-audit should fly
|
||||
world.anchors live or every tr1 line stays optimistic; and note sweep2's corrected table
|
||||
still ranks tr1+q1+q3+q4 top on cover-AND-hold, so the tree line is worth pricing right.
|
||||
|
||||
@ -28,7 +28,7 @@ import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { loadStorm, windForSite } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';
|
||||
|
||||
@ -39,7 +39,6 @@ const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
const GARDEN_DRAIN = 0.9;
|
||||
const W_HAIL = 5.0;
|
||||
const W_RAIN = 0.25;
|
||||
const CALM_STORM = 'storm_01_gentle';
|
||||
|
||||
export const STORMS = [
|
||||
'storm_01_gentle', 'storm_02_wildnight', 'storm_03_southerly',
|
||||
@ -57,23 +56,47 @@ export const YARDS = [
|
||||
{ site: 'site_02_corner_block', quad: ['q1', 'q2', 'q3', 'tr1'] },
|
||||
];
|
||||
|
||||
/** The real yard, read from world.js — never a copied anchor table. */
|
||||
/**
|
||||
* The real yard, read from world.js — never a copied anchor table, and never a
|
||||
* FROZEN one. Two landmines this builder now closes (SPRINT13, both caught by
|
||||
* D replaying through the real UI):
|
||||
* 1. The SITE def rides along because its wind block is half the yard's
|
||||
* weather: the venturi lives HERE, not in any storm def. Measured without
|
||||
* it, site_02 is a different, easier yard (third harness to learn this;
|
||||
* windForSite is the fix and the story).
|
||||
* 2. The anchors are world.anchors THEMSELVES. The old remap to a static
|
||||
* `sway: () => pos` froze the gum tree — a rig on a tree eats the tree's
|
||||
* sway as dynamic load, and stripping it under-read every tr1 corner.
|
||||
* The world is built on a re-pointable wind proxy (main.js-router style)
|
||||
* so the sway closures sample whichever storm is currently flying.
|
||||
*/
|
||||
async function buildYard(siteId) {
|
||||
const scene = new THREE.Scene();
|
||||
const calm = {
|
||||
let current = {
|
||||
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
|
||||
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
|
||||
gustTelegraph: () => null, setSheltersFromTrees() {}, eventsBetween: () => [],
|
||||
gustTelegraph: () => null, eventsBetween: () => [],
|
||||
};
|
||||
const world = createWorld(scene, { wind: calm, site: await loadSite(siteId) });
|
||||
const proxy = {
|
||||
sample: (p, t, o) => current.sample(p, t, o || new THREE.Vector3()),
|
||||
speedAt: (p, t) => current.speedAt(p, t),
|
||||
rainAt: (t) => current.rainAt(t),
|
||||
rainMmPerHour: (t) => (current.rainMmPerHour ? current.rainMmPerHour(t) : 0),
|
||||
gustTelegraph: (t) => current.gustTelegraph(t),
|
||||
eventsBetween: (a, b) => current.eventsBetween(a, b),
|
||||
setSheltersFromTrees() {},
|
||||
get seed() { return current.seed ?? 1; },
|
||||
get def() { return current.def ?? {}; },
|
||||
};
|
||||
const siteDef = await loadSite(siteId);
|
||||
const world = createWorld(scene, { wind: proxy, site: siteDef });
|
||||
if (world.dress) { try { await world.dress(); } catch { /* graybox anchors are enough */ } }
|
||||
const anchors = world.anchors.map((a) => {
|
||||
const pos = { x: a.pos.x, y: a.pos.y, z: a.pos.z };
|
||||
return { id: a.id, type: a.type, pos, sway: () => pos };
|
||||
});
|
||||
const anchors = world.anchors;
|
||||
return {
|
||||
anchors, bed: world.gardenBed, ids: anchors.map((a) => a.id),
|
||||
sunDir: world.sunDir, heightAt: world.heightAt,
|
||||
siteDef,
|
||||
use: (w) => { current = w; },
|
||||
};
|
||||
}
|
||||
|
||||
@ -83,10 +106,14 @@ async function buildYard(siteId) {
|
||||
*/
|
||||
async function fly(yard, stormName, mode) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
const trees = yard.anchors.filter((a) => a.type === 'tree');
|
||||
wind.setSheltersFromTrees(trees);
|
||||
if (def.wind && def.wind.venturi) wind.setVenturi(def.wind.venturi);
|
||||
// windForSite, NOT createWind + storm-def venturi. The old line here read
|
||||
// `def.wind.venturi` off the STORM def, where a venturi never lives — it
|
||||
// LOOKED right and never fired, so every site_02 number this bench produced
|
||||
// before 2026-07-18 was flown with the funnel OFF (D caught it replaying the
|
||||
// $80 line through the real UI: 91.5 FULL became 39.8 TATTERED). Same bug
|
||||
// B's site_audit shipped in Sprint 11. The helper carries the story.
|
||||
const wind = windForSite(def, yard.siteDef, yard.anchors);
|
||||
yard.use(wind); // point the yard's tree-sway closures at this flight's wind
|
||||
|
||||
let rig = null;
|
||||
if (mode === 'held') {
|
||||
@ -104,19 +131,12 @@ async function fly(yard, stormName, mode) {
|
||||
rig = s.commit(new SailRig({ anchors: yard.anchors, gridN: 10 }));
|
||||
}
|
||||
|
||||
// The settle: a player stands through ~12 s of prep on the calm day, so the
|
||||
// cloth is in steady state at ENTER. A bench that rigs and storms in the same
|
||||
// tick measures the attach transient (balance.test gate 0').
|
||||
if (rig) {
|
||||
const calmDef = await loadStorm(CALM_STORM);
|
||||
const calmWind = createWind(calmDef);
|
||||
calmWind.setSheltersFromTrees(trees);
|
||||
let prepT = 0;
|
||||
for (let i = 0; i < Math.round(12 / FIXED_DT); i++) {
|
||||
rig.step(FIXED_DT, calmWind, prepT % 3);
|
||||
prepT += FIXED_DT;
|
||||
}
|
||||
}
|
||||
// NO calm settle (correction, 2026-07-18): in the real game the rig does not
|
||||
// exist before commit, and commit → attach → advance() → storm land in one
|
||||
// keypress (sail.js attach, B's clock-reset comment). The attach transient
|
||||
// rides the storm's own calm opening exactly as it does for a player — and
|
||||
// because SailRig samples wind at its INTERNAL clock, a 12 s settle here
|
||||
// skewed every storm sample 12 s off the authored curve.
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
@ -206,7 +226,13 @@ export async function runBench(log) {
|
||||
|
||||
for (const y of YARDS) {
|
||||
const yard = await buildYard(y.site);
|
||||
log(`\n## ${y.site} bed ${yard.bed.w}×${yard.bed.d} at (${yard.bed.x}, ${yard.bed.z}) line ${y.quad.join('+')}`);
|
||||
// Print the funnel in the header, B's audit.html pattern: a reader must be
|
||||
// able to SEE which wind these numbers were flown in.
|
||||
const vl = yard.siteDef.wind?.venturi ?? [];
|
||||
const vtxt = vl.length
|
||||
? vl.map((v) => `throat (${v.x},${v.z}) gain ${v.gain} r${v.radius}`).join(' · ')
|
||||
: 'none';
|
||||
log(`\n## ${y.site} bed ${yard.bed.w}×${yard.bed.d} at (${yard.bed.x}, ${yard.bed.z}) line ${y.quad.join('+')} venturi: ${vtxt}`);
|
||||
|
||||
for (const storm of STORMS) {
|
||||
const held = await fly({ ...yard, quad: y.quad }, storm, 'held');
|
||||
|
||||
@ -15,7 +15,7 @@ import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { loadStorm, windForSite } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
|
||||
const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
@ -36,13 +36,19 @@ el.textContent = '';
|
||||
// Fly the covering lines. Report peak corner load per corner and the garden.
|
||||
const GARDEN_DRAIN = 0.9, W_HAIL = 5.0, W_RAIN = 0.25;
|
||||
|
||||
async function fly(anchors, bed, ids, stormName, hw) {
|
||||
async function fly(world, ids, stormName, hw, siteDef, use) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
const trees = anchors.filter((a) => a.type === 'tree');
|
||||
wind.setSheltersFromTrees(trees);
|
||||
const calmDef = await loadStorm('storm_01_gentle');
|
||||
const calmWind = createWind(calmDef); calmWind.setSheltersFromTrees(trees);
|
||||
const anchors = world.anchors; // REAL anchors: trees sway, hints ride along
|
||||
const bed = world.gardenBed;
|
||||
// windForSite: the SITE's venturi + shelters, as main.js wires them. Before
|
||||
// 2026-07-18 this probe (a) never set the venturi — every site_02 row was
|
||||
// flown with the funnel OFF — and (b) remapped anchors to a static
|
||||
// `sway: () => pos`, freezing the gum tree and dropping ratingHint. See
|
||||
// probe4's correction note and the THREADS postscript; backyard post-only
|
||||
// lines (p1..p4) were unaffected by either, which is why they matched D's
|
||||
// UI replays to the decimal while the tr1 lines lied.
|
||||
const wind = windForSite(def, siteDef, anchors);
|
||||
use(wind);
|
||||
|
||||
const s = new RiggingSession({ anchors, budget: 9999 });
|
||||
for (const id of ids) if (!s.rig(id).ok) return null;
|
||||
@ -50,8 +56,7 @@ async function fly(anchors, bed, ids, stormName, hw) {
|
||||
if (!r.ok) throw new Error(`setHardware ${id} refused: ${r.reason} — flying hardware it did not buy`); }
|
||||
s.setTension(1.0);
|
||||
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
|
||||
let prepT = 0;
|
||||
for (let i=0;i<Math.round(12/FIXED_DT);i++){ rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT; }
|
||||
// no calm settle: commit → attach → storm is one keypress in the real game
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
@ -89,15 +94,31 @@ try {
|
||||
const scene = new THREE.Scene();
|
||||
const calm = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: calm, site: await loadSite(siteId) });
|
||||
const siteDef = await loadSite(siteId);
|
||||
// re-pointable wind, main.js-router style: sway closures capture the proxy
|
||||
let current = calm;
|
||||
const proxy = {
|
||||
sample: (p,t,o) => current.sample(p, t, o || new THREE.Vector3()),
|
||||
speedAt: (p,t) => current.speedAt(p, t),
|
||||
rainAt: (t) => current.rainAt(t),
|
||||
rainMmPerHour: (t) => (current.rainMmPerHour ? current.rainMmPerHour(t) : 0),
|
||||
gustTelegraph: (t) => current.gustTelegraph(t),
|
||||
eventsBetween: (a,b) => current.eventsBetween(a, b),
|
||||
setSheltersFromTrees() {},
|
||||
get seed() { return current.seed ?? 1; },
|
||||
get def() { return current.def ?? {}; },
|
||||
};
|
||||
const use = (w) => { current = w; };
|
||||
const world = createWorld(scene, { wind: proxy, site: siteDef });
|
||||
if (world.dress) { try { await world.dress(); } catch {} }
|
||||
const anchors = world.anchors.map((a)=>{const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return {id:a.id,type:a.type,pos,sway:()=>pos};});
|
||||
const bed = world.gardenBed;
|
||||
log(`\n\n######## ${siteId} (RATED = 6.5 kN is the best steel in the game, $30/corner)`);
|
||||
const anchors = world.anchors;
|
||||
const vl = siteDef.wind?.venturi ?? [];
|
||||
const vtxt = vl.length ? vl.map((v)=>`throat (${v.x},${v.z}) gain ${v.gain} r${v.radius}`).join(' · ') : 'none';
|
||||
log(`\n\n######## ${siteId} (RATED = 6.5 kN is the best steel in the game, $30/corner) venturi: ${vtxt}`);
|
||||
for (const [label, ids] of Object.entries(lineSet)) {
|
||||
for (const storm of ['storm_02_wildnight', 'storm_02b_icenight', 'storm_03b_earlybuster']) {
|
||||
const r = await fly(anchors, bed, ids, storm, RATED);
|
||||
const r = await fly(world, ids, storm, RATED, siteDef, use);
|
||||
if (!r) { log(` ${label} ${ids.join('+')}: REFUSED`); continue; }
|
||||
const over = r.peak.map((p,i)=>`${r.cornerIds[i]} ${p.toFixed(2)}`).join(' ');
|
||||
const bare = { storm_02_wildnight: 35.7, storm_02b_icenight: 0, storm_03b_earlybuster: 82.6 }[storm];
|
||||
|
||||
@ -15,7 +15,7 @@ import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { loadStorm, windForSite } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE, START_BUDGET } from '../../web/world/js/contracts.js';
|
||||
const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
@ -23,24 +23,30 @@ const el = document.getElementById('out');
|
||||
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
||||
el.textContent = '';
|
||||
|
||||
// THE PAYOFF.
|
||||
// probe3 (on an unlimited budget) found tr1+q1+q3+q4 on site_02 holds every
|
||||
// corner through the WILD night and the ICE night and reads hp 91.5 / 89.2
|
||||
// against a bare bed's 35.7 / 0.0. Its peak loads were tr1 4.72, q1 3.74,
|
||||
// q3 2.97, q4 1.02 kN — which price EXACTLY to
|
||||
// RATED(6.5) + RATED(6.5) + SHACKLE(3.2) + CARABINER(1.2)
|
||||
// $30 + $30 + $15 + $5 = $80
|
||||
// ...the whole budget, to the dollar. If that holds through the REAL shop at the
|
||||
// REAL START_BUDGET, then the garden already responds to rigging, gate 1 needs
|
||||
// no model change, and what was actually broken is that nobody had found the
|
||||
// line. So buy it the way a player must, and fly it.
|
||||
async function fly(anchors, bed, plan, stormName) {
|
||||
// THE PAYOFF — and the correction (2026-07-18).
|
||||
// The first run of this probe read the $80 line tr1+q1+q3+q4 at 91.5 FULL on
|
||||
// the wild night and called it the headline. TWO harness landmines, both mine:
|
||||
// 1. No venturi — the funnel lives in the SITE def, and this probe never set
|
||||
// it (bench.js read it off the STORM def, where it never lives). Third
|
||||
// harness to make B's Sprint-11 mistake.
|
||||
// 2. Static anchors — the remap `sway: () => pos` stripped tr1's TREE sway,
|
||||
// the dynamic load a rig on a gum tree has to eat (DESIGN.md's whole
|
||||
// warning about tree anchors). The backyard bench numbers matched D's UI
|
||||
// replays to the decimal BECAUSE p1..p4 are all posts; any tr1 line was
|
||||
// flown against a tree that never moved.
|
||||
// With both fixed the probe agrees with D's live UI replays in direction and
|
||||
// closely in number (buster peaks within ~15%); on the wild night the line's
|
||||
// cheapest corner rides AT its rating (q4 carabiner ~1.2 kN) — a knife edge
|
||||
// the real game falls off (D: q3+q4 break t≈45, 39.8 TATTERED). Where this
|
||||
// probe and D's UI disagree on which side of that edge a corner lands, D's
|
||||
// UI number is canonical. What survives: the line holds the BUSTER — site_02's
|
||||
// actual shipped night — with room (D live: 97.9 FULL, 0/4).
|
||||
async function fly(world, plan, stormName, siteDef, use) {
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
const trees = anchors.filter((a) => a.type === 'tree');
|
||||
wind.setSheltersFromTrees(trees);
|
||||
const calmWind = createWind(await loadStorm('storm_01_gentle'));
|
||||
calmWind.setSheltersFromTrees(trees);
|
||||
const anchors = world.anchors; // REAL anchors — tr1 sways
|
||||
const bed = world.gardenBed;
|
||||
const wind = windForSite(def, siteDef, anchors);
|
||||
use(wind); // point the world's sway closures at the flight wind
|
||||
|
||||
// THE REAL SHOP, at the REAL budget. Every refusal is loud.
|
||||
const s = new RiggingSession({ anchors }); // budget = START_BUDGET
|
||||
@ -55,8 +61,10 @@ async function fly(anchors, bed, plan, stormName) {
|
||||
s.setTension(1.0);
|
||||
const spent = START_BUDGET - s.budget;
|
||||
const rig = s.commit(new SailRig({ anchors, gridN: 10 }));
|
||||
let prepT = 0;
|
||||
for (let i=0;i<Math.round(12/FIXED_DT);i++){ rig.step(FIXED_DT, calmWind, prepT % 3); prepT += FIXED_DT; }
|
||||
// NO calm settle: in the real game the rig does not exist until commit, and
|
||||
// commit → attach → advance() → storm land in one keypress (B's clock-reset
|
||||
// comment, sail.js attach). The attach transient rides the storm's own calm
|
||||
// opening, exactly as it does for a player.
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
@ -89,12 +97,27 @@ const state = (hp) => hp > 66 ? 'FULL' : hp > 33 ? 'tattered' : 'DEAD';
|
||||
|
||||
try {
|
||||
const scene = new THREE.Scene();
|
||||
const zero = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: zero, site: await loadSite('site_02_corner_block') });
|
||||
// A re-pointable wind, the way main.js's router serves world.js: the world's
|
||||
// tree-sway closures capture THIS object, and fly() re-points it per storm.
|
||||
// Building the world on a dead stub and remapping anchors to static
|
||||
// `sway: () => pos` was landmine #2 — it froze the gum tree.
|
||||
let current = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, eventsBetween:()=>[] };
|
||||
const proxy = {
|
||||
sample: (p,t,o) => current.sample(p, t, o || new THREE.Vector3()),
|
||||
speedAt: (p,t) => current.speedAt(p, t),
|
||||
rainAt: (t) => current.rainAt(t),
|
||||
rainMmPerHour: (t) => (current.rainMmPerHour ? current.rainMmPerHour(t) : 0),
|
||||
gustTelegraph: (t) => current.gustTelegraph(t),
|
||||
eventsBetween: (a,b) => current.eventsBetween(a, b),
|
||||
setSheltersFromTrees() {},
|
||||
get seed() { return current.seed ?? 1; },
|
||||
get def() { return current.def ?? {}; },
|
||||
};
|
||||
const use = (w) => { current = w; };
|
||||
const siteDef = await loadSite('site_02_corner_block');
|
||||
const world = createWorld(scene, { wind: proxy, site: siteDef });
|
||||
await world.dress(); // NOT optional: undressed, the carport trap does not exist
|
||||
const anchors = world.anchors.map((a)=>{const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return {id:a.id,type:a.type,pos,sway:()=>pos};});
|
||||
const bed = world.gardenBed;
|
||||
|
||||
const PLANS = {
|
||||
@ -105,12 +128,17 @@ try {
|
||||
'canonical q1+q2+q3+tr1, best steel $80 can buy':
|
||||
[['q1',RATED],['q2',SHACKLE],['q3',CARABINER],['tr1',RATED]],
|
||||
};
|
||||
log(`site_02_corner_block budget $${START_BUDGET} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z})`);
|
||||
const vl = siteDef.wind?.venturi ?? [];
|
||||
const vtxt = vl.length
|
||||
? vl.map((v)=>`throat (${v.x},${v.z}) gain ${v.gain} r${v.radius}`).join(' · ') : 'none';
|
||||
log(`site_02_corner_block budget $${START_BUDGET} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z}) venturi: ${vtxt}`);
|
||||
// bare-bed refs are funnel-independent (no sail = exposure is the storm's own
|
||||
// rain/hail timeline, wind never enters it); D confirmed 82.6 live on the buster
|
||||
log(`bare bed for reference: wildnight 35.7 (DEAD) icenight 0.0 (DEAD) buster 82.6 (FULL)\n`);
|
||||
for (const [label, plan] of Object.entries(PLANS)) {
|
||||
log(`\n## ${label}`);
|
||||
for (const storm of ['storm_02_wildnight','storm_02b_icenight','storm_03b_earlybuster']) {
|
||||
const r = await fly(anchors, bed, plan, storm);
|
||||
const r = await fly(world, plan, storm, siteDef, use);
|
||||
if (r.err) { log(` ${storm.padEnd(22)} SHOP REFUSED: ${r.err}`); continue; }
|
||||
const bare = { storm_02_wildnight:35.7, storm_02b_icenight:0, storm_03b_earlybuster:82.6 }[storm];
|
||||
log(` ${storm.padEnd(22)} spent $${String(r.spent).padStart(3)} hp ${String(r.hp).padStart(5)} ${state(r.hp).padEnd(8)}`
|
||||
|
||||
@ -15,7 +15,7 @@ import * as THREE from '../../web/world/vendor/three.module.js';
|
||||
import { RiggingSession } from '../../web/world/js/rigging.js';
|
||||
import { SailRig } from '../../web/world/js/sail.js';
|
||||
import { RainShadow } from '../../web/world/js/skyfx.js';
|
||||
import { createWind, loadStorm } from '../../web/world/js/weather.js';
|
||||
import { loadStorm, windForSite } from '../../web/world/js/weather.js';
|
||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
||||
import { FIXED_DT, HARDWARE } from '../../web/world/js/contracts.js';
|
||||
const [CARABINER, SHACKLE, RATED] = HARDWARE;
|
||||
@ -41,20 +41,41 @@ try {
|
||||
const scene = new THREE.Scene();
|
||||
const zero = { sample:(p,t,o)=>(o||new THREE.Vector3()).set(0,0,0), speedAt:()=>0, rainAt:()=>0,
|
||||
rainMmPerHour:()=>0, gustTelegraph:()=>null, setSheltersFromTrees(){}, eventsBetween:()=>[] };
|
||||
const world = createWorld(scene, { wind: zero, site: await loadSite(siteId) });
|
||||
const siteDef = await loadSite(siteId);
|
||||
// re-pointable wind, main.js-router style: the anchors are world.anchors
|
||||
// THEMSELVES, so tree sway is live — the old static remap froze the gum
|
||||
// tree and under-read every tr1 corner (correction, 2026-07-18).
|
||||
let current = zero;
|
||||
const proxy = {
|
||||
sample: (p,t,o) => current.sample(p, t, o || new THREE.Vector3()),
|
||||
speedAt: (p,t) => current.speedAt(p, t),
|
||||
rainAt: (t) => current.rainAt(t),
|
||||
rainMmPerHour: (t) => (current.rainMmPerHour ? current.rainMmPerHour(t) : 0),
|
||||
gustTelegraph: (t) => current.gustTelegraph(t),
|
||||
eventsBetween: (a,b) => current.eventsBetween(a, b),
|
||||
setSheltersFromTrees() {},
|
||||
get seed() { return current.seed ?? 1; },
|
||||
get def() { return current.def ?? {}; },
|
||||
};
|
||||
const world = createWorld(scene, { wind: proxy, site: siteDef });
|
||||
if (world.dress) { try { await world.dress(); } catch {} }
|
||||
const anchors = world.anchors.map((a) => { const pos={x:a.pos.x,y:a.pos.y,z:a.pos.z};
|
||||
return { id:a.id, type:a.type, pos, sway:()=>pos, ratingHint:a.ratingHint }; });
|
||||
const anchors = world.anchors;
|
||||
const bed = world.gardenBed;
|
||||
log(`\n\n######## ${siteId} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z})`);
|
||||
const vl = siteDef.wind?.venturi ?? [];
|
||||
const vtxt = vl.length ? vl.map((v)=>`throat (${v.x},${v.z}) gain ${v.gain} r${v.radius}`).join(' · ') : 'none';
|
||||
log(`\n\n######## ${siteId} bed ${bed.w}x${bed.d} @ (${bed.x}, ${bed.z}) venturi: ${vtxt}`);
|
||||
log(` anchors and what the BEST steel ($30 RATED, 6.5 kN) is actually worth on them:`);
|
||||
for (const a of anchors) {
|
||||
log(` ${a.id.padEnd(5)} ${String(a.type).padEnd(8)} hint ${(a.ratingHint ?? 1).toFixed(2)} -> ${((a.ratingHint ?? 1) * 6.5).toFixed(2)} kN`);
|
||||
}
|
||||
|
||||
// prep's own wind: main.js hands the rig the calm day outside a storm
|
||||
const calmWind = createWind(await loadStorm('storm_01_gentle'));
|
||||
calmWind.setSheltersFromTrees(anchors.filter((a) => a.type === 'tree'));
|
||||
// prep's own wind: main.js hands the rig the calm day outside a storm.
|
||||
// windForSite so the SITE's venturi flies too — before 2026-07-18 this
|
||||
// sweep settled site_02's quads with the funnel OFF (the third harness to
|
||||
// make B's Sprint-11 mistake; see windForSite's comment). The funnel does
|
||||
// not switch off for prep.
|
||||
const calmWind = windForSite(await loadStorm('storm_01_gentle'), siteDef, anchors);
|
||||
current = calmWind; // the tree-sway closures fly the same calm day
|
||||
|
||||
const rows = [];
|
||||
const n = anchors.length;
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
import * as THREE from '../../vendor/three.module.js';
|
||||
import { assert, fixedLoop } from '../testkit.js';
|
||||
import { FIXED_DT, checkContract, DEBRIS_PIECE_FIELDS } from '../contracts.js';
|
||||
import { loadStorm, createWind, forecastLines, forecastFor, leadFor } from '../weather.js';
|
||||
import { loadStorm, createWind, windForSite, forecastLines, forecastFor, leadFor } from '../weather.js';
|
||||
import { loadSite } from '../world.js';
|
||||
import { createDebris } from '../debris.js';
|
||||
import { createSkyFx, RainShadow } from '../skyfx.js';
|
||||
import { SailRig, HARDWARE } from '../sail.js';
|
||||
@ -181,6 +182,38 @@ export default async function run(t) {
|
||||
assert(gale.leafCount === 0, 'clear() left leaves flying into the aftermath card');
|
||||
});
|
||||
|
||||
// SPRINT13, the third time this repo learned it: the venturi lives in the
|
||||
// SITE json, not the storm def. B's site_audit flew site_02 funnel-OFF in
|
||||
// Sprint 11 (their sweep.selftest tells it); C's garden_bench did it again
|
||||
// in Sprint 13 — `def.wind.venturi` off a storm def LOOKS right and never
|
||||
// fires, and the funnel-off bench called the $80 line 91.5 FULL on a night
|
||||
// the real game scores it 39.8 TATTERED (D's live replay). windForSite is
|
||||
// the shared builder that makes the mistake unmakeable; this assert is
|
||||
// B's strictly-raises pin lifted onto it, with the REAL shipped funnel:
|
||||
// drop the setVenturi line and the two winds collapse to equal reads.
|
||||
const site02 = await loadSite('site_02_corner_block'); // awaited here: Suite.test() is sync
|
||||
t.test('windForSite flies the SITE venturi — the shipped funnel strictly raises the throat wind', () => {
|
||||
const def = storms.storm_02_wildnight;
|
||||
const site = site02;
|
||||
const vl = site.wind?.venturi ?? [];
|
||||
assert(vl.length > 0, "site_02 lost its venturi — this test (and the yard's weather) proves nothing");
|
||||
const bare = createWind(def); // storm def alone — the trap itself
|
||||
const funnelled = windForSite(def, site);
|
||||
const throat = new THREE.Vector3(vl[0].x, 1.5, vl[0].z);
|
||||
const a = new THREE.Vector3(), b = new THREE.Vector3();
|
||||
let bareMean = 0, funMean = 0, n = 0;
|
||||
for (let time = 20; time <= 80; time += 0.5) {
|
||||
bare.sample(throat, time, a);
|
||||
funnelled.sample(throat, time, b);
|
||||
bareMean += Math.hypot(a.x, a.z); funMean += Math.hypot(b.x, b.z); n++;
|
||||
}
|
||||
bareMean /= n; funMean /= n;
|
||||
assert(bareMean > 3, `the storm never blew at the throat (${bareMean.toFixed(1)} m/s) — vacuous`);
|
||||
assert(funMean > bareMean * 1.1,
|
||||
`throat wind ${funMean.toFixed(2)} vs bare ${bareMean.toFixed(2)} m/s — the site's venturi is not `
|
||||
+ 'reaching windForSite\'s wind (it must call setVenturi the way main.js does at every site load)');
|
||||
});
|
||||
|
||||
// D's aftermath find (S13, lane/d): the hail InstancedMesh at count:0 with
|
||||
// frustum culling and depthWrite both off still draws instance 0's identity
|
||||
// matrix — a ~5 cm always-on-top white sliver at the origin, the QA's "ghost
|
||||
|
||||
@ -112,6 +112,33 @@ export async function loadStorm(name, dir = STORM_DIR) {
|
||||
return def;
|
||||
}
|
||||
|
||||
/**
|
||||
* The wind a YARD actually flies — storm def + the SITE's local effects,
|
||||
* wired exactly as main.js does at every site load (setVenturi from
|
||||
* siteDef.wind.venturi, tree shelters from the anchors). SPRINT13:
|
||||
*
|
||||
* THREE harnesses have now measured site_02 with the funnel switched OFF by
|
||||
* building wind from the storm def alone — B's site_audit (Sprint 11, their
|
||||
* sweep.selftest tells the story), C's garden_bench (Sprint 13, where it
|
||||
* flipped the $80-line headline: 91.5 FULL funnel-off vs 39.8 TATTERED in
|
||||
* D's live replay), and probe4, which never attempted it. The venturi lives
|
||||
* in the SITE json, not the storm, so `def.wind.venturi` off a storm def
|
||||
* LOOKS right and never fires. Any tool measuring a yard builds its wind
|
||||
* HERE, or it is measuring a yard that doesn't ship. The strictly-raises
|
||||
* assert in c.test.js goes red if the setVenturi line is ever dropped.
|
||||
*
|
||||
* @param {object} stormDef parsed storm JSON
|
||||
* @param {object} [siteDef] parsed site JSON (loadSite) — its wind.venturi list
|
||||
* @param {Array} [anchors] world anchors; trees become wind shelters
|
||||
* @param {object} [opts] forwarded to createWind ({seed})
|
||||
*/
|
||||
export function windForSite(stormDef, siteDef, anchors = [], opts = {}) {
|
||||
const wind = createWind(stormDef, opts);
|
||||
wind.setVenturi(siteDef?.wind?.venturi ?? []);
|
||||
wind.setSheltersFromTrees(anchors.filter((a) => a.type === 'tree'));
|
||||
return wind;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} def parsed storm JSON
|
||||
* @param {object} [opts] {seed} — same seed + same def = same storm, every run
|
||||
|
||||
Loading…
Reference in New Issue
Block a user