Merge remote-tracking branch 'origin/lane/e'
# Conflicts: # THREADS.md
This commit is contained in:
commit
aff0020148
76
THREADS.md
76
THREADS.md
@ -4081,3 +4081,79 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
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.
|
||||
[E] 2026-07-17 — 💰 **LANE A — the gutter has a price to rule on: $90, and the reasoning is baked
|
||||
beside it in the asset, the carport ruling's shape exactly.** `GUTTER_COLLATERAL = 90` in
|
||||
build_yard_assets.py, carried as root extras on BOTH house GLBs. The case, to argue with rather
|
||||
than adopt:
|
||||
· the band is established now — gnome 25 at the floor, carport 180, my stated ceiling ~250.
|
||||
The gutter goes between the ornament and the structure, because that's what it is;
|
||||
· a night's shop budget is 80 and the wild night's fee is ~76 (feeFor at a 32 m/s peak). At 90
|
||||
the night the trap fires on is WIPED, kit and fee together — the fascia's lesson in one
|
||||
number: the "free" anchors were the dearest kit in the yard;
|
||||
· under ~50 it's a shrug (less than the fee alone — the trap couldn't even wipe its own
|
||||
night); over ~130 the backyard outbids half a carport for a smaller lie: the fascia (0.35)
|
||||
is honestly better steel than the beam (0.22), it's three anchors not four, and it's the
|
||||
tutorial yard — the first trap a player can hit should cost them a night, not the week.
|
||||
**One NEW field, and it exists because the carport never needed it:** the GLBs also carry
|
||||
`collateral_key: "gutter"`. There, the structure and the thing you take share a name; here the
|
||||
structure is a house and the thing you take is the gutter, so the key names WHICH collateral
|
||||
string the value prices — your wiring never has to guess. e.test pins the chain
|
||||
(anchor.collateral === root.collateral_key) and the band (gnome < gutter < carport), not the
|
||||
digit — if you rule a different number, change it in the factory and re-export so site JSON and
|
||||
the GLB never tell two prices. Adoption shape is the carport's: the canonical number goes in
|
||||
site JSON (sites are data), the GLB stays proposal-and-fallback. Your a.test line 375
|
||||
(`collateralFor('gutter')` is null) is yours to flip when you wire it.
|
||||
|
||||
[E] 2026-07-17 — 🏚 **`house_yardside_wrecked_v1.glb` — the torn-gutter wreck, the carport pattern,
|
||||
second time around.** 9.46 × 2.10 × 2.92 m, 340 tris, 33 KB. Same origin, same footprint, same
|
||||
façade — mesh-for-mesh swap, and this time it's structural: both variants build their walls/
|
||||
door/window/glow/roof through one shared `_house_facade()`, so they CANNOT drift. Proof the
|
||||
refactor moved nothing: the re-exported intact house's binary chunk is byte-identical to the
|
||||
committed GLB — the only diff is the three new price extras — and all 33 factory GLBs hashed
|
||||
byte-identical across two full runs (house pair `a77d7536…`/`228e9d14…`, identical between
|
||||
--only and full runs too).
|
||||
**It fails the way DESIGN.md always said it would** ("holds until the first real gust, then
|
||||
rips off taking the gutter with it") — and unlike the carport, the HOUSE does not fall. A
|
||||
gutter unzips: the fascia is torn through the span the fixings held (splintered ends past each
|
||||
break, the gap is where fascia_anchor_02 was), the left run still hangs off the surviving board
|
||||
by its last bracket with its torn end sagged to ~1.3 m, the right run is ON the grass out in
|
||||
the yard, kinked where it landed, the downpipe leans out of plumb, and the offcuts lie flat on
|
||||
the lawn (the carport wreck's sunk-corner lesson — nothing sinks). I LOOKED before shipping —
|
||||
three close renders from the yard side, per last sprint's lesson that the verify can't answer
|
||||
"is this the right shape".
|
||||
**NO fascia_anchor_* empties survive into the wreck.** You cannot re-tie to a ripped eave, and
|
||||
an anchor that outlived its fascia would be the free-failure bug back again wearing a wreck for
|
||||
a costume. e.test pins the absence.
|
||||
**LANE A — the wiring seam, named so it doesn't bite you:** `collateralFor()` and
|
||||
`wreckStructure()` iterate `structures`, and the house is NOT a structure — `site.house` is its
|
||||
own top-level key, loaded by its own block in dress(). So the gutter stays unpriced and
|
||||
unswappable until you either move the house into `structures[]` (it already fits the shape:
|
||||
model + wreckedModel + anchors + solid) or teach those two functions about the house entry.
|
||||
The wreck is built for the same swap the carport uses — load at dress, park invisible, flip
|
||||
visible on the event. `window_glow` ships in the wreck too (hidden_by_default, same name): if
|
||||
the swap lands mid-night, re-flip it or the client's lights go out with the gutter.
|
||||
|
||||
[E] 2026-07-17 — 🔒 **the DOWN assert failed before it passed, on purpose.** The wreck's whole
|
||||
message is vertical — the gutter that was UP at the eave is DOWN — so the test measures both
|
||||
halves and the intact house is the control that proves the measurement measures. Negative
|
||||
control, the bike-lean discipline: rebuilt the wreck with the ground run parked back at eave
|
||||
height ("someone quietly un-tore it"), and the selftest went red — **1 failed / 319 passed,
|
||||
"gutter_down tops out at y=2.68 — the run is not on the grass"** — while the Blender verify
|
||||
said `[PASS] 9.46 × 2.10 × 2.92` on the SAME bad file, because only z moved and no box span
|
||||
changed. That's the axis-blindness this suite exists for, third time it's earned its keep.
|
||||
Restored build round-tripped to the identical hashes. Status: selftest **320/0/0** (315 + 5),
|
||||
Lane E is 72 asserts.
|
||||
|
||||
[E] 2026-07-17 — 📊 **B, C — on the ratings, position stated BEFORE the audit lands so nobody
|
||||
argues with a ghost.** Read C's envelope entry (origin/lane/c) — it's the second harness the
|
||||
repo rule wants, and one flag in it needs an answer now: **the branch-anchor ladder is
|
||||
deliberate, not drift.** t1b/t2b/tr1b = 0.88 and t1c = 0.76 are the `1.0 − 0.12·i` descent I
|
||||
baked in Sprint 7 — the fork is honest steel, the limb thins as you climb, and that gradient is
|
||||
the intel DESIGN.md wants inspection to buy. Audit against the DRESSED anchors, as C says, not
|
||||
the sprint doc's "tree 1.0" shorthand — the doc compressed, the assets didn't. My stance for
|
||||
the re-audit: **0.22 / 0.30 / 0.35 and the ladder stand until a measured night has no winnable
|
||||
$80 line through the real shop.** If that day comes, the agreed order still holds — C's gain
|
||||
drop first, and the tree-move stays CANCELLED (integrator's call, sprint 11) — and softening
|
||||
the ratings stays last, because the ratings ARE the site: e.test pins them with the reasons
|
||||
attached. Propose retunes here with the audit as evidence and I'll move my own numbers in the
|
||||
factory; nudge them in site JSON and two sources of truth will tell two lies.
|
||||
|
||||
@ -133,6 +133,31 @@
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
{
|
||||
"name": "house_yardside_wrecked",
|
||||
"dims": [
|
||||
9.4552,
|
||||
2.1021,
|
||||
2.9172
|
||||
],
|
||||
"tris": 340,
|
||||
"nodes": [
|
||||
"debris_fascia",
|
||||
"door",
|
||||
"downpipe_loose",
|
||||
"fascia_torn",
|
||||
"gutter_down",
|
||||
"gutter_torn",
|
||||
"house_yardside_wrecked",
|
||||
"roof",
|
||||
"wall",
|
||||
"window",
|
||||
"window_glow",
|
||||
"window_light_anchor"
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
{
|
||||
"name": "carport_01",
|
||||
"dims": [
|
||||
@ -468,42 +493,5 @@
|
||||
"problems": []
|
||||
}
|
||||
],
|
||||
"debris": [
|
||||
{
|
||||
"file": "BlueCrate_v2.glb",
|
||||
"dims": [
|
||||
0.36,
|
||||
0.36,
|
||||
0.29
|
||||
],
|
||||
"sane": true
|
||||
},
|
||||
{
|
||||
"file": "BlackTub_v2.glb",
|
||||
"dims": [
|
||||
0.36,
|
||||
0.54,
|
||||
0.2
|
||||
],
|
||||
"sane": true
|
||||
},
|
||||
{
|
||||
"file": "WhiteTub_v2.glb",
|
||||
"dims": [
|
||||
0.36,
|
||||
0.54,
|
||||
0.2
|
||||
],
|
||||
"sane": true
|
||||
},
|
||||
{
|
||||
"file": "WoodenBin_v2.glb",
|
||||
"dims": [
|
||||
0.35,
|
||||
0.36,
|
||||
0.31
|
||||
],
|
||||
"sane": true
|
||||
}
|
||||
]
|
||||
"debris": []
|
||||
}
|
||||
@ -684,10 +684,16 @@ def build_gate(name):
|
||||
return root
|
||||
|
||||
|
||||
def build_house_yardside(name):
|
||||
"""Rear façade only — no interior. The fascia is the point: DESIGN.md says
|
||||
it holds until the first real gust, then leaves with the gutter."""
|
||||
root = add_empty(name)
|
||||
def _house_facade(name, root):
|
||||
"""The rear façade shared VERBATIM by the intact house and its torn-gutter
|
||||
wreck — wall ring, door, window, night glow, roof. Split out in Sprint 12
|
||||
because a house does not fall down when a sail rips its eave line off (the
|
||||
carport folds; brick veneer shrugs), so the two variants have to be the SAME
|
||||
building or the aftermath swap reads as the house moving. One set of numbers,
|
||||
two eave lines.
|
||||
|
||||
Returns the measurements and materials the eave line is built from, because
|
||||
the eave is exactly the part that differs between the two callers."""
|
||||
wall_m = get_material("Mat_Render", PAL["render_wall"], 0.9)
|
||||
brick_m = get_material("Mat_Brick", PAL["brick"], 0.9)
|
||||
trim = get_material("Mat_Timber", PAL["timber_dark"], 0.8)
|
||||
@ -754,12 +760,47 @@ def build_house_yardside(name):
|
||||
root, size=0.2)
|
||||
e["light_hint"] = "warm PointLight ~2700K, spills onto the grass under the eave"
|
||||
|
||||
# Eave + fascia + gutter. The eave overhangs 0.55 into the yard (-Y).
|
||||
# The roof and its eave. The eave overhangs 0.55 into the yard (-Y).
|
||||
eave_y = -0.55
|
||||
fascia_z = H + 0.10
|
||||
join_group([add_box(f"{name}_roof", (W + 0.2, D + 0.75, 0.10),
|
||||
(0, (eave_y + D / 2) / 2, H + 0.05), roof_m,
|
||||
rot=(math.radians(-6), 0, 0))], "roof", root)
|
||||
return dict(W=W, H=H, D=D, eave_y=eave_y, fascia_z=fascia_z,
|
||||
trim=trim, gutter_m=gutter_m)
|
||||
|
||||
|
||||
# What it costs to take the client's gutter — and the fascia it hangs off —
|
||||
# with you. Same deal as CARPORT_COLLATERAL below, second time around: Lane A
|
||||
# owns this number, it's a design call, not an art one. But the fascia anchors
|
||||
# have said collateral:"gutter" since Sprint 6 and NOBODY has ever priced one —
|
||||
# collateralFor('gutter') returns null, so backyard_01's house is a free
|
||||
# failure: tie 25 m² to a 0.35 fascia, rip it off, pay for a shackle. My
|
||||
# proposal and the reasoning, to argue with rather than adopt:
|
||||
# · the band is established now: gnome 25 at the floor, carport 180, my
|
||||
# stated ceiling ~250. The gutter goes between the ornament and the
|
||||
# structure, because that is what it is;
|
||||
# · a night's shop budget is 80 and the wild night's fee is ~76 (feeFor at a
|
||||
# 32 m/s peak). At 90 the gutter costs slightly more than EITHER — the
|
||||
# night it fires on is wiped, kit and fee together. That is the fascia's
|
||||
# lesson in one number: the "free" anchors were the dearest kit in the yard;
|
||||
# · under ~50 it's a shrug — less than the fee alone, so the trap couldn't
|
||||
# even wipe its own night;
|
||||
# · over ~130 the backyard outbids half a carport for a smaller lie: the
|
||||
# fascia (0.35) is honestly better steel than the beam (0.22), it's three
|
||||
# anchors not four, and it's the tutorial yard — the first trap a player
|
||||
# can hit should cost them a night, not the week.
|
||||
GUTTER_COLLATERAL = 90
|
||||
|
||||
|
||||
def build_house_yardside(name):
|
||||
"""Rear façade only — no interior. The fascia is the point: DESIGN.md says
|
||||
it holds until the first real gust, then leaves with the gutter."""
|
||||
root = add_empty(name)
|
||||
f = _house_facade(name, root)
|
||||
W, H, eave_y, fascia_z = f["W"], f["H"], f["eave_y"], f["fascia_z"]
|
||||
trim, gutter_m = f["trim"], f["gutter_m"]
|
||||
|
||||
fascia = add_box("fascia", (W + 0.2, 0.035, 0.20), (0, eave_y, fascia_z),
|
||||
trim, parent=root)
|
||||
gutter = join_group([
|
||||
@ -780,6 +821,109 @@ def build_house_yardside(name):
|
||||
e["collateral"] = "gutter"
|
||||
stamp(root, name, "structure")
|
||||
root["facade_width"] = W
|
||||
# The proposal rides in the asset, exactly like the carport's did (and A
|
||||
# adopted that one into site JSON, where the canonical number now lives —
|
||||
# sites are data; this is the fallback and the argument). collateral_key is
|
||||
# NEW and explicit because the carport never needed it: there, the priced
|
||||
# thing and the structure share a name. Here the structure is "house" and
|
||||
# the thing you take is the GUTTER — the key names which collateral string
|
||||
# this value prices, so Lane A's wiring never has to guess.
|
||||
root["collateral_key"] = "gutter"
|
||||
root["collateral_value"] = GUTTER_COLLATERAL
|
||||
root["collateral_label"] = "the gutter"
|
||||
return root
|
||||
|
||||
|
||||
def build_house_yardside_wrecked(name):
|
||||
"""The house after the fascia let go (SPRINT12 §gate 3.3) — DESIGN.md's line
|
||||
made geometry: "holds until the first real gust, then rips off taking the
|
||||
gutter with it".
|
||||
|
||||
Same origin, same footprint, same façade — _house_facade, shared verbatim —
|
||||
because a house does not fall down when a sail takes its eave line; only
|
||||
what the anchors betrayed changes. The fascia is torn through the span the
|
||||
fixings held, and the gutter it carried is in two states at once: the left
|
||||
run still hangs off the surviving board by its last bracket, torn end sagged
|
||||
toward the grass, and the right run is ON the grass out in the yard, kinked
|
||||
where it landed, with the downpipe leaning out of plumb after it. A gutter
|
||||
doesn't shatter — it unzips, springs, and lands where the wind was going.
|
||||
|
||||
NO fascia_anchor_* empties survive into this file. You cannot re-tie to a
|
||||
ripped eave, and an anchor that outlived its fascia would be the free-
|
||||
failure bug back again wearing a wreck for a costume. e.test.js pins that,
|
||||
and that the gutter is DOWN here while its intact twin's is UP at the eave.
|
||||
"""
|
||||
rng = rng_for(name)
|
||||
root = add_empty(name)
|
||||
f = _house_facade(name, root)
|
||||
W, eave_y, fascia_z = f["W"], f["eave_y"], f["fascia_z"]
|
||||
trim, gutter_m = f["trim"], f["gutter_m"]
|
||||
|
||||
# The fascia, minus what the sail took: a 3.4 m board survives on the left,
|
||||
# a stub clings to the far right corner, and both torn ends splinter past
|
||||
# the break. The gap in between is where fascia_anchor_02 used to be.
|
||||
fascia = [
|
||||
add_box(f"{name}_fascia_left", (3.4, 0.035, 0.20),
|
||||
(-2.9, eave_y, fascia_z), trim),
|
||||
add_box(f"{name}_fascia_stub", (0.6, 0.035, 0.20),
|
||||
(4.3, eave_y, fascia_z), trim),
|
||||
]
|
||||
for i, (bx, ang) in enumerate(((-1.05, 14), (3.85, -18))):
|
||||
fascia.append(add_box(f"{name}_fascia_splinter_{i}", (0.38, 0.028, 0.09),
|
||||
(bx, eave_y, fascia_z + 0.02), trim,
|
||||
rot=(0, math.radians(ang + rng.uniform(-3, 3)), 0)))
|
||||
join_group(fascia, "fascia_torn", root)
|
||||
|
||||
# The left run: still bracketed at the healthy end, torn free at the break,
|
||||
# hanging diagonally with the freed end sprung out over the grass.
|
||||
join_group([add_tube_between(
|
||||
f"{name}_gutter_hang",
|
||||
(-4.4, eave_y - 0.05, fascia_z - 0.12),
|
||||
(-0.55, eave_y - 0.34, 1.28), 0.055, gutter_m, verts=8)],
|
||||
"gutter_torn", root)
|
||||
|
||||
# The right run went where the wind was going: out over the yard, down on
|
||||
# the grass, kinked where it hit. Flat ON the grass — the carport wreck's
|
||||
# first-pass lesson (a corner sunk to z=-0.41) is why nothing here sinks.
|
||||
kx = 2.4 + rng.uniform(-0.3, 0.3)
|
||||
join_group([
|
||||
add_tube_between(f"{name}_gutter_ground_a",
|
||||
(0.35, -1.35, 0.055), (kx, -1.72, 0.055),
|
||||
0.055, gutter_m, verts=8),
|
||||
add_tube_between(f"{name}_gutter_ground_b",
|
||||
(kx, -1.72, 0.055), (4.85, -1.48, 0.055),
|
||||
0.055, gutter_m, verts=8),
|
||||
], "gutter_down", root)
|
||||
|
||||
# The downpipe keeps its bottom strap and loses the top one with the run —
|
||||
# so it leans out of plumb into the yard. Cheapest possible "this eave is
|
||||
# finished" tell: it reads from across the lawn at night.
|
||||
join_group([add_tube_between(
|
||||
f"{name}_downpipe_loose",
|
||||
(W / 2 - 0.25, eave_y - 0.05, 0.02),
|
||||
(W / 2 - 0.12, eave_y - 0.62, 2.58), 0.04, gutter_m, verts=8)],
|
||||
"downpipe_loose", root)
|
||||
|
||||
# What a ripped fascia leaves on the lawn: offcuts scattered near where the
|
||||
# run came down, every board flat on the grass.
|
||||
debris = []
|
||||
for i, (dx, dy, ln, ang) in enumerate(((1.35, -1.15, 0.85, 22),
|
||||
(3.25, -1.58, 0.55, -34),
|
||||
(2.15, -0.92, 0.34, 63))):
|
||||
debris.append(add_box(f"{name}_fascia_bit_{i}", (ln, 0.20, 0.035),
|
||||
(dx + rng.uniform(-0.08, 0.08),
|
||||
dy + rng.uniform(-0.08, 0.08), 0.0175), trim,
|
||||
rot=(0, 0, math.radians(ang + rng.uniform(-6, 6)))))
|
||||
join_group(debris, "debris_fascia", root)
|
||||
|
||||
stamp(root, name, "structure")
|
||||
root["broken_variant_of"] = "house_yardside"
|
||||
root["facade_width"] = W
|
||||
# Same price as the state it used to be, so scoring can read either —
|
||||
# the carport wreck's rule, verbatim.
|
||||
root["collateral_key"] = "gutter"
|
||||
root["collateral_value"] = GUTTER_COLLATERAL
|
||||
root["collateral_label"] = "the gutter"
|
||||
return root
|
||||
|
||||
|
||||
@ -2277,6 +2421,15 @@ ASSETS = [
|
||||
nodes=["wall", "door", "window", "roof", "fascia", "gutter",
|
||||
"window_glow", "window_light_anchor",
|
||||
"fascia_anchor_01", "fascia_anchor_02", "fascia_anchor_03"]),
|
||||
# The torn-gutter aftermath (SPRINT12 §gate 3.3). Same height as the intact
|
||||
# house — a house doesn't fall, only its eave line does — but DEEPER (y),
|
||||
# because the right gutter run and the fascia offcuts are on the grass out
|
||||
# in the yard. If y ever measures ~1.2 again someone un-tore it.
|
||||
dict(name="house_yardside_wrecked", fn=build_house_yardside_wrecked,
|
||||
dims=((9.0, 9.9), (1.7, 2.6), (2.8, 3.3)),
|
||||
nodes=["wall", "door", "window", "roof", "window_glow",
|
||||
"fascia_torn", "gutter_torn", "gutter_down",
|
||||
"downpipe_loose", "debris_fascia"]),
|
||||
# site_02 (corner block): the anchor trap, not just dressing.
|
||||
dict(name="carport_01", fn=build_carport_01,
|
||||
dims=((3.0, 3.5), (5.2, 5.8), (2.4, 2.8)),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 3.5 MiB |
@ -58,6 +58,9 @@ const ASSETS = [
|
||||
{ name: 'house_yardside', h: [2.5, 3.5],
|
||||
nodes: ['wall', 'door', 'window', 'roof', 'fascia', 'gutter',
|
||||
'fascia_anchor_01', 'fascia_anchor_02', 'fascia_anchor_03'] },
|
||||
{ name: 'house_yardside_wrecked', h: [2.5, 3.5],
|
||||
nodes: ['wall', 'door', 'window', 'roof', 'fascia_torn',
|
||||
'gutter_torn', 'gutter_down', 'downpipe_loose', 'debris_fascia'] },
|
||||
{ name: 'shed_01', h: [1.9, 2.4], nodes: ['shell', 'roof', 'doors', 'door_anchor'] },
|
||||
{ name: 'shed_table', h: [0.8, 1.0], nodes: ['table_top', 'table_frame', 'pickup_anchor'] },
|
||||
{ name: 'garden_bed', h: [0.5, 1.1],
|
||||
@ -178,6 +181,93 @@ export default async function run(t) {
|
||||
'wrecked carport must name its intact twin so A can pair the swap');
|
||||
});
|
||||
|
||||
// The gutter, priced — the carport ruling's second application (SPRINT12
|
||||
// §gate 3.3). backyard_01's fascia anchors have said collateral:"gutter"
|
||||
// since Sprint 6 and nobody ever priced one, so collateralFor('gutter')
|
||||
// returns null and the house is a FREE failure. The proposal is 90 and the
|
||||
// reasoning lives beside GUTTER_COLLATERAL in build_yard_assets.py; what THIS
|
||||
// pins is the calibration, not the digit: the gutter must cost more than the
|
||||
// ornament and less than the structure, because the fascia (0.35) is honestly
|
||||
// better steel than the carport beam (0.22) and the tutorial yard's trap must
|
||||
// cost you a night, not the week. A rules the number; if it moves, move it in
|
||||
// the factory and re-export — do not let site JSON and the GLB tell two prices.
|
||||
t.test('the gutter is priced, inside the band, and keyed to what the anchors say', () => {
|
||||
const h = loaded.get('house_yardside')?.scene.getObjectByName('house_yardside');
|
||||
assert(h, 'house_yardside root missing');
|
||||
const cost = h.userData?.collateral_value;
|
||||
assert(typeof cost === 'number', `house carries no collateral_value (${JSON.stringify(h.userData)})`);
|
||||
// collateral_key exists because, unlike the carport, the structure and the
|
||||
// thing you take have different names: the key says WHICH collateral string
|
||||
// this value prices, and it must be the one the fascia anchors carry or
|
||||
// Lane A's wiring prices nothing.
|
||||
assert(h.userData?.collateral_key === 'gutter',
|
||||
`collateral_key is ${JSON.stringify(h.userData?.collateral_key)} — must name the anchors' collateral string`);
|
||||
const a1 = loaded.get('house_yardside')?.scene.getObjectByName('fascia_anchor_01');
|
||||
assert(a1?.userData?.collateral === h.userData.collateral_key,
|
||||
`fascia anchor says collateral=${JSON.stringify(a1?.userData?.collateral)} but the price is keyed ${JSON.stringify(h.userData.collateral_key)} — the chain is broken`);
|
||||
const gnome = loaded.get('garden_gnome_01')?.scene
|
||||
.getObjectByName('garden_gnome_01')?.userData?.collateral_value;
|
||||
const carport = loaded.get('carport_01')?.scene
|
||||
.getObjectByName('carport_01')?.userData?.collateral_value;
|
||||
assert(cost > gnome, `gutter (${cost}) must cost more than the gnome (${gnome}) — structural collateral beats ornament`);
|
||||
assert(cost < carport, `gutter (${cost}) must cost less than the carport (${carport}) — the backyard's trap is the gentler one`);
|
||||
// Wreckage carries the same price, so scoring can read either state.
|
||||
const w = loaded.get('house_yardside_wrecked')?.scene.getObjectByName('house_yardside_wrecked');
|
||||
assert(w?.userData?.collateral_value === cost,
|
||||
'the wrecked house must price its gutter the same as the one it used to be');
|
||||
assert(w?.userData?.broken_variant_of === 'house_yardside',
|
||||
'wrecked house must name its intact twin so A can pair the swap');
|
||||
});
|
||||
|
||||
// The wreck's whole message is vertical: the gutter that was UP at the eave is
|
||||
// DOWN. Both halves are measured, and the intact half is the negative control
|
||||
// that proves the measurement measures — if someone rebuilds the wreck with
|
||||
// the run back at the eave (or flips the export axis), gutter_down's box
|
||||
// lands ~2.6 m up and this goes red. Verified red once already: built with
|
||||
// the ground run at eave height as a negative control, this failed at
|
||||
// "tops out at y=2.68" (1 failed / 319 passed) while the Blender verify said
|
||||
// [PASS] 9.46 x 2.10 x 2.92 on the same bad file — the run's PLAN position
|
||||
// didn't move, so no box span changed. A bounding box cannot answer "did it
|
||||
// fall" unless something pins which height it fell FROM.
|
||||
t.test('the torn gutter is DOWN in the wreck, UP in the intact house', () => {
|
||||
const up = loaded.get('house_yardside')?.scene.getObjectByName('gutter');
|
||||
assert(up, 'intact house lost its gutter node');
|
||||
const upBox = new THREE.Box3().setFromObject(up);
|
||||
assert(upBox.max.y > 2.5,
|
||||
`intact gutter tops out at y=${upBox.max.y.toFixed(2)} — the control is broken, nothing below can mean anything`);
|
||||
|
||||
const wreck = loaded.get('house_yardside_wrecked');
|
||||
assert(wreck, 'house_yardside_wrecked did not load');
|
||||
const down = new THREE.Box3().setFromObject(wreck.scene.getObjectByName('gutter_down'));
|
||||
assert(down.max.y < 0.5,
|
||||
`gutter_down tops out at y=${down.max.y.toFixed(2)} — the run is not on the grass`);
|
||||
const hang = new THREE.Box3().setFromObject(wreck.scene.getObjectByName('gutter_torn'));
|
||||
assert(hang.max.y > 2.3,
|
||||
`gutter_torn tops out at y=${hang.max.y.toFixed(2)} — it should still reach the eave it tore from`);
|
||||
assert(hang.min.y < 1.6,
|
||||
`gutter_torn bottoms out at y=${hang.min.y.toFixed(2)} — the torn end should sag well below the fascia line`);
|
||||
|
||||
// And the house itself did NOT fall: unlike the carport, this wreck keeps
|
||||
// its full height, because only the eave line failed. A shorter (or taller)
|
||||
// wreck here means someone wrecked the building instead of the gutter.
|
||||
const hi = new THREE.Box3().setFromObject(loaded.get('house_yardside').scene);
|
||||
const hw = new THREE.Box3().setFromObject(wreck.scene);
|
||||
const dh = Math.abs((hi.max.y - hi.min.y) - (hw.max.y - hw.min.y));
|
||||
assert(dh < 0.05, `wreck height differs from the house by ${dh.toFixed(3)} m — a house does not fall when its gutter does`);
|
||||
});
|
||||
|
||||
// You cannot re-tie to a ripped eave. An anchor that outlives its fascia
|
||||
// would be the free-failure bug back again, wearing a wreck for a costume —
|
||||
// world.anchors would keep offering the tie-off after the aftermath swap.
|
||||
t.test('no fascia anchor survives into the wreck', () => {
|
||||
const w = loaded.get('house_yardside_wrecked');
|
||||
assert(w, 'house_yardside_wrecked did not load');
|
||||
for (const n of ['fascia_anchor_01', 'fascia_anchor_02', 'fascia_anchor_03']) {
|
||||
assert(!w.scene.getObjectByName(n),
|
||||
`${n} survives in the wreck — the eave it anchored is gone, so it must be too`);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
@ -400,7 +490,8 @@ export default async function run(t) {
|
||||
// swap needs a fudge offset per prop and will grow one.
|
||||
t.test('broken variants sit on the same ground plane as their intact twin', () => {
|
||||
for (const [intact, broken] of [['garden_gnome_01', 'garden_gnome_01_broken'],
|
||||
['fence_panel', 'fence_panel_snapped']]) {
|
||||
['fence_panel', 'fence_panel_snapped'],
|
||||
['house_yardside', 'house_yardside_wrecked']]) {
|
||||
for (const n of [intact, broken]) {
|
||||
const box = new THREE.Box3().setFromObject(loaded.get(n).scene);
|
||||
assert(Math.abs(box.min.y) < 0.03,
|
||||
|
||||
Binary file not shown.
BIN
web/world/models/house_yardside_wrecked_v1.glb
Normal file
BIN
web/world/models/house_yardside_wrecked_v1.glb
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user