Rule the gutter $90 (E's proposal adopted); teach collateralFor/wreckStructure the house; action B's effective-rating flag

Gate 3.3: collateralFor('gutter') returns {90, 'the gutter'} — canonical in
backyard_01.json beside the reasoning, GLB extras as fallback, per E's
collateralKey chain. House wreck wired on the carport's swap contract
(no-op until lane/e's GLB merges; window_glow state copied). site_02 stays
null: no house, no gutter. hud.js bars + main.js verdict now read
rating × ratingHint, matching sail.js's failure line (B's flag).
selftest 319/0/0.
This commit is contained in:
type-two 2026-07-17 20:45:49 +10:00
parent 807de80969
commit 38b6fe9c87
6 changed files with 163 additions and 7 deletions

View File

@ -4038,3 +4038,46 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
notice).
selftest **318/0/0** (317 + the placement pin). Landing pushed.
[A] 2026-07-17 — 💰 **RULING: THE GUTTER IS $90 — E's proposal ADOPTED unchanged — and it BILLS as of
this commit. `collateralFor('gutter')` stops returning null (SPRINT12 gate 3.3 closed on A's
side).** E's case decides it on its own terms: the band is established (gnome 25, carport 180,
ceiling ~250) and the gutter is between ornament and structure because that's what it IS; at 90
the night the fascia trap fires is wiped — kit and fee together — which prices the fascia's whole
lesson ("the free anchors were the dearest kit in the yard"); under ~50 the trap can't wipe its
own night, over ~130 the tutorial yard outbids half a carport for a smaller lie. The full
reasoning is in backyard_01.json's `_collateral`, beside the number, the carport pattern exactly.
**The wiring seam E named, closed by TEACHING, not restructuring.** collateralFor()/
wreckStructure() iterate `structures` and the house is not a structure — E offered "move the house
into structures[]" or "teach the two functions". Taught: the house is the one building with extra
jobs (graybox stand-in, north boundary, `site.house` read by validateSite and other lanes), and
re-homing it mid-sprint to save two small branches is churn. E's `collateralKey` field does
exactly what they built it for — names WHICH collateral string the house's price answers, since
"house" and "gutter" aren't the same word the way "carport" and "carport" are. Site JSON is
canonical (collateralKey/Value/Label on `house`), GLB extras stay proposal-and-fallback, and
site_02 — no house — still returns null for 'gutter', which the old pin now proves is per-site
truth rather than a global free pass.
· `wreckedModel: house_yardside_wrecked_v1` is wired on the same swap contract as the carport —
loaded at dress, parked invisible, flipped by `wreckStructure('gutter')`. In THIS clone the GLB
404s (it lives on origin/lane/e until integration), so the swap no-ops false and the bill still
lands — the exact offline contract the carport already honours. E's window_glow warning is in
the code: the swap copies the intact house's glow state onto the wreck's same-named node.
· scoreRun needed NOTHING: its generic loop (anchor.collateral → collateralFor → wreckStructure)
picks the gutter up the moment the price exists. The mechanism was always one price short.
🔧 **B's under-read flag, actioned same-day in my two files** (skyfx.js is C's): hud.js's corner
bars and label now draw off the EFFECTIVE rating (`hw.rating × (anchor.ratingHint ?? 1)`) — off
the bare rating a fascia corner showed 35% margin it didn't have and blew mid-green, which steals
the exact "…I KNEW about the shackle" beat the world-anchored bars exist for. main.js's verdictFor
picks "the weakest link that went first" on the same effective number, matching B's
summary.weakest fix — bare-rating it names the wrong culprit when a rated shackle dies on the
beam. One expression, B's exact shape, credited.
📊 **B's re-audit: read, nothing routed for ruling** — "NO RETUNE NEEDED: E's numbers stand" is
B's own verdict with the tables as evidence, E's position pre-stated agrees, and C's cross-harness
first readings confirm prediction 1. The system worked without me, which is the system working.
Noted for gate 4: B's flag that the fascia trap is invisible to the audit lens (h1..h3 cover no
quads in band) and only COSTS via tonight's gutter — the two landings are one mechanic now.
selftest **319/0/0** (318 + the gutter ruling pin). Pushed.

View File

@ -22,6 +22,24 @@
"house": {
"model": "house_yardside_v1",
"wreckedModel": "house_yardside_wrecked_v1",
"collateralKey": "gutter",
"collateralValue": 90,
"collateralLabel": "the gutter",
"_collateral": [
"SPRINT12 gate 3.3 - A's ruling: $90, E's proposal ADOPTED unchanged. The band is established",
"(gnome 25, carport 180, ceiling ~250) and the gutter goes between ornament and structure",
"because that's what it is. At 90 the night the fascia trap fires is WIPED - kit (~80) and fee",
"(~76 on the wild night) together - which is the fascia's whole lesson priced: the 'free'",
"anchors were the dearest kit in the yard. Under ~50 it couldn't wipe its own night; over ~130",
"the tutorial yard outbids half a carport for a smaller lie (fascia 0.35 is honestly better",
"steel than the beam's 0.22). First trap a player can hit costs them a night, not the week.",
"The house is NOT in structures[] - it keeps its own loader block (graybox, north boundary,",
"bed shading) - so collateralKey names which collateral string this price answers, per E's",
"chain: fascia anchors carry collateral 'gutter', this prices it, the wreck swaps on it.",
"Canonical number lives HERE (sites are data); the GLB's baked extras are E's proposal and",
"the fallback. Rule a different number -> change the factory too, or two sources tell two lies."
],
"x": 0, "z": -10.5,
"anchors": [
{ "id": "h1", "node": "fascia_anchor_01", "type": "house", "work": "bracket" },

View File

@ -488,14 +488,22 @@ export function createHud(d) {
const dist = _p.distanceTo(_cam);
b.holder.scale.setScalar(Math.max(0.75, Math.min(3.2, dist / 7)));
const frac = clamp01((c.load || 0) / c.hw.rating);
// SPRINT12 — B's flag, actioned: the bar reads the EFFECTIVE rating,
// rating × ratingHint, because that's the number sail.js fails on now.
// Off the bare rating a fascia corner showed 35% margin it didn't have
// and blew mid-green — the HUD's one job at that moment is the "…the
// shackle, I KNEW about the shackle" beat, and a bar that lies about
// the margin steals it. The label prints the same effective number:
// load-vs-what-it-breaks-at, one truth, both places.
const effRating = c.hw.rating * (c.anchor?.ratingHint ?? 1);
const frac = clamp01((c.load || 0) / effRating);
b.fill.scale.x = Math.max(0.001, (c.broken ? 1 : frac) * BAR_W);
b.fill.material.color.setHex(barColor(frac, c.broken));
if (redraw) {
const text = c.broken
? `${c.anchorId.toUpperCase()} BLOWN`
: `${c.anchorId.toUpperCase()} ${((c.load || 0) / 1000).toFixed(1)}/${(c.hw.rating / 1000).toFixed(1)}kN`;
: `${c.anchorId.toUpperCase()} ${((c.load || 0) / 1000).toFixed(1)}/${(effRating / 1000).toFixed(1)}kN`;
if (text !== b.lastText) {
b.lastText = text;
drawLabel(b.label, text, c.broken ? '#ff8f86' : frac > 0.85 ? '#ffb1ab' : '#dde5ea');

View File

@ -162,8 +162,15 @@ function createGarden(world) {
* @returns {{verdict: string, mode: string}}
*/
export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped }) {
// SPRINT12 — B's flag, actioned: "weakest link that went first" reduces on
// the EFFECTIVE rating (rating × ratingHint), matching sail.js's failure
// line and B's summary.weakest tie-break fix. On the bare rating a rated
// shackle on the carport beam (6.5 × 0.22 = 1.43 kN effective) out-ranks a
// carabiner on honest steel and the verdict names the wrong culprit — this
// sentence exists to teach which steel lied.
const eff = (c) => c.hw.rating * (c.anchor?.ratingHint ?? 1);
const worst = lost.length
? lost.reduce((a, c) => (a && a.hw.rating <= c.hw.rating ? a : c))
? lost.reduce((a, c) => (a && eff(a) <= eff(c) ? a : c))
: null;
const named = worst ? `${worst.hw.name} at ${worst.anchorId.toUpperCase()}` : '';
const hailKilled = dmg.hail > dmg.rain;

View File

@ -486,6 +486,27 @@ export default async function run(t) {
assertEq(site2World.collateralFor(undefined), null, 'and no key is not a bill');
});
t.test('SPRINT12 ruling: the gutter BILLS — 90, and the free failure is dead', () => {
// E priced it ($90, reasoning baked in the asset and adopted in the site
// JSON's _collateral) and named the seam: the house is not a structure, so
// collateralFor/wreckStructure had to be taught the house entry. This is
// the assert E left me to flip: backyard_01's house was the last free
// failure in the game.
const priced = world.collateralFor('gutter');
assert(priced, 'the gutter has a price at all — the null era ends here');
assertEq(priced.cost, 90, "A's ruling: 90 — wipes the trap's night (kit + fee), not the week");
assertEq(priced.label, 'the gutter', 'and reads as English on the invoice');
// The band that makes the number legible: ornament < gutter < structure.
assert(site.gnome.collateralValue < priced.cost && priced.cost < 180,
'gnome 25 < gutter 90 < carport 180');
// The key prices THIS site's house, not a global constant: the corner
// block has no house, so its 'gutter' stays null (pinned above).
// And nothing is wrecked before anyone breaks anything. (The wreck GLB
// arrives from lane/e at integration; absent, the swap no-ops false and
// the bill still lands — same offline contract as the carport.)
assertEq(world.isWrecked('gutter'), false, 'the gutter starts on the house');
});
// --- the wind router -----------------------------------------------------
// Loaded HERE, not inside t.test(). These two were written `async` and
// Suite.test() cannot await — so they were recorded as passes while asserting

View File

@ -443,6 +443,23 @@ export function createWorld(scene, opts = {}) {
// rating_hint / collateral that make it a trap. The site's own asserts (E's
// e.test) pin those ratings below the fascia's — do not "fix" them upward.
const structures = new Map();
/**
* SPRINT12 the house's collateral seam, taught rather than restructured.
* E named the trap precisely: collateralFor()/wreckStructure() iterate
* `structures`, and the house is NOT a structure so the gutter stayed
* unpriced and unswappable. The alternative was moving the house into
* structures[] (it fits the shape), but the house is the one building with
* extra jobs graybox stand-in, north boundary, the thing validateSite and
* two other lanes' code read at `site.house` and re-homing it mid-sprint
* to avoid two small branches is churn, not design. If a third house-like
* thing ever appears, THEN generalise. `collateralKey` (E's field, baked in
* the GLB, canonical in site JSON) names which collateral string this
* price answers the carport never needed it because there structure id
* and collateral string are the same word.
*/
const houseEntry = { spec: HOUSE, glb: null, wreck: null };
const houseKey = () =>
HOUSE ? (HOUSE.collateralKey ?? houseEntry.glb?.userData?.collateral_key ?? null) : null;
for (const st of site.structures ?? []) {
const marker = new THREE.Group();
marker.name = st.id;
@ -675,6 +692,22 @@ export function createWorld(scene, opts = {}) {
solids.push(houseGlb);
houseGlb.updateWorldMatrix(true, true);
for (const a of HOUSE.anchors ?? []) adoptAnchor(houseGlb, a.node, a.id);
houseEntry.glb = houseGlb;
// SPRINT12 — the torn-gutter wreck, the carport pattern on the one
// building that is not a structure. Loaded now and parked invisible for
// the same reason the carport's is: the swap happens as the storm ends,
// and an async load there drops the payoff frames after the card.
if (HOUSE.wreckedModel) {
const wreck = await load(HOUSE.wreckedModel);
if (wreck) {
wreck.name = 'house_yardside_wrecked';
wreck.position.copy(houseGlb.position);
wreck.visible = false;
root.add(wreck);
houseEntry.wreck = wreck;
}
}
}
// --- trees -----------------------------------------------------------
@ -828,6 +861,16 @@ export function createWorld(scene, opts = {}) {
if (!Number.isFinite(cost)) return null;
return { cost, label: spec.collateralLabel ?? glb?.userData?.collateral_label ?? spec.id };
}
// SPRINT12 — the gutter (A's ruling: $90, E's proposal adopted; the
// reasoning is in backyard_01.json beside the number). Same contract as
// structures: site JSON canonical, GLB extras fallback, null when
// unpriced — a site whose house declares no collateralKey still reads
// "not scored", never "free".
if (HOUSE && key === houseKey()) {
const cost = HOUSE.collateralValue ?? houseEntry.glb?.userData?.collateral_value ?? null;
if (!Number.isFinite(cost)) return null;
return { cost, label: HOUSE.collateralLabel ?? houseEntry.glb?.userData?.collateral_label ?? key };
}
return null;
},
@ -841,14 +884,30 @@ export function createWorld(scene, opts = {}) {
*/
wreckStructure(id) {
const entry = structures.get(id);
if (!entry?.wreck || !entry.glb) return false;
entry.glb.visible = false;
entry.wreck.visible = true;
return true;
if (entry?.wreck && entry.glb) {
entry.glb.visible = false;
entry.wreck.visible = true;
return true;
}
// The house, keyed by its collateral string ('gutter'): the gutter
// unzips, the house stands. E's window_glow warning honoured — the wreck
// ships its glow hidden_by_default under the same name, so the swap
// copies the intact house's current glow state or the client's lights
// would go out with the gutter if this ever lands mid-night.
if (HOUSE && id === houseKey() && houseEntry.wreck && houseEntry.glb) {
const glowWas = houseEntry.glb.getObjectByName('window_glow')?.visible ?? false;
const glow = houseEntry.wreck.getObjectByName('window_glow');
if (glow) glow.visible = glowWas;
houseEntry.glb.visible = false;
houseEntry.wreck.visible = true;
return true;
}
return false;
},
/** Is this structure standing? Lane D asked for a poke-able truth. */
isWrecked(id) {
if (HOUSE && id === houseKey()) return houseEntry.wreck?.visible === true;
return structures.get(id)?.wreck?.visible === true;
},