diff --git a/tools/blender/asset_report.json b/tools/blender/asset_report.json index 1d555e5..e3b97bd 100644 --- a/tools/blender/asset_report.json +++ b/tools/blender/asset_report.json @@ -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": [] } \ No newline at end of file diff --git a/tools/blender/build_yard_assets.py b/tools/blender/build_yard_assets.py index 9354f43..4f71e63 100644 --- a/tools/blender/build_yard_assets.py +++ b/tools/blender/build_yard_assets.py @@ -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)), diff --git a/tools/blender/contact_sheet.png b/tools/blender/contact_sheet.png index 3199653..fa6ef91 100644 Binary files a/tools/blender/contact_sheet.png and b/tools/blender/contact_sheet.png differ diff --git a/web/world/models/house_yardside_v1.glb b/web/world/models/house_yardside_v1.glb index db71f21..ffe2101 100644 Binary files a/web/world/models/house_yardside_v1.glb and b/web/world/models/house_yardside_v1.glb differ diff --git a/web/world/models/house_yardside_wrecked_v1.glb b/web/world/models/house_yardside_wrecked_v1.glb new file mode 100644 index 0000000..748a024 Binary files /dev/null and b/web/world/models/house_yardside_wrecked_v1.glb differ