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>
136 lines
7.1 KiB
HTML
136 lines
7.1 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head><meta charset="utf-8"><title>probe3 — does the covering line hold?</title>
|
|
<style>body{background:#11161a;color:#dde5ea;font:13px/1.5 ui-monospace,Menlo,monospace;padding:18px}pre{white-space:pre}</style></head>
|
|
<body>
|
|
<pre id="out">running…</pre>
|
|
<script type="importmap">
|
|
{ "imports": {
|
|
"three": "../../web/world/vendor/three.module.js",
|
|
"three/addons/": "../../web/world/vendor/addons/"
|
|
} }
|
|
</script>
|
|
<script type="module">
|
|
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 { 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;
|
|
const el = document.getElementById('out');
|
|
const lines = []; const log = (s) => { lines.push(s); el.textContent = lines.join('\n'); };
|
|
el.textContent = '';
|
|
|
|
// THE DECIDING QUESTION.
|
|
// sweep.html found quads that cover 100% of the bed in BOTH yards, while the
|
|
// canonical line covers 25%. B's Sprint-9 sweep says every bed-covering quad has
|
|
// a corner over 6.5 kN — unholdable even on RATED, the best steel in the game —
|
|
// and that p1..p4 is the ONLY buyable line. If that's true, then "coverage" and
|
|
// "holdability" are anti-correlated by construction, the affordable rig can only
|
|
// ever shade a quarter of the bed, and NO shadow-model change can make the
|
|
// garden respond to rigging. That is a very different bug from the one the
|
|
// sprint chartered, so it has to be measured, not inherited.
|
|
//
|
|
// 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(world, ids, stormName, hw, siteDef, use) {
|
|
const def = await loadStorm(stormName);
|
|
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;
|
|
for (const id of ids) { const r = s.setHardware(id, 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 }));
|
|
// 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);
|
|
const sky = createSkyFx({ wind, night: true, camera });
|
|
const peak = ids.map(() => 0);
|
|
let hp = 100, lost = 0, hsum = 0, hn = 0, brokeAt = null;
|
|
const steps = Math.round(def.duration / FIXED_DT);
|
|
for (let i=0;i<steps;i++){
|
|
const t = i * FIXED_DT;
|
|
rig.step(FIXED_DT, wind, t);
|
|
sky.step(FIXED_DT, t, { sail: rig });
|
|
for (let k=0;k<rig.corners.length;k++) if ((rig.corners[k].load||0) > peak[k]) peak[k] = rig.corners[k].load;
|
|
if (brokeAt === null && rig.corners.some((c)=>c.broken)) brokeAt = t;
|
|
// the hail shadow WHILE it is hailing is the only one that scores
|
|
if (sky.hailAmount > 0) { hsum += sky.hailShadowOver(bed); hn++; }
|
|
const ex = W_HAIL*sky.gardenHailExposure(bed,t) + W_RAIN*sky.gardenExposure(bed,t);
|
|
if (ex > 0) hp = Math.max(0, hp - GARDEN_DRAIN*ex*FIXED_DT);
|
|
}
|
|
lost = rig.corners.filter((c)=>c.broken).length;
|
|
sky.dispose?.();
|
|
// Peaks are indexed by rig.corners — the RING order commit() reordered the
|
|
// picks into — so label them with corners[k].anchorId, NOT the input order.
|
|
// (Found at re-verify: p1..p4 flies as p4,p2,p3,p1; an input-order label
|
|
// attributes each corner's load to the wrong anchor.)
|
|
return { hp: Math.round(hp*10)/10, lost, peak: peak.map((p)=>p/1000),
|
|
cornerIds: rig.corners.map((c)=>c.anchorId),
|
|
hailShadow: hn ? hsum/hn : 0, brokeAt };
|
|
}
|
|
|
|
try {
|
|
for (const [siteId, lineSet] of [
|
|
['backyard_01', { cover100: ['h1','t1','t2b','p2'], cover100b: ['h1','h3','p1','p2'], canonical: ['p1','p2','p3','p4'] }],
|
|
['site_02_corner_block', { posts: ['q1','q2','q3','q4'], cover100: ['tr1','q1','q3','q4'], canonical: ['q1','q2','q3','tr1'] }],
|
|
]) {
|
|
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 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 bed = world.gardenBed;
|
|
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(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];
|
|
log(` ${label.padEnd(10)} ${ids.join('+').padEnd(18)} ${storm.padEnd(22)} hp ${String(r.hp).padStart(5)}`
|
|
+ ` (bare ${bare}, sep ${(r.hp-bare).toFixed(1).padStart(5)}) lost ${r.lost}/4`
|
|
+ `${r.brokeAt!==null?` @t=${r.brokeAt.toFixed(0)}`:' '} hailShadow ${r.hailShadow.toFixed(2)} peak kN: ${over}`);
|
|
}
|
|
log('');
|
|
}
|
|
}
|
|
} catch (e) { log('THREW: ' + (e && e.stack || e)); }
|
|
document.title = 'probe3 done';
|
|
</script>
|
|
</body></html>
|