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:
parent
807de80969
commit
38b6fe9c87
43
THREADS.md
43
THREADS.md
@ -4038,3 +4038,46 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
|||||||
notice).
|
notice).
|
||||||
|
|
||||||
selftest **318/0/0** (317 + the placement pin). Landing pushed.
|
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.
|
||||||
|
|||||||
@ -22,6 +22,24 @@
|
|||||||
|
|
||||||
"house": {
|
"house": {
|
||||||
"model": "house_yardside_v1",
|
"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,
|
"x": 0, "z": -10.5,
|
||||||
"anchors": [
|
"anchors": [
|
||||||
{ "id": "h1", "node": "fascia_anchor_01", "type": "house", "work": "bracket" },
|
{ "id": "h1", "node": "fascia_anchor_01", "type": "house", "work": "bracket" },
|
||||||
|
|||||||
@ -488,14 +488,22 @@ export function createHud(d) {
|
|||||||
const dist = _p.distanceTo(_cam);
|
const dist = _p.distanceTo(_cam);
|
||||||
b.holder.scale.setScalar(Math.max(0.75, Math.min(3.2, dist / 7)));
|
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.scale.x = Math.max(0.001, (c.broken ? 1 : frac) * BAR_W);
|
||||||
b.fill.material.color.setHex(barColor(frac, c.broken));
|
b.fill.material.color.setHex(barColor(frac, c.broken));
|
||||||
|
|
||||||
if (redraw) {
|
if (redraw) {
|
||||||
const text = c.broken
|
const text = c.broken
|
||||||
? `${c.anchorId.toUpperCase()} BLOWN`
|
? `${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) {
|
if (text !== b.lastText) {
|
||||||
b.lastText = text;
|
b.lastText = text;
|
||||||
drawLabel(b.label, text, c.broken ? '#ff8f86' : frac > 0.85 ? '#ffb1ab' : '#dde5ea');
|
drawLabel(b.label, text, c.broken ? '#ff8f86' : frac > 0.85 ? '#ffb1ab' : '#dde5ea');
|
||||||
|
|||||||
@ -162,8 +162,15 @@ function createGarden(world) {
|
|||||||
* @returns {{verdict: string, mode: string}}
|
* @returns {{verdict: string, mode: string}}
|
||||||
*/
|
*/
|
||||||
export function verdictFor({ hp, lost, win, dmg, pondPeak, pondDumped }) {
|
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
|
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;
|
: null;
|
||||||
const named = worst ? `${worst.hw.name} at ${worst.anchorId.toUpperCase()}` : '';
|
const named = worst ? `${worst.hw.name} at ${worst.anchorId.toUpperCase()}` : '';
|
||||||
const hailKilled = dmg.hail > dmg.rain;
|
const hailKilled = dmg.hail > dmg.rain;
|
||||||
|
|||||||
@ -486,6 +486,27 @@ export default async function run(t) {
|
|||||||
assertEq(site2World.collateralFor(undefined), null, 'and no key is not a bill');
|
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 -----------------------------------------------------
|
// --- the wind router -----------------------------------------------------
|
||||||
// Loaded HERE, not inside t.test(). These two were written `async` and
|
// Loaded HERE, not inside t.test(). These two were written `async` and
|
||||||
// Suite.test() cannot await — so they were recorded as passes while asserting
|
// Suite.test() cannot await — so they were recorded as passes while asserting
|
||||||
|
|||||||
@ -443,6 +443,23 @@ export function createWorld(scene, opts = {}) {
|
|||||||
// rating_hint / collateral that make it a trap. The site's own asserts (E's
|
// 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.
|
// e.test) pin those ratings below the fascia's — do not "fix" them upward.
|
||||||
const structures = new Map();
|
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 ?? []) {
|
for (const st of site.structures ?? []) {
|
||||||
const marker = new THREE.Group();
|
const marker = new THREE.Group();
|
||||||
marker.name = st.id;
|
marker.name = st.id;
|
||||||
@ -675,6 +692,22 @@ export function createWorld(scene, opts = {}) {
|
|||||||
solids.push(houseGlb);
|
solids.push(houseGlb);
|
||||||
houseGlb.updateWorldMatrix(true, true);
|
houseGlb.updateWorldMatrix(true, true);
|
||||||
for (const a of HOUSE.anchors ?? []) adoptAnchor(houseGlb, a.node, a.id);
|
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 -----------------------------------------------------------
|
// --- trees -----------------------------------------------------------
|
||||||
@ -828,6 +861,16 @@ export function createWorld(scene, opts = {}) {
|
|||||||
if (!Number.isFinite(cost)) return null;
|
if (!Number.isFinite(cost)) return null;
|
||||||
return { cost, label: spec.collateralLabel ?? glb?.userData?.collateral_label ?? spec.id };
|
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;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -841,14 +884,30 @@ export function createWorld(scene, opts = {}) {
|
|||||||
*/
|
*/
|
||||||
wreckStructure(id) {
|
wreckStructure(id) {
|
||||||
const entry = structures.get(id);
|
const entry = structures.get(id);
|
||||||
if (!entry?.wreck || !entry.glb) return false;
|
if (entry?.wreck && entry.glb) {
|
||||||
entry.glb.visible = false;
|
entry.glb.visible = false;
|
||||||
entry.wreck.visible = true;
|
entry.wreck.visible = true;
|
||||||
return 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. */
|
/** Is this structure standing? Lane D asked for a poke-able truth. */
|
||||||
isWrecked(id) {
|
isWrecked(id) {
|
||||||
|
if (HOUSE && id === houseKey()) return houseEntry.wreck?.visible === true;
|
||||||
return structures.get(id)?.wreck?.visible === true;
|
return structures.get(id)?.wreck?.visible === true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user