diff --git a/THREADS.md b/THREADS.md index 17a72eb..4433d4b 100644 --- a/THREADS.md +++ b/THREADS.md @@ -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. diff --git a/web/world/data/sites/backyard_01.json b/web/world/data/sites/backyard_01.json index a13a393..60b8e41 100644 --- a/web/world/data/sites/backyard_01.json +++ b/web/world/data/sites/backyard_01.json @@ -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" }, diff --git a/web/world/js/hud.js b/web/world/js/hud.js index 4e1f2de..fe4f340 100644 --- a/web/world/js/hud.js +++ b/web/world/js/hud.js @@ -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'); diff --git a/web/world/js/main.js b/web/world/js/main.js index b2052a3..2b801ad 100644 --- a/web/world/js/main.js +++ b/web/world/js/main.js @@ -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; diff --git a/web/world/js/tests/a.test.js b/web/world/js/tests/a.test.js index aa55099..5f639b1 100644 --- a/web/world/js/tests/a.test.js +++ b/web/world/js/tests/a.test.js @@ -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 diff --git a/web/world/js/world.js b/web/world/js/world.js index b8a98bf..688e48d 100644 --- a/web/world/js/world.js +++ b/web/world/js/world.js @@ -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; },