diff --git a/THREADS.md b/THREADS.md index 99e71a3..1e89af1 100644 --- a/THREADS.md +++ b/THREADS.md @@ -2987,3 +2987,46 @@ Selftest: **277 passed, 0 failed, 0 skipped**, LANE BAL now visible. open on carports until landed), C's venturi (landed, waiting on site JSON shape), E's carport (landed as a data-driven trap, waiting on site_02), B's site_audit tool (from the quad sweep). Gate 3 (John plays the week) remains open โ€” the game has never been more ready for it. + +[E] 2026-07-18 โ€” ๐Ÿ’ฐ **LANE A โ€” the carport trap was UNSCOREABLE and that's on me. Fixed.** I shipped the + anchors last sprint carrying `collateral="carport"`, but the root carried no `collateral_value` โ€” so + the thing your aftermath reaches for (`world.gnome.collateralValue`, main.js:548) simply wasn't there + for the carport. A trap with no price isn't a trap, it's a warning label. It now carries + `collateral_value` + `collateral_label` on the root, same shape and same place as the gnome, and + e.test pins it above the gnome's 25. + **The number is YOURS โ€” this is a design call, not an art one.** My proposal is **180**, and the + reasoning is in the code beside it so you can argue with it: the gnome is 25, a night's shop is 80, a + good week banks ~475. At 180 it's 2.25 nights' budget โ€” it turns a good week into a broke one and is + felt for the rest of the run, without instantly ending a strong one. Below ~120 the trap is a shrug; + above ~250 tying off the carport once is a silent game over, which teaches nothing because the player + never gets to act on the lesson. It should be the worst bill on the site and still a week you can dig + out of. Change the constant, not the asset. + +[E] 2026-07-18 โ€” **`carport_01_wrecked_v1.glb`** โ€” the payoff for the trap, same origin and footprint as + `carport_01` so you swap mesh-for-mesh like the gnome and the fence. 6.28 ร— 5.57 ร— **2.20 m** (the + intact one is 2.69 โ€” a wreck must stand SHORTER than the thing it was, and there's an assert for that + now because my first pass came out taller). It fails the way a light structure actually does: the pads + stay put โ€” they were never the weak part, which is the joke โ€” the frame racks over like a + parallelogram, and the roof sheet peels off downwind in one piece. A carport doesn't shatter; it leans, + and then it's somewhere else. Copy for the line you asked about: + ยท collateral row: **"the carport โ€” $180"** + ยท verdict/ticker: **"you took the carport with you"** + ยท if you want the knife in: *"They'll want that back up by Thursday."* + +[E] 2026-07-18 โ€” ๐Ÿž two measured bugs on the way in, both caught by measuring instead of reasoning โ€” the + exact lesson from my Sprint 9 phantom-post entry, applied earlier this time and it cost minutes rather + than an afternoon: (1) I rotated the wreck's beams 31ยฐ about Y, but a beam spans the short axis and the + frame racks in that SAME axis โ€” both post tops travel together, so the beam rides across level. Tilting + it swung one end to 2.96 m and made the wreck taller than the carport. (2) The roof sheets dipped to + z = โˆ’0.41, i.e. through the ground, which is what inflated the measured height. Both are now comments + where the numbers are. + +[E] 2026-07-18 โ€” status: selftest **291/0/0**, Lane E is 64 asserts, **40/40 output files byte-identical** + across two full runs. ยงE dressing (fence lengths, bed size) is **blocked on gate 1** โ€” `data/sites/` + doesn't exist yet, and that's correct, it's yours from day one. **Ping me the moment + `site_02_corner_block.json` lands** and I'll fit the dressing to the real numbers and refresh the + sheets same session; the factory is site-agnostic and the carport was the only new geometry it needed. + **Lane B โ€” audit the carport before it ships.** If the one tree plus ground anchors can't make an + in-band quad at $80, say so and **I'll move the tree, not the ratings** โ€” the ratings are the site. + Carried, still yours either way: grass_atlas (**nine sprints, 0 refs** โ€” bin it), the screenshot POST, + `moon.png`. diff --git a/tools/blender/asset_report.json b/tools/blender/asset_report.json index e8178c2..3dc7157 100644 --- a/tools/blender/asset_report.json +++ b/tools/blender/asset_report.json @@ -155,6 +155,24 @@ "status": "PASS", "problems": [] }, + { + "name": "carport_01_wrecked", + "dims": [ + 6.2819, + 5.5704, + 2.1983 + ], + "tris": 144, + "nodes": [ + "beams", + "carport_01_wrecked", + "footings", + "posts", + "roof_down" + ], + "status": "PASS", + "problems": [] + }, { "name": "shed_01", "dims": [ diff --git a/tools/blender/build_yard_assets.py b/tools/blender/build_yard_assets.py index 0f2c1ef..3a211af 100644 --- a/tools/blender/build_yard_assets.py +++ b/tools/blender/build_yard_assets.py @@ -781,6 +781,21 @@ def build_house_yardside(name): return root +# What it costs to take the client's carport with you. Lane A owns this number โ€” +# it's a design call, not an art one โ€” but the trap needs A number or it isn't a +# trap, and shipping the asset without one is why nothing scored it. My proposal +# and the reasoning, to argue with rather than adopt: +# ยท the gnome is 25, a night's shop budget is 80, a good week banks ~475; +# ยท at 180 it's 2.25 nights' budget โ€” it turns a good week into a broke one and +# is felt for the rest of the run, without instantly ending a strong one; +# ยท cheaper than ~120 and the trap is a shrug; dearer than ~250 and tying off +# the carport once is a silent game over, which teaches nothing because the +# player never gets to act on the lesson. +# The point is that it should be the worst thing on the site's bill and still be +# a week you can dig out of. +CARPORT_COLLATERAL = 180 + + def build_carport_01(name): """A single-car carport โ€” the corner block's whole personality (SPRINT9 ยงE). @@ -858,6 +873,87 @@ def build_carport_01(name): stamp(root, name, "structure") root["site_hint"] = "corner_block" root["is_anchor_trap"] = True + # Without this the trap is unscoreable: the anchors say collateral="carport" + # but nothing said what a carport COSTS, so Lane A's aftermath had no number + # to reach for. main.js already reads world.gnome.collateralValue โ€” same + # shape, same place. + root["collateral_value"] = CARPORT_COLLATERAL + root["collateral_label"] = "the carport" + return root + + +def build_carport_01_wrecked(name): + """The carport after you tied a sail to it (SPRINT10 ยงE). + + Same origin and footprint as `carport_01`, so Lane A swaps mesh-for-mesh the + way the gnome and fence already do. This is the payoff for the trap: the + site's lesson only lands if taking the beam LOOKS like taking the carport, + not like a number going down on a card. + + It fails the way a light structure actually fails โ€” not flattened. The + windward pair of posts stays in its pads and the leeward pair folds, so the + whole frame racks over like a parallelogram and the roof sheet peels off + downwind in one piece. That's the tell: a carport doesn't shatter, it leans + and then it's somewhere else. + """ + rng = rng_for(name) + root = add_empty(name) + steel = get_material("Mat_Steel", PAL["steel_gal"], 0.4, metallic=0.85) + dark = get_material("Mat_SteelDark", PAL["steel_dark"], 0.45, metallic=0.8) + sheet = get_material("Mat_Colorbond", PAL["colorbond"], 0.45, metallic=0.5) + conc = get_material("Mat_Concrete", PAL["concrete"], 0.95) + + W, D = 3.0, 5.4 + H_HI, H_LO = 2.45, 2.20 + RACK = math.radians(34) # how far the frame leaned before it stopped + + pads, posts = [], [] + for sx in (-1, 1): + for sy in (-1, 1): + x, y = sx * (W / 2 - 0.09), sy * (D / 2 - 0.09) + h = H_HI if sy < 0 else H_LO + # The pads stay: they were never the weak part. That's the joke. + pads.append(add_box(f"{name}_pad_{sx}_{sy}", (0.26, 0.26, 0.09), + (x, y, 0.045), conc)) + lean = RACK * rng.uniform(0.86, 1.0) + # Racked over toward +X, hinging at the pad โ€” so the top travels and + # the base doesn't. + posts.append(add_box(f"{name}_post_{sx}_{sy}", (0.09, 0.09, h), + (x + math.sin(lean) * h / 2, y, + 0.09 + math.cos(lean) * h / 2), steel, + rot=(0, lean, 0))) + join_group(pads, "footings", root) + join_group(posts, "posts", root) + + # The beams stay LEVEL. A beam spans the short axis and the frame racks in + # that same axis, so both post tops travel together and the beam rides across + # โ€” it does not tilt. Rotating it swung one end to 2.96 m, i.e. the wreck came + # out TALLER than the carport it used to be. Measured, not reasoned. + beams = [] + for sy in (-1, 1): + h = H_HI if sy < 0 else H_LO + beams.append(add_box(f"{name}_beam_{sy}", (W, 0.05, 0.14), + (math.sin(RACK) * h, sy * (D / 2 - 0.09), + 0.09 + math.cos(RACK) * h), dark)) + join_group(beams, "beams", root) + + # The sheet let go and went downwind โ€” folded, not flat, and clear of the + # frame. Nobody's parking under this again. + # Flat-ish and ON the grass. The first pass tilted them enough to sink a + # corner to z=-0.41 โ€” through the ground โ€” which is both wrong and what + # inflated the wreck's measured height. Kept nearer the frame too: a 7 m + # footprint would overlap whatever Lane A has parked next door. + join_group([add_box(f"{name}_sheet_a", (W + 0.2, D * 0.55, 0.04), + (W * 0.95, -D * 0.18, 0.12), sheet, + rot=(0, math.radians(3), math.radians(-13))), + add_box(f"{name}_sheet_b", (W * 0.8, D * 0.38, 0.04), + (W * 0.88, D * 0.30, 0.28), sheet, + rot=(math.radians(9), math.radians(-4), math.radians(8)))], + "roof_down", root) + + stamp(root, name, "structure") + root["broken_variant_of"] = "carport_01" + root["collateral_value"] = CARPORT_COLLATERAL return root @@ -2054,6 +2150,11 @@ ASSETS = [ nodes=["footings", "posts", "beams", "roof", "beam_anchor_01", "beam_anchor_02", "post_anchor_01", "post_anchor_02"]), + # Wider than the intact carport (the sheet goes downwind) but SHORTER โ€” a + # wreck that stands taller than the thing it was is a bug, not a wreck. + dict(name="carport_01_wrecked", fn=build_carport_01_wrecked, + dims=((4.5, 6.6), (5.2, 5.8), (1.9, 2.45)), + nodes=["footings", "posts", "beams", "roof_down"]), dict(name="shed_01", fn=build_shed_01, dims=((2.4, 2.7), (1.8, 2.1), (1.95, 2.25)), nodes=["shell", "roof", "doors", "door_anchor"]), diff --git a/tools/blender/contact_sheet.png b/tools/blender/contact_sheet.png index 2e517f8..79d6b91 100644 Binary files a/tools/blender/contact_sheet.png and b/tools/blender/contact_sheet.png differ diff --git a/web/world/js/tests/e.test.js b/web/world/js/tests/e.test.js index f34753c..3f6b05b 100644 --- a/web/world/js/tests/e.test.js +++ b/web/world/js/tests/e.test.js @@ -83,6 +83,8 @@ const ASSETS = [ { name: 'carport_01', h: [2.4, 2.8], nodes: ['footings', 'posts', 'beams', 'roof', 'beam_anchor_01', 'beam_anchor_02', 'post_anchor_01', 'post_anchor_02'] }, + { name: 'carport_01_wrecked', h: [1.9, 2.45], + nodes: ['footings', 'posts', 'beams', 'roof_down'] }, ]; function sizeOf(gltf) { @@ -153,6 +155,36 @@ export default async function run(t) { // house fascia, which DESIGN.md already calls a lie. If someone quietly // "fixes" these ratings upward the site stops teaching anything and just // becomes a smaller yard, so they're pinned here with the reason attached. + // The trap has to be scoreable or it isn't a trap, only a warning. The anchors + // said collateral="carport" from Sprint 9 but nothing said what a carport + // COSTS, so Lane A's aftermath had no number to reach for and the site's whole + // lesson was unpriced. main.js already reads world.gnome.collateralValue โ€” this + // is the same shape in the same place. + t.test('the carport is priced, and it is the worst bill on the site', () => { + const c = loaded.get('carport_01')?.scene.getObjectByName('carport_01'); + assert(c, 'carport_01 root missing'); + const cost = c.userData?.collateral_value; + assert(typeof cost === 'number', `carport carries no collateral_value (${JSON.stringify(c.userData)})`); + const gnome = loaded.get('garden_gnome_01')?.scene + .getObjectByName('garden_gnome_01')?.userData?.collateral_value; + assert(cost > gnome, `carport (${cost}) must cost more than the gnome (${gnome})`); + // Wreckage carries the same price, so scoring can read either state. + const w = loaded.get('carport_01_wrecked')?.scene.getObjectByName('carport_01_wrecked'); + assert(w?.userData?.collateral_value === cost, + 'the wrecked carport must be priced the same as the one it used to be'); + assert(w?.userData?.broken_variant_of === 'carport_01', + 'wrecked carport must name its intact twin so A can pair the swap'); + }); + + // A wreck that stands taller than the thing it was is a bug, not a wreck. + t.test('the wrecked carport is shorter than the carport', () => { + const a = new THREE.Box3().setFromObject(loaded.get('carport_01').scene); + const b = new THREE.Box3().setFromObject(loaded.get('carport_01_wrecked').scene); + const hi = a.max.y - a.min.y, lo = b.max.y - b.min.y; + assert(lo < hi, `wreck stands ${lo.toFixed(2)} m vs the intact ${hi.toFixed(2)} m`); + assert(Math.abs(b.min.y) < 0.05, `wreck sits at y=${b.min.y.toFixed(2)} โ€” roof sheet through the ground?`); + }); + t.test('carport anchors stay a trap โ€” the corner block has no good tie-off', () => { const g = loaded.get('carport_01'); assert(g, 'carport_01 did not load'); diff --git a/web/world/models/carport_01_v1.glb b/web/world/models/carport_01_v1.glb index 0d8cd41..ea8fe43 100644 Binary files a/web/world/models/carport_01_v1.glb and b/web/world/models/carport_01_v1.glb differ diff --git a/web/world/models/carport_01_wrecked_v1.glb b/web/world/models/carport_01_wrecked_v1.glb new file mode 100644 index 0000000..653fc62 Binary files /dev/null and b/web/world/models/carport_01_wrecked_v1.glb differ