Lane E S16 gate 5: the next temptations — glasshouse (collateral apex, $320, wreck empties not falls), pool kit (14 fence posts, 14 explicit tie_off denials, unpriced by the bike ruling), pergola (the 0.65 middle rung D endorsed, $120, partial-failure wreck). Shared-helper wrecks, deterministic (2 runs, 44 files shasum-identical, zero churn on the 31 existing GLBs); manifest gains exactly the pergola entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 20:21:38 +10:00
parent ed62c7d4be
commit 1571c2f1f9
9 changed files with 770 additions and 0 deletions

View File

@ -719,6 +719,193 @@
"anchors": [], "anchors": [],
"status": "PASS", "status": "PASS",
"problems": [] "problems": []
},
{
"name": "pergola_01",
"dims": [
3.2,
2.6,
2.71
],
"tris": 276,
"nodes": [
"beams",
"deck",
"pergola_01",
"pergola_anchor_01",
"pergola_anchor_02",
"posts",
"rafters"
],
"anchors": [
{
"node": "pergola_anchor_01",
"anchor_type": "pergola",
"rating_hint": 0.65,
"collateral": "pergola"
},
{
"node": "pergola_anchor_02",
"anchor_type": "pergola",
"rating_hint": 0.65,
"collateral": "pergola"
}
],
"status": "PASS",
"problems": []
},
{
"name": "pergola_01_wrecked",
"dims": [
3.3288,
2.6,
2.6648
],
"tris": 300,
"nodes": [
"beam_down",
"beams",
"deck",
"pergola_01_wrecked",
"posts",
"rafters_down"
],
"anchors": [],
"status": "PASS",
"problems": []
},
{
"name": "glasshouse_01",
"dims": [
2.645,
1.945,
2.3025
],
"tris": 356,
"nodes": [
"door",
"frame",
"glasshouse_01",
"panes",
"ridge_anchor"
],
"anchors": [
{
"node": "ridge_anchor",
"tie_off": false
}
],
"status": "PASS",
"problems": []
},
{
"name": "glasshouse_01_wrecked",
"dims": [
4.3356,
2.9387,
2.3025
],
"tris": 680,
"nodes": [
"door",
"frame",
"glasshouse_01_wrecked",
"panes_left",
"shards"
],
"anchors": [],
"status": "PASS",
"problems": []
},
{
"name": "pool_kit_01",
"dims": [
5.645,
4.5207,
1.22
],
"tris": 2780,
"nodes": [
"fence_post_01",
"fence_post_02",
"fence_post_03",
"fence_post_04",
"fence_post_05",
"fence_post_06",
"fence_post_07",
"fence_post_08",
"fence_post_09",
"fence_post_10",
"fence_post_11",
"fence_post_12",
"fence_post_13",
"fence_post_14",
"fence_rails",
"pool_gate",
"pool_kit_01",
"pool_shell",
"pool_water"
],
"anchors": [
{
"node": "fence_post_01",
"tie_off": false
},
{
"node": "fence_post_02",
"tie_off": false
},
{
"node": "fence_post_03",
"tie_off": false
},
{
"node": "fence_post_04",
"tie_off": false
},
{
"node": "fence_post_05",
"tie_off": false
},
{
"node": "fence_post_06",
"tie_off": false
},
{
"node": "fence_post_07",
"tie_off": false
},
{
"node": "fence_post_08",
"tie_off": false
},
{
"node": "fence_post_09",
"tie_off": false
},
{
"node": "fence_post_10",
"tie_off": false
},
{
"node": "fence_post_11",
"tie_off": false
},
{
"node": "fence_post_12",
"tie_off": false
},
{
"node": "fence_post_13",
"tie_off": false
},
{
"node": "fence_post_14",
"tie_off": false
}
],
"status": "PASS",
"problems": []
} }
], ],
"debris": [] "debris": []

View File

@ -137,6 +137,11 @@ PAL = {
"bike_kid": "#D8483C", # the Henderson kid's bike — bought bright on purpose "bike_kid": "#D8483C", # the Henderson kid's bike — bought bright on purpose
"bike_grip": "#3B4048", # grips and saddle "bike_grip": "#3B4048", # grips and saddle
"ref_pink": "#E85C8A", # the reference capsule — deliberately loud "ref_pink": "#E85C8A", # the reference capsule — deliberately loud
"alu_frame": "#C9CED2", # powder-coated aluminium — glasshouse + pool fence
"pool_water": "#4FA8BE", # chlorinated blue, NOT the pond's grey-green —
# a pool is the one water in the game that is
# supposed to look inviting
"deck_board": "#A8845C", # decking oil, a shade lighter than fence timber
} }
# Rainwater caught in a sail is not swimming-pool blue. It's shallow, murky, it # Rainwater caught in a sail is not swimming-pool blue. It's shallow, murky, it
@ -1522,6 +1527,544 @@ def build_swing_set_01_wrecked(name):
return root return root
# ---------------------------------------------------------------------------
# THE PERGOLA (SPRINT16 gate 5 — the honest middle rung, D-endorsed)
# ---------------------------------------------------------------------------
# One shape, stated once, used by the intact pergola AND the wreck — the
# _swing_frame precedent, kept on purpose: a wreck built from re-typed numbers
# drifts from its twin one edit at a time.
PERGOLA = dict(
DECK_W=3.2, # deck span, x
DECK_D=2.6, # deck depth, y
DECK_H=0.30, # two steps up — the deck IS the footing, that's the point
POST=0.09, # 90x90 treated pine
POST_H=2.20, # above the deck
INSET=0.18, # posts in from the deck edge
BEAM_H=0.14, # 140x45 beams along x on front/back post pairs
N_RAFTERS=7, # 70x35 rafters spanning y, the open roof
)
# What it costs when the timber cracks. Proposal — Lane A owns the number, the
# ladder it slots into is gnome 25 < gutter 90 < PERGOLA 120 < swing 140 <
# carport 180 (< glasshouse 320, below):
# · the failure is PARTIAL by design: the bolts hold, the timber breaks — a
# torn-off beam and a snapped post on a structure that stays standing. The
# bill is one beam, one post and the labour to re-bolt, not a rebuild;
# · under ~90 it undercuts the gutter, and a structural timber repair does
# not cost less than a run of guttering;
# · over ~140 it outbids the swing set, whose failure is the WHOLE frame on
# the grass — a worse day than a cracked beam, and the prices must agree
# with the wrecks the player actually sees.
PERGOLA_COLLATERAL = 120
def _pergola(name, root, deck_m, timber_m, dark_m, *, wrecked=False):
"""Build the pergola. Returns the two anchor points (Blender coords) so the
caller hangs anchors on the geometry's own numbers, not a second copy of
the arithmetic. Intact and wrecked share every dimension through PERGOLA.
HOW IT FAILS, and why that is the 0.65 story told twice: the posts are
BOLTED to the deck real M12s through the joists, and the bolts hold.
What gives is the timber and the deck's own stiffness: the loaded front
beam tears off its post tops, the front posts snap at half height above
the racking bolts, and the rafters still fixed at the back beam hinge
down like a lean-to until their front ends hit the deck. The back half
stands. A pergola fails PARTIALLY, which is exactly why its rating (0.65)
sits above the unbolted swing frame (0.45) and below concrete (1.00), and
why its bill (a beam and a post) sits below the swing's (a whole frame).
"""
P = PERGOLA
hw, hd = P["DECK_W"] / 2, P["DECK_D"] / 2
px, py = hw - P["INSET"], hd - P["INSET"] # post centres
top = P["DECK_H"] + P["POST_H"] # post top z
beam_z = top + P["BEAM_H"] / 2 # beam centre z
# --- the deck: joists + boards. Identical in the wreck — it was never the
# weak part; it is the footing the rating is ABOUT.
deck = [add_box(f"{name}_deck_slab", (P["DECK_W"], P["DECK_D"], 0.045),
(0, 0, P["DECK_H"] - 0.0225), deck_m)]
for i in range(4):
y = -hd + 0.12 + i * (P["DECK_D"] - 0.24) / 3
deck.append(add_box(f"{name}_joist_{i}", (P["DECK_W"] - 0.1, 0.045, P["DECK_H"] - 0.05),
(0, y, (P["DECK_H"] - 0.05) / 2), dark_m))
# Board grooves read as lines; three thin gaps of darker timber.
for i in range(5):
x = -hw + 0.3 + i * (P["DECK_W"] - 0.6) / 4
deck.append(add_box(f"{name}_board_gap_{i}", (0.015, P["DECK_D"] - 0.02, 0.008),
(x, 0, P["DECK_H"] + 0.004), dark_m))
join_group(deck, "deck", root)
# --- posts. Front pair (y, the yard side, where the anchors live) snaps
# in the wreck; back pair always stands.
posts = []
for sx in (-1, 1):
posts.append(add_box(f"{name}_post_b_{sx}", (P["POST"], P["POST"], P["POST_H"]),
(sx * px, py, P["DECK_H"] + P["POST_H"] / 2), timber_m))
if wrecked:
# Snapped at 55% height: the bolted stub stands plumb, the top
# half lies on the deck with its beam end, split face up.
stub = P["POST_H"] * 0.55
posts.append(add_box(f"{name}_post_f_{sx}", (P["POST"], P["POST"], stub),
(sx * px, -py, P["DECK_H"] + stub / 2), timber_m))
posts.append(add_box(f"{name}_post_top_{sx}",
(P["POST"], P["POST"], P["POST_H"] - stub),
(sx * (px - 0.35), -py + 0.42,
P["DECK_H"] + P["POST"] / 2), timber_m,
rot=(math.radians(88), 0, math.radians(20 * sx))))
else:
posts.append(add_box(f"{name}_post_f_{sx}", (P["POST"], P["POST"], P["POST_H"]),
(sx * px, -py, P["DECK_H"] + P["POST_H"] / 2), timber_m))
join_group(posts, "posts", root)
# --- beams. Back beam always up; front beam is the one that tears off.
beams = [add_box(f"{name}_beam_b", (P["DECK_W"] - 0.1, 0.045, P["BEAM_H"]),
(0, py, beam_z), timber_m)]
if not wrecked:
beams.append(add_box(f"{name}_beam_f", (P["DECK_W"] - 0.1, 0.045, P["BEAM_H"]),
(0, -py, beam_z), timber_m))
join_group(beams, "beams", root)
if wrecked:
# The torn beam, flat on the deck along the front edge, slightly askew.
# Rolled 90° about its own long axis so the 140 mm face lies on the
# boards (a beam at rest lies on its side; on edge it would balance).
join_group([add_box(f"{name}_beam_down", (P["DECK_W"] - 0.1, 0.045, P["BEAM_H"]),
(0.18, -py + 0.15, P["DECK_H"] + 0.03),
timber_m, rot=(math.radians(90), 0, math.radians(6)))],
"beam_down", root)
# --- rafters: up in the intact roof; hinged down at the back beam in the
# wreck, front ends resting on the deck — the lean-to collapse.
rafters = []
for i in range(P["N_RAFTERS"]):
x = -hw + 0.35 + i * (P["DECK_W"] - 0.7) / (P["N_RAFTERS"] - 1)
if wrecked:
drop = math.atan2(top + P["BEAM_H"] - (P["DECK_H"] + 0.05), P["DECK_D"] - 0.3)
L = math.hypot(top + P["BEAM_H"] - (P["DECK_H"] + 0.05), P["DECK_D"] - 0.3)
rafters.append(add_box(f"{name}_rafter_{i}", (0.035, L, 0.07),
(x, py - L / 2 * math.cos(drop),
(top + P["BEAM_H"] + P["DECK_H"] + 0.05) / 2),
timber_m, rot=(-drop, 0, 0)))
else:
rafters.append(add_box(f"{name}_rafter_{i}", (0.035, P["DECK_D"] - 0.15, 0.07),
(x, 0, top + P["BEAM_H"] + 0.035), timber_m))
join_group(rafters, "rafters_down" if wrecked else "rafters", root)
# Anchor points: where the front beam meets the post tops — the bolted
# corner an author would genuinely strap to.
return [(-px, -py, top + P["BEAM_H"] * 0.6), (px, -py, top + P["BEAM_H"] * 0.6)]
def build_pergola_01(name):
"""A timber pergola on a bolted deck — the ladder's missing middle rung.
SPRINT16 gate 5, proposed in S15 and carried on D's playtester endorsement:
"the ladder jumps 0.45-on-grass straight to 1.00-in-concrete; a 0.65
bolted-to-deck mid-rung turns fascia-or-nothing into a real decision."
WHY 0.65, and why one rating for the whole type (the manifest rule my own
S15 entry proposed: new types ship ONE rating per type, so a node-less
JSON anchor can resolve it unambiguously): the post-beam corner is real
90x90 timber with real M12 bolts through a deck genuinely better than a
welded frame standing loose on grass (swing_frame 0.45), because the load
path reaches something fixed. And genuinely worse than 300 mm of concrete
(post 1.00), because what it reaches is a DECK: the deck flexes, the
timber cracks before the bolts let go, and 0.65 is that sentence as a
number. Typed `pergola` in its own enum word calling it a post would
promise concrete the deck cannot deliver (the swing_frame lesson, kept).
"""
root = add_empty(name)
deck_m = get_material("Mat_Deck", PAL["deck_board"], 0.8)
timber_m = get_material("Mat_Timber", PAL["timber"], 0.75)
dark_m = get_material("Mat_TimberDark", PAL["timber_dark"], 0.8)
anchors = _pergola(name, root, deck_m, timber_m, dark_m, wrecked=False)
for i, ap in enumerate(anchors):
e = add_empty(f"pergola_anchor_{i + 1:02d}", ap, root, size=0.18)
e["anchor_type"] = "pergola"
e["rating_hint"] = 0.65
e["collateral"] = "pergola"
e["why"] = ("bolted post-beam corner: the bolts are real, the deck is "
"the footing — better than loose-on-grass, worse than "
"concrete, and the number argues exactly that")
stamp(root, name, "structure")
root["collateral_key"] = "pergola"
root["collateral_value"] = PERGOLA_COLLATERAL
root["collateral_label"] = "the pergola"
root["breakable"] = True
return root
def build_pergola_01_wrecked(name):
"""The pergola after a loaded corner won: front beam torn off, front posts
snapped above their bolts, rafters hinged down onto the deck like a
lean-to. The back half and the deck, and every bolt still standing,
because a bolted structure fails PARTIALLY, which is the whole 0.65 story.
Same origin, same PERGOLA numbers, one flag different, so intact and
wrecked cannot drift (the _swing_frame pattern)."""
root = add_empty(name)
deck_m = get_material("Mat_Deck", PAL["deck_board"], 0.8)
timber_m = get_material("Mat_Timber", PAL["timber"], 0.75)
dark_m = get_material("Mat_TimberDark", PAL["timber_dark"], 0.8)
_pergola(name, root, deck_m, timber_m, dark_m, wrecked=True)
# No pergola_anchor_* survives — you cannot re-bolt to a snapped post from
# inside a storm, and an anchor that outlives its structure is the
# free-failure bug in a costume (fascia rule, third application).
stamp(root, name, "structure")
root["broken_variant_of"] = "pergola_01"
root["collateral_key"] = "pergola"
root["collateral_value"] = PERGOLA_COLLATERAL
root["collateral_label"] = "the pergola"
return root
# ---------------------------------------------------------------------------
# THE GLASSHOUSE (SPRINT16 gate 5 — the collateral apex)
# ---------------------------------------------------------------------------
GLASSHOUSE = dict(
W=2.6, # x — ridge runs along x
D=1.9, # y
WALL_H=1.70,
RIDGE_H=2.28,
BAR=0.045, # aluminium extrusion section
)
# The apex, priced as one. Proposal — Lane A owns the number:
# · it must top the carport (180) by a distance, or "apex" is a word: the
# carport is one sheet of Colorbond; this is ~14 panes of horticultural
# glass, and glass does not bend, it all goes at once. 320 is the panes,
# the reglaze labour, and a racked door that never closes right again;
# · it must stay under a six-night week's gross (~$540 in fees alone), so
# one catastrophe is a week worked for nothing, not an automatic BROKE —
# the carport ruling ("the worst thing on the bill and still a week you
# can dig out of"), scaled to the thing that costs a week;
# · unlike the bike (unpriced, nothing can break it) this ships priced ON
# THE SPEC'S OWN ORDER: arc 2's glasshouse yard is the event's home, and
# the price chain has to exist before that yard can be authored against
# it. Until an event lands, nothing bills it — the value is inert data,
# not a lie on an invoice. The tripwire is written in THREADS: the arc-2
# yard must wire the wreck event before this number reaches a bill.
GLASSHOUSE_COLLATERAL = 320
def _glasshouse_frame(name, root, alu):
"""The aluminium skeleton, shared by intact and wreck — the frame is the
part that SURVIVES (aluminium racks, glass shatters), which is why the
wreck stands at full height and reads as emptied, not felled."""
G = GLASSHOUSE
hw, hd, B = G["W"] / 2, G["D"] / 2, G["BAR"]
parts = []
# Corner + mid posts.
for x in (-hw, 0, hw):
for sy in (-1, 1):
parts.append(add_box(f"{name}_post_{x:.1f}_{sy}", (B, B, G["WALL_H"]),
(x, sy * hd, G["WALL_H"] / 2), alu))
# Eave beams (long sides) + end sills.
for sy in (-1, 1):
parts.append(add_box(f"{name}_eave_{sy}", (G["W"], B, B),
(0, sy * hd, G["WALL_H"]), alu))
for sx in (-1, 1):
parts.append(add_box(f"{name}_end_{sx}", (B, G["D"], B),
(sx * hw, 0, G["WALL_H"]), alu))
# Gable rafters up to the ridge.
for sy in (-1, 1):
p0 = (sx * hw, sy * hd, G["WALL_H"])
p1 = (sx * hw, 0, G["RIDGE_H"])
parts.append(add_tube_between(f"{name}_gable_{sx}_{sy}", p0, p1,
B * 0.45, alu, verts=6))
# The ridge — the straightest, most anchor-looking bar in any yard, which
# is exactly why it gets a named node and a denial (below, on the empty).
parts.append(add_box(f"{name}_ridge", (G["W"], B, B), (0, 0, G["RIDGE_H"]), alu))
join_group(parts, "frame", root)
# Door frame on the -x gable end.
join_group([
add_box(f"{name}_door_l", (B * 0.7, B * 0.7, 1.55), (-hw - 0.001, -0.36, 0.775), alu),
add_box(f"{name}_door_r", (B * 0.7, B * 0.7, 1.55), (-hw - 0.001, 0.36, 0.775), alu),
add_box(f"{name}_door_t", (B * 0.7, 0.76, B * 0.7), (-hw - 0.001, 0, 1.55), alu),
], "door", root)
def build_glasshouse_01(name):
"""A backyard glasshouse — the collateral apex, and NOT an anchor.
SPRINT16 gate 5. The palette's other structures are temptations because
they offer bad steel; this one is a temptation because it offers NO steel
while being ringed in members that look like it an aluminium ridge bar
at 2.28 m is the most tie-off-shaped object in the game. Every reading of
it must come back "don't": the ridge carries an explicit tie_off denial,
and NO node anywhere on either variant carries a rating_hint, which
e.test pins nothing about a glasshouse may adopt as an anchor, because
30 kg of glass in a storm is not a thing you add load to, it is a thing
you keep everything away from.
The panes read fragile at a glance because they are TRANSLUCENT the only
see-through walls in the palette and that read survives export as a
material fact (transparent + alpha), which e.test also pins: a repaint
that turns the glass opaque has silently deleted the tell.
"""
root = add_empty(name)
alu = get_material("Mat_AluFrame", PAL["alu_frame"], 0.35, metallic=0.7)
glass = get_material("Mat_Glass", PAL["glass"], 0.05, metallic=0.0, opacity=0.30)
G = GLASSHOUSE
hw, hd = G["W"] / 2, G["D"] / 2
_glasshouse_frame(name, root, alu)
panes = []
# Wall panes: long sides + gable ends (leaving the door opening).
for sy in (-1, 1):
panes.append(add_box(f"{name}_wall_{sy}", (G["W"] - 0.06, 0.012, G["WALL_H"] - 0.08),
(0, sy * (hd - 0.012), G["WALL_H"] / 2), glass))
panes.append(add_box(f"{name}_endwall_e", (0.012, G["D"] - 0.06, G["WALL_H"] - 0.08),
(hw - 0.012, 0, G["WALL_H"] / 2), glass))
for sy in (-1, 1): # west end: two strips beside the door
panes.append(add_box(f"{name}_endwall_w_{sy}", (0.012, (G["D"] - 0.8) / 2 - 0.05, G["WALL_H"] - 0.08),
(-hw + 0.012, sy * (0.4 + (G["D"] - 0.8) / 4), G["WALL_H"] / 2), glass))
# Roof panes: two slopes from eave to ridge.
slope = math.atan2(G["RIDGE_H"] - G["WALL_H"], hd)
run = math.hypot(G["RIDGE_H"] - G["WALL_H"], hd)
for sy in (-1, 1):
panes.append(add_box(f"{name}_roof_{sy}", (G["W"] - 0.04, run - 0.03, 0.012),
(0, sy * hd / 2, (G["WALL_H"] + G["RIDGE_H"]) / 2 + 0.01),
glass, rot=(sy * slope, 0, 0)))
# Gable triangles, approximated as narrow strips under the rafters.
for sx in (-1, 1):
panes.append(add_box(f"{name}_gable_pane_{sx}", (0.012, G["D"] * 0.5, (G["RIDGE_H"] - G["WALL_H"]) * 0.8),
(sx * (hw - 0.012), 0, G["WALL_H"] + (G["RIDGE_H"] - G["WALL_H"]) * 0.42), glass))
join_group(panes, "panes", root)
# The temptation, denied ON the data. Same mechanism as the swing set's
# crossbar: the most anchor-looking member carries the explicit no.
not_a_tie_off(
add_empty("ridge_anchor", (0, 0, G["RIDGE_H"]), root, size=0.2),
"the ridge bar — the straightest steel-looking line in the yard",
"a 45 mm aluminium extrusion whose whole job is holding glass; load a "
"sail corner on it and you are not testing the bar, you are testing "
"fourteen panes at once")
stamp(root, name, "structure")
root["collateral_key"] = "glasshouse"
root["collateral_value"] = GLASSHOUSE_COLLATERAL
root["collateral_label"] = "the glasshouse"
root["fragile"] = True
root["pane_count"] = 14
return root
def build_glasshouse_01_wrecked(name):
"""The glasshouse after the storm found it. The frame is aluminium — it
stays up, near enough at full height but the glass is GONE: two cracked
panes still hanging, the rest a glitter field of shards on the ground,
thrown wider than the footprint ever was. A glasshouse doesn't fall, it
empties; a wreck that read as a collapsed building would be lying about
what glass does. Same frame helper as the intact one, so the two cannot
drift."""
rng = rng_for(name)
root = add_empty(name)
alu = get_material("Mat_AluFrame", PAL["alu_frame"], 0.35, metallic=0.7)
glass = get_material("Mat_Glass", PAL["glass"], 0.05, metallic=0.0, opacity=0.30)
G = GLASSHOUSE
hw, hd = G["W"] / 2, G["D"] / 2
_glasshouse_frame(name, root, alu)
# Two survivors, one visibly askew in its frame.
join_group([
add_box(f"{name}_pane_left_a", (0.012, G["D"] * 0.4, G["WALL_H"] * 0.5),
(hw - 0.012, -0.3, G["WALL_H"] * 0.35), glass),
add_box(f"{name}_pane_left_b", (G["W"] * 0.3, 0.012, G["WALL_H"] * 0.45),
(0.4, hd - 0.012, G["WALL_H"] * 0.3), glass,
rot=(0, math.radians(8), 0)),
], "panes_left", root)
# The shard field. Deterministic scatter, thrown mostly downwind (+x, the
# storm convention every other wreck in the palette follows) and WIDER
# than the standing footprint — the measured claim e.test pins.
shards = []
for i in range(26):
a = rng.uniform(0, math.tau)
r = rng.uniform(0.2, 1.0)
x = math.cos(a) * (hw + r * 0.9) * (1.25 if math.cos(a) > 0 else 0.8)
y = math.sin(a) * (hd + r * 0.6)
s = rng.uniform(0.06, 0.22)
shards.append(add_box(f"{name}_shard_{i}", (s, s * rng.uniform(0.4, 0.9), 0.008),
(x, y, 0.006), glass,
rot=(0, 0, rng.uniform(0, math.pi))))
# A few inside on the beds' footprint too — the glass fell in as well as out.
for i in range(8):
x, y = rng.uniform(-hw * 0.8, hw * 0.8), rng.uniform(-hd * 0.7, hd * 0.7)
s = rng.uniform(0.05, 0.16)
shards.append(add_box(f"{name}_shard_in_{i}", (s, s * 0.7, 0.008),
(x, y, 0.006), glass, rot=(0, 0, rng.uniform(0, math.pi))))
join_group(shards, "shards", root)
stamp(root, name, "structure")
root["broken_variant_of"] = "glasshouse_01"
root["collateral_key"] = "glasshouse"
root["collateral_value"] = GLASSHOUSE_COLLATERAL
root["collateral_label"] = "the glasshouse"
return root
# ---------------------------------------------------------------------------
# THE POOL KIT (SPRINT16 gate 5 — the compliant ring whose steel all says no)
# ---------------------------------------------------------------------------
POOL_KIT = dict(
RING_W=5.6, # fence ring, x
RING_D=4.2, # fence ring, y
FENCE_H=1.22, # AS 1926.1 minimum is 1.2 m — compliance is the flavour
POOL_W=3.6,
POOL_D=2.2,
COPING_H=0.18, # raised fibreglass lip; keeps every vertex above grade
POST=0.045, # 40 mm aluminium tube posts
)
def _pool_fence_posts():
"""Post centres round the ring, deterministic order. Module-level so the
builder and the ASSETS node list can never disagree about the count."""
P = POOL_KIT
hx, hy = P["RING_W"] / 2, P["RING_D"] / 2
pts = []
for x in (-hx, -hx / 2, 0, hx / 2, hx): # long sides, 5 each
for sy in (-1, 1):
pts.append((x, sy * hy))
for y in (-hy / 2, hy / 2): # short sides, 2 each
for sx in (-1, 1):
pts.append((sx * hx, y))
return pts # 14 posts
def build_pool_kit_01(name):
"""A backyard pool inside its compliant fence ring — the yard the palette
could not dress, and the puzzle is the fence.
SPRINT16 gate 5. Fourteen 40 mm aluminium posts stand in a perfect ring
at exactly the height a low tie line wants, core-mounted, evenly spaced
they LOOK like the most generous anchor field in the game. Every single
one carries an explicit `tie_off: false`, because silence is not neutral:
`adoptAnchor` reads `rating_hint ?? 1`, so an unrated node a site names
becomes the best steel in the game by omission (the S14 audit lesson, and
the reason the spec says "honest on every one"). A pool fence holds a
child OUT; it does not hold a sail DOWN it is 1.2 m of child-proofing
on core-drilled tube, and a loaded corner would have the whole panel out
of the ground before the shackle noticed.
That denial field is the level design: the pool yard's whole puzzle (arc
2) is that the legal tie-offs are all in the wrong places, and the ring
of honest nos in the middle of the yard is what makes it true.
UNPRICED, and that is a statement (the bike/jacaranda ruling, not the
glasshouse's spec order): nothing in the sim can bend a pool fence or
crack a coping yet, and billing an event the player cannot see is the lie
the invoice exists to kill. `priced: False` + the reason ride in extras;
price it the day debris can take a panel out.
"""
root = add_empty(name)
alu = get_material("Mat_AluFrame", PAL["alu_frame"], 0.35, metallic=0.7)
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
water = get_material("Mat_PoolWater", PAL["pool_water"], 0.08, opacity=0.88)
P = POOL_KIT
hx, hy = P["RING_W"] / 2, P["RING_D"] / 2
pw, pd = P["POOL_W"] / 2, P["POOL_D"] / 2
# --- the pool: raised coping ring + water just below the lip -----------
join_group([
add_box(f"{name}_coping_n", (P["POOL_W"] + 0.3, 0.15, P["COPING_H"]),
(0, pd + 0.075, P["COPING_H"] / 2), conc),
add_box(f"{name}_coping_s", (P["POOL_W"] + 0.3, 0.15, P["COPING_H"]),
(0, -pd - 0.075, P["COPING_H"] / 2), conc),
add_box(f"{name}_coping_e", (0.15, P["POOL_D"], P["COPING_H"]),
(pw + 0.075, 0, P["COPING_H"] / 2), conc),
add_box(f"{name}_coping_w", (0.15, P["POOL_D"], P["COPING_H"]),
(-pw - 0.075, 0, P["COPING_H"] / 2), conc),
], "pool_shell", root)
join_group([add_box(f"{name}_water", (P["POOL_W"], P["POOL_D"], 0.02),
(0, 0, P["COPING_H"] - 0.045), water)], "pool_water", root)
# --- the fence: rails + pickets, joined; POSTS as individual named nodes
# so a site (or the editor's node list, if it ever offers per-node
# anchors) meets the denial ON the node it would name. RULE 1's regex
# only guards *_anchor names; these are guarded by their own pin in
# e.test — same rule, wider net.
gate_span = (0.25, 1.15) # gate opening on the south run, x range
rails = []
for sy in (-1, 1):
for z in (0.12, P["FENCE_H"] - 0.03):
if sy < 0: # south run has the gate opening
rails.append(add_box(f"{name}_rail_s{z:.2f}_a", (gate_span[0] + hx, 0.04, 0.04),
((gate_span[0] - hx) / 2, -hy, z), alu))
rails.append(add_box(f"{name}_rail_s{z:.2f}_b", (hx - gate_span[1], 0.04, 0.04),
((gate_span[1] + hx) / 2, -hy, z), alu))
else:
rails.append(add_box(f"{name}_rail_n{z:.2f}", (P["RING_W"], 0.04, 0.04),
(0, hy, z), alu))
for sx in (-1, 1):
for z in (0.12, P["FENCE_H"] - 0.03):
rails.append(add_box(f"{name}_rail_{'e' if sx > 0 else 'w'}{z:.2f}",
(0.04, P["RING_D"], 0.04), (sx * hx, 0, z), alu))
# Pickets every ~0.1 m — the non-climbable read.
for sy in (-1, 1):
n = int(P["RING_W"] / 0.105)
for i in range(1, n):
x = -hx + i * P["RING_W"] / n
if sy < 0 and gate_span[0] < x < gate_span[1]:
continue
rails.append(add_box(f"{name}_pk_y{sy}_{i}", (0.014, 0.014, P["FENCE_H"] - 0.14),
(x, sy * hy, (P["FENCE_H"] - 0.14) / 2 + 0.1), alu))
for sx in (-1, 1):
n = int(P["RING_D"] / 0.105)
for i in range(1, n):
y = -hy + i * P["RING_D"] / n
rails.append(add_box(f"{name}_pk_x{sx}_{i}", (0.014, 0.014, P["FENCE_H"] - 0.14),
(sx * hx, y, (P["FENCE_H"] - 0.14) / 2 + 0.1), alu))
join_group(rails, "fence_rails", root)
# The posts — fourteen honest nos, each its own node.
for i, (x, y) in enumerate(_pool_fence_posts()):
post = join_group([add_cyl(f"{name}_postmesh_{i}", P["POST"] / 2, P["FENCE_H"],
(x, y, P["FENCE_H"] / 2), alu, verts=8)],
f"fence_post_{i + 1:02d}", root)
not_a_tie_off(
post, "pool fence post — compliance steel, not rigging steel",
"40 mm core-mounted aluminium tube holding a child-proof panel; "
"it resists a climbing toddler, not a loaded sail corner — the "
"ring LOOKS like fourteen perfect anchors and every one says no")
# The gate: self-closing, opens outward, latch high — the compliance
# tells, in geometry and in data.
gx0, gx1 = gate_span
gate = join_group([
add_box(f"{name}_gate_frame_l", (0.03, 0.03, P["FENCE_H"] - 0.1),
(gx0 + 0.03, -hy - 0.06, (P["FENCE_H"] - 0.1) / 2 + 0.05), alu,
rot=(0, 0, math.radians(-14))),
add_box(f"{name}_gate_frame_r", (0.03, 0.03, P["FENCE_H"] - 0.1),
(gx1 - 0.15, -hy - 0.28, (P["FENCE_H"] - 0.1) / 2 + 0.05), alu,
rot=(0, 0, math.radians(-14))),
add_box(f"{name}_gate_top", (gx1 - gx0 - 0.1, 0.03, 0.03),
((gx0 + gx1) / 2 - 0.06, -hy - 0.17, P["FENCE_H"] - 0.08), alu,
rot=(0, 0, math.radians(-14))),
*[add_box(f"{name}_gate_pk_{i}", (0.014, 0.014, P["FENCE_H"] - 0.22),
(gx0 + 0.1 + i * (gx1 - gx0 - 0.2) / 7,
-hy - 0.08 - i * 0.028, (P["FENCE_H"] - 0.22) / 2 + 0.08), alu)
for i in range(8)],
], "pool_gate", root)
gate["self_closing"] = True
gate["opens"] = "outward, ajar — someone left it swinging, which is the tell"
gate["latch_height_m"] = 1.5
stamp(root, name, "structure")
root["priced"] = False
root["unpriced_why"] = ("nothing in the sim can bend a pool fence or crack "
"a coping yet — billing an unseen event is the lie "
"the invoice exists to kill (the bike ruling); "
"price it when debris can take a panel out")
root["compliance"] = "AS 1926.1 flavour: 1.2 m, non-climbable, self-closing outward gate"
return root
def build_shed_01(name): def build_shed_01(name):
"""Colorbond garden shed, skillion roof. Spare hardware lives in here.""" """Colorbond garden shed, skillion roof. Spare hardware lives in here."""
root = add_empty(name) root = add_empty(name)
@ -2969,6 +3512,38 @@ ASSETS = [
dict(name="fence_panel_snapped", fn=build_fence_panel_snapped, dict(name="fence_panel_snapped", fn=build_fence_panel_snapped,
dims=((2.38, 2.60), (0.03, 1.05), (1.70, 1.90)), dims=((2.38, 2.60), (0.03, 1.05), (1.70, 1.90)),
nodes=["palings", "rails", "debris_palings"]), nodes=["palings", "rails", "debris_palings"]),
# SPRINT16 gate 5 — the next temptations. The pergola is the ladder's
# honest middle rung (D-endorsed, S15 THREADS): TALLER than the swing set
# it out-rates, and its z floor is above the swing's 2.15 ceiling on
# purpose — if it ever measures swing-height, someone shrank the rung.
dict(name="pergola_01", fn=build_pergola_01,
dims=((3.1, 3.4), (2.4, 2.8), (2.55, 2.85)),
nodes=["deck", "posts", "beams", "rafters",
"pergola_anchor_01", "pergola_anchor_02"]),
# The wreck keeps the deck footprint but spills the torn beam past the
# edge (x floor ABOVE the intact deck's 3.2) and its roof line is the
# rafters hinged down — near intact height at the surviving back beam.
dict(name="pergola_01_wrecked", fn=build_pergola_01_wrecked,
dims=((3.2, 3.6), (2.4, 2.8), (2.55, 2.85)),
nodes=["deck", "posts", "beams", "beam_down", "rafters_down"]),
# The collateral apex. Narrow bands on z because the ridge IS the claim:
# a glasshouse that grew would stop reading as the small fragile thing.
dict(name="glasshouse_01", fn=build_glasshouse_01,
dims=((2.5, 2.8), (1.8, 2.1), (2.15, 2.40)),
nodes=["frame", "panes", "door", "ridge_anchor"]),
# Emptied, not felled: SAME height band as the intact one (the frame is
# aluminium and stands), but the shard field throws the plan dims WIDE —
# the x/y floors sit above the intact ceilings, which is the "it emptied
# outward" claim as a number.
dict(name="glasshouse_01_wrecked", fn=build_glasshouse_01_wrecked,
dims=((3.4, 5.4), (2.6, 3.8), (2.15, 2.40)),
nodes=["frame", "door", "panes_left", "shards"]),
# The compliance ring. z is pinned tight around 1.2 m because the height
# IS the flavour (AS 1926.1); y reaches past the ring for the ajar gate.
dict(name="pool_kit_01", fn=build_pool_kit_01,
dims=((5.4, 5.9), (4.1, 4.8), (1.12, 1.32)),
nodes=["pool_shell", "pool_water", "fence_rails", "pool_gate",
"fence_post_01", "fence_post_14"]),
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

After

Width:  |  Height:  |  Size: 4.5 MiB

View File

@ -82,6 +82,14 @@ export const FACTORY_ANCHOR_RATINGS = Object.freeze({
"house_yardside/fascia_anchor_03" "house_yardside/fascia_anchor_03"
] ]
}, },
"pergola": {
"collateral": "pergola",
"rating_hint": 0.65,
"sources": [
"pergola_01/pergola_anchor_01",
"pergola_01/pergola_anchor_02"
]
},
"post": { "post": {
"rating_hint": 1.0, "rating_hint": 1.0,
"sources": [ "sources": [

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.