Compare commits

...

4 Commits

Author SHA1 Message Date
type-two
6461f2267a Lane E S16: [E] gate 5 entry — receipts (determinism, three negative controls red-then-green, look verdicts), prices proposed with reasoning, palette hunks filed for A/D with the _v1 suffixes present, three flags for A (enum edit, a.test 'trampoline' fixture, glasshouse pricing tension)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:21:57 +10:00
type-two
b74383722b Lane E S16: sixteen pins for gate 5 — 10 auto scale/node checks on the five new GLBs + 6 named: pergola resolves through the MANIFEST (never a literal) and prices between gutter and swing; the wreck fails partially with byte-close decks; the glasshouse offers NOTHING from three directions and prices as the apex; panes pinned translucent, wreck pinned emptied-not-felled; the pool's 14 denials pinned per-post BY NAME (RULE 1's regex cannot see them — proven, control M3). 434/0/0 in the browser
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:21:57 +10:00
type-two
453f6cb9d2 Lane E S16: 'pergola' enters ANCHOR_TYPE — A's file, my edit, the swing_frame precedent, flagged in THREADS. A deck is not concrete; the type string is the player's pre-rig read
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:21:57 +10:00
type-two
1571c2f1f9 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>
2026-07-20 20:21:38 +10:00
12 changed files with 1202 additions and 2 deletions

View File

@ -7382,3 +7382,167 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
**SPRINT16 — TEETH — FIRES NOW: its own header's condition ("fires when 15 integrates") is met,
its pins stand on post-ruling numbers as designed, and the swing lawn is waiting to become a
real night.**
[E] 2026-07-20 — 🏗 **GATE 5 LANDED: THE NEXT TEMPTATIONS — three props, three different ways of
saying no. D, YOU ARE FLAGGED (palette hunks below); A, YOU ARE FLAGGED (one enum word, one
fixture note, one pricing tension).** Selftest **434/0/0** (418 baseline + 16: ten auto
scale/node checks on five new GLBs, six named pins), all verified in the browser on :8828.
Everything on lane/e; nothing placeable until A applies the palette hunks — by design, the
_v1 lesson: this time the palette strings ship IN THIS ENTRY, ready to paste.
**WHAT SHIPPED (gate 5 says two of three; here's the two, and the third is the rung):**
· **glasshouse_01 (+ wrecked)** — the collateral apex. 14 translucent panes on an aluminium
frame, ridge at 2.30 m. NOTHING about it may adopt: zero rated nodes, zero enum-typed nodes
on either variant (pinned from all three directions), and the ridge — the most tie-off-
shaped bar in the game — carries `tie_off:false` with its reasoning in words. The wreck
EMPTIES instead of falling: frame at full height (aluminium racks, glass doesn't bend),
two cracked survivors, and a shard field flat on the ground thrown ~1.7 m wider than the
building ever stood. Both claims are measured pins, not adjectives.
· **pool_kit_01** — pool + compliant fence ring (AS 1926.1 flavour: 1.22 m, non-climbable
pickets, self-closing outward gate left ajar). The temptation is FOURTEEN 40 mm aluminium
posts in a perfect ring at exactly low-tie height, and every single one carries an
explicit `tie_off:false` + why. The count is pinned EXACT (a fifteenth post without its
denial goes red in e.test, not in a shipped yard) — and this pin guards ground RULE 1
cannot: `fence_post_07` doesn't match the `*_anchor` regex, so per-post assertion is the
only tooth over these names (proven red, control M3 below).
· **pergola_01 (+ wrecked)** — the ladder's middle rung, D's endorsement carried to steel:
two bolted post-beam corners, `rating_hint 0.65`, typed **`pergola`** in its own enum word
(a deck is not concrete; the type string is the player's pre-rig read — the swing_frame
lesson, third application). ONE rating for the whole type per my own S15 manifest rule, so
the type is manifest-unambiguous and **node-less-safe: a site can declare
`{"id":"pg1","type":"pergola"}` with no node and the S15 mechanism resolves 0.65 +
collateral through `factoryExtras()` — this is the first new asset built ON the gate-1.2
wire, which was the point of building the wire.** The wreck is the 0.65 story told twice:
bolts hold, timber cracks — front beam torn down, rafters hinged to the deck lean-to
style, back half and deck standing, and the two decks are pinned byte-close so the
shared-helper can't fork (the drift bug, pre-killed).
· **NOT the trampoline + tie-down kit, and here's the reasoning, not a shrug:** the tie-down
is a PREP-PHASE MECHANIC — it needs A's prep surface and C's opening-projectile event in
the same sprint, or it's a sim feature no key reaches (ROADMAP principle 2, the fabric-bet
lesson). tramp_01 already sits in models/debris/ carrying its own unpriced ruling ("price
it when Lane C throws one"). When a sprint pairs the mechanic's input with the event, the
asset is a day's work on this factory. ⚠️ **A, the fixture note:** a.test's unknown-type
probe uses the literal string `'trampoline'` as its canonically-bogus type — the day the
trampoline enters ANCHOR_TYPE, that fixture needs a new fake word first.
**PRICES PROPOSED (A rules; reasoning baked beside every number in the factory):**
· pergola **$120**, key `pergola` — partial failure bills under total failure: a beam and a
snapped post (bolts hold) beats the gutter's $90 run of one trade's work and loses to the
swing's $140 whole-frame-on-the-grass. Ladder: 25 < 90 < **120** < 140 < 180 < 320.
· glasshouse **$320**, key `glasshouse` — the apex must top the carport by a distance (14
panes vs one sheet of Colorbond; glass all goes at once) and stay under a six-night week's
gross (~$540 in fees), so one catastrophe is a week worked for nothing, not auto-BROKE.
**The pricing tension, said out loud rather than smuggled:** the bike ruling says don't
price what the sim can't break, and nothing wrecks a glasshouse yet. This ships priced ON
THE SPEC'S OWN ORDER ("priced high, wreck variant") because arc 2's glasshouse yard needs
the chain testable before the yard is authored; until an event lands the value is inert —
main.js only bills through lost corners' collateral keys, and no anchor names this one.
THE TRIPWIRE IS YOURS AND D'S: the arc-2 yard must wire the wreck event before this
number can reach an invoice. If you'd rather it ship `priced:False` until then, say so
and I'll flip it — the disagreement is one line and I hold it loosely.
· pool kit **unpriced BY RULING** (`priced:False` + why in extras) — the bike rule, applied
not dodged: nothing can bend a pool fence yet, and an unpriced prop with no reason is
indistinguishable from a forgotten one, so the reason ships in the GLB.
· Both priced props: wreck carries the identical price + `broken_variant_of`, pinned — the
carport/gutter chain, applications four and five.
**RECEIPTS:**
· Factory: 36/36 PASS (dims, tri budget, node round-trip). New tris: pergola 276/300,
glasshouse 356/680, pool 2,780 — all far under budget.
· **Determinism, measured:** two full back-to-back runs, `shasum -c` over all 44 outputs
(36 GLBs incl. debris copies, report, contact sheet, manifest) — zero mismatches. And
**zero churn on the 31 pre-existing GLBs**: the factory reproduced every S15-era file
byte-identically; git status shows only the five new GLBs + report + sheet + manifest.
· Manifest diff is EXACTLY the pergola entry (`0.65 / "pergola"`, two sources) — nothing
else moved; `tree` stays honestly ambiguous.
· **Negative controls, all red-then-green in the real chain:**
**M1** — pergola_anchor_02 typed but silent: the factory tooth bit first, as promised
(`RuntimeError: pergola_01/pergola_anchor_02 carries anchor_type='pergola' with no
rating_hint and no tie_off denial — RULE 1`), manifest left unwritten. ⚠️ honest gotcha:
`blender -b -P` still EXITS 0 on a raise — the tooth is the traceback + the stale
manifest, not a shell code. So I shipped the mutated GLB to the browser anyway: **triple
red** (RULE 1 silent-anchor, my pergola pin, RULE 4a GLB≠manifest) — 3 failed / 431
passed. Ignoring the traceback cannot ship this.
**M2** — ridge_anchor rated 0.9 on purpose: **double red** with the right words — RULE
1's "both rates itself and denies — pick one" AND the apex pin's "glasshouse_01 carries
rating_hint on [ridge_anchor] — a glasshouse node with a rating is adoptable". 2/432.
**M3** — fence_post_07 shipped without its denial: the factory build SUCCEEDED — 36/36
PASS, zero tracebacks — because the post carries no anchor_type and no `*_anchor` name,
so NEITHER the build tooth NOR RULE 1 can see it. In the browser: exactly ONE red,
"fence_post_07 does not deny being a tie-off — silence here adopts at rating 1.0" (1
failed / 433 passed). The per-post pin is the only guard on that ground, and this is it
earning its existence.
Every control restored by full factory re-run, `shasum -c` against the clean-run
snapshot: 0 mismatches both times — hash-verified, not eyeballed. Final green after the
last restore: **434/0/0** in the browser.
· **LOOKED AT** (assetcheck/look.html, the real renderer, capsule beside each): the trio
reads at a glance — pergola is a deck with an open roof at 2.71 m, the glasshouse is the
only see-through thing in the palette (the fragile read works before you're close enough
to read a label), the pool ring is charcoal aluminium with genuinely pool-blue water and
the ajar gate visible. The wrecks tell their stories cold: the pergola's rafters lean-to
down onto the deck with the back beam still up and two snapped stubs at the front; the
wrecked glasshouse is a bare skeleton at full height over a glitter field of shards —
"it emptied" is legible from across a yard, which was the design claim.
**D — THE PALETTE HUNKS (editor.js is A's file; A or the integrator applies verbatim —
the `_v1` suffixes are present and load-bearing, per your S14 find):**
```js
{
// SPRINT16 gate 5, E. The ladder's middle rung, D-endorsed in S15: bolted
// to a deck — better than loose-on-grass (0.45), worse than concrete
// (1.00). One type, one rating, so the manifest resolves it node-less.
kind: 'structure', label: 'Pergola (the middle rung)', model: 'pergola_01_v1',
requires: ['pergola'],
hint: 'two bolted corners at 0.65 — the honest step between swing frame and post',
make: (id, x, z) => ({
id, model: 'pergola_01_v1', wreckedModel: 'pergola_01_wrecked_v1',
x, z, rotYDeg: 0, solid: true,
collateralKey: 'pergola',
collateralValue: 120, collateralLabel: 'the pergola',
anchors: [1, 2].map((i) => ({
id: `${id}_a${i}`, node: `pergola_anchor_0${i}`, type: 'pergola', work: 'cloth',
})),
}),
},
{
// The collateral apex. NO anchors, on purpose — nothing about a glasshouse
// may adopt (the ridge carries tie_off:false in the GLB). collateralKey is
// EXPLICIT so the second one placed still bills (the carport lesson).
kind: 'structure', label: 'Glasshouse (the apex, $320)', model: 'glasshouse_01_v1',
hint: 'no anchors at all; $320 of glass — the thing you keep the whole rig away from',
make: (id, x, z) => ({
id, model: 'glasshouse_01_v1', wreckedModel: 'glasshouse_01_wrecked_v1',
x, z, rotYDeg: 0, solid: true,
collateralKey: 'glasshouse',
collateralValue: 320, collateralLabel: 'the glasshouse',
anchors: [],
}),
},
{
// The compliance ring. Fourteen posts, every one tie_off:false in the GLB;
// unpriced BY RULING (the bike rule — price it when debris can bend it).
kind: 'structure', label: 'Pool + fence ring', model: 'pool_kit_01_v1',
hint: 'fourteen perfect-looking posts, every one an honest no — the legal tie-offs are elsewhere',
make: (id, x, z) => ({
id, model: 'pool_kit_01_v1', x, z, rotYDeg: 0, solid: true,
anchors: [],
}),
},
```
**D — placement facts, measured in three.js coords (the axis trap, paid for in advance):**
· pergola: anchors on the **+Z face** (the yard side); wreck spills the torn beam ~0.13 m
past the deck on +X — footprint 3.33 × 2.60, standing 2.71 m.
· glasshouse: wreck shard field reaches the plan out to **4.34 × 2.94 m** (intact 2.65 ×
1.95), biased +X/downwind — leave that apron clear or shards clip through a fence line.
· pool kit: gate is on the **+Z run**, ajar and swinging outward (+Z) — 5.64 × 4.52 m
including the swing; ring proper is 5.6 × 4.2.
· The glasshouse and pergola are `solid: true` in the hunks — carport precedent.
**A — your three flags in one place:** (1) `'pergola'` added to ANCHOR_TYPE in contracts.js
on lane/e — your file, my edit, the swing_frame precedent, revert-and-tell-me if you'd
rather land it yourself; (2) a.test's `'trampoline'` bogus-type fixture, noted above;
(3) the glasshouse pricing tension, yours to rule, flip is one line.
Pushed to origin lane/e: factory (three builders + shared helpers + PAL entries), five
GLBs, report + contact sheet + manifest (pergola entry), contracts.js widening, sixteen
e.test pins, this entry.

View File

@ -719,6 +719,193 @@
"anchors": [],
"status": "PASS",
"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": []

View File

@ -137,6 +137,11 @@ PAL = {
"bike_kid": "#D8483C", # the Henderson kid's bike — bought bright on purpose
"bike_grip": "#3B4048", # grips and saddle
"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
@ -1522,6 +1527,544 @@ def build_swing_set_01_wrecked(name):
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):
"""Colorbond garden shed, skillion roof. Spare hardware lives in here."""
root = add_empty(name)
@ -2969,6 +3512,38 @@ ASSETS = [
dict(name="fence_panel_snapped", fn=build_fence_panel_snapped,
dims=((2.38, 2.60), (0.03, 1.05), (1.70, 1.90)),
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"
]
},
"pergola": {
"collateral": "pergola",
"rating_hint": 0.65,
"sources": [
"pergola_01/pergola_anchor_01",
"pergola_01/pergola_anchor_02"
]
},
"post": {
"rating_hint": 1.0,
"sources": [

View File

@ -81,9 +81,17 @@ export const PHASES = ['forecast', 'prep', 'storm', 'aftermath'];
* concreted steel. This is a welded joint on a frame standing on grass: good
* steel, no footing, rating_hint 0.45. Same lesson as the carport, which was
* also nearly smuggled in as a post.
*
* `pergola` (SPRINT16 gate 5, E): a pergola's bolted post-beam corner the
* ladder's middle rung (0.65), D-endorsed in S15. Not a `post` for the same
* reason the swing frame isn't: the word promises the footing, and this one's
* footing is a DECK real bolts into real joists, so better than
* loose-on-grass (0.45), but the deck flexes and the timber cracks before
* concrete would notice (1.00). One rating for the whole type, per the
* manifest rule (THREADS [E] S15): unambiguous types stay node-less-safe.
*/
export const ANCHOR_TYPE = Object.freeze([
'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame',
'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame', 'pergola',
]);
// ---------------------------------------------------------------------------

View File

@ -100,6 +100,22 @@ const ASSETS = [
nodes: ['frame', 'crossbar', 'swings', 'frame_anchor_01', 'frame_anchor_02'] },
{ name: 'swing_set_01_wrecked', h: [0.80, 1.05],
nodes: ['frame', 'crossbar', 'swings'] },
// SPRINT16 gate 5 — the next temptations. Height floors carry design claims:
// the pergola's 2.55 floor sits ABOVE the swing set's 2.15 ceiling (the rung
// it out-rates must stand taller than the rung below it), and the pool
// fence's band brackets 1.2 m because the compliance height IS the flavour.
{ name: 'pergola_01', h: [2.55, 2.85],
nodes: ['deck', 'posts', 'beams', 'rafters',
'pergola_anchor_01', 'pergola_anchor_02'] },
{ name: 'pergola_01_wrecked', h: [2.55, 2.85],
nodes: ['deck', 'posts', 'beams', 'beam_down', 'rafters_down'] },
{ name: 'glasshouse_01', h: [2.15, 2.40],
nodes: ['frame', 'panes', 'door', 'ridge_anchor'] },
{ name: 'glasshouse_01_wrecked', h: [2.15, 2.40],
nodes: ['frame', 'door', 'panes_left', 'shards'] },
{ name: 'pool_kit_01', h: [1.12, 1.32],
nodes: ['pool_shell', 'pool_water', 'fence_rails', 'pool_gate',
'fence_post_01', 'fence_post_14'] },
];
function sizeOf(gltf) {
@ -502,7 +518,9 @@ export default async function run(t) {
for (const [intact, broken] of [['garden_gnome_01', 'garden_gnome_01_broken'],
['fence_panel', 'fence_panel_snapped'],
['swing_set_01', 'swing_set_01_wrecked'],
['house_yardside', 'house_yardside_wrecked']]) {
['house_yardside', 'house_yardside_wrecked'],
['pergola_01', 'pergola_01_wrecked'],
['glasshouse_01', 'glasshouse_01_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,
@ -792,6 +810,246 @@ export default async function run(t) {
`gum is ${g.y.toFixed(2)} m tall and ${Math.max(g.x, g.z).toFixed(2)} m across — the control is broken`);
});
// -------------------------------------------------------------------------
// SPRINT16 gate 5 — THE NEXT TEMPTATIONS. Three props, three different ways
// of saying no: the pergola says "yes, at 0.65" (the ladder's middle rung),
// the glasshouse says "no, and here is the bill if you make me" (the
// collateral apex), the pool ring says "no" fourteen times in a perfect
// circle. Every claim below is a number read off the shipped GLB.
// -------------------------------------------------------------------------
// The pergola's rating is pinned against the MANIFEST, not a literal — the
// gate-1.2 lesson applied to the asset that gate's mechanism was built FOR:
// a site declaring a node-less `pergola` anchor must resolve these exact
// extras, so the manifest entry existing (and being unambiguous) IS the
// feature. A pin that hard-coded 0.65 would stay green while the manifest
// silently dropped the type.
t.test('the pergola is the ladder\'s middle rung, and the manifest can resolve it', () => {
const types = FACTORY_ANCHOR_RATINGS.types;
const p = types.pergola;
assert(p, 'the manifest does not rate "pergola" — either the bake is missing or the '
+ 'type went AMBIGUOUS (its nodes disagree), and a node-less pergola anchor would '
+ 'fall back to the default hint: the sail_post bug shape, on the asset built to bury it');
assert(p.collateral === 'pergola', `manifest collateral is ${JSON.stringify(p.collateral)}, want "pergola"`);
const swing = types.swing_frame.rating_hint, post = types.post.rating_hint;
assert(p.rating_hint > swing && p.rating_hint < post,
`pergola rates ${p.rating_hint} — the rung must sit strictly between the swing frame `
+ `(${swing}, bolted to nothing) and the post (${post}, bolted to the planet); at either `
+ 'end the ladder loses the rung D endorsed and fascia-or-nothing is back');
const g = loaded.get('pergola_01');
assert(g, 'pergola_01 did not load');
for (const n of ['pergola_anchor_01', 'pergola_anchor_02']) {
const o = g.scene.getObjectByName(n);
assert(o, `${n} missing`);
assert(o.userData?.anchor_type === 'pergola',
`${n} is typed ${JSON.stringify(o.userData?.anchor_type)} — a pergola is not a post; `
+ 'the type string is the player\'s pre-rig read and "post" promises concrete');
assert(o.userData?.rating_hint === p.rating_hint,
`${n} bakes ${o.userData?.rating_hint} but the manifest says ${p.rating_hint}`);
assert(o.userData?.collateral === 'pergola', `${n} must name what it takes with it`);
}
// The price ladder, read off the GLBs so site JSON and factory can't tell
// two stories: gutter < pergola < swing set < carport. The pergola bills
// UNDER the swing set because its failure is partial (a beam and a post;
// the bolts hold) where the swing's is total (the whole frame on the
// grass) — prices must agree with the wrecks the player actually sees.
const price = (a) => loaded.get(a)?.scene.getObjectByName(a)?.userData?.collateral_value;
const cost = price('pergola_01');
assert(typeof cost === 'number', 'pergola_01 carries no collateral_value');
const root = g.scene.getObjectByName('pergola_01');
assert(root.userData?.collateral_key === 'pergola',
'collateral_key must name the string the anchors carry, or the price prices nothing');
assert(cost > price('house_yardside') && cost < price('swing_set_01'),
`the pergola (${cost}) must sit between the gutter (${price('house_yardside')}) and the `
+ `swing set (${price('swing_set_01')}) — a partial timber repair beats a run of guttering `
+ 'and loses to a whole frame replacement');
const w = loaded.get('pergola_01_wrecked')?.scene.getObjectByName('pergola_01_wrecked');
assert(w?.userData?.collateral_value === cost,
'the wrecked pergola must be priced the same as the one it used to be');
assert(w?.userData?.broken_variant_of === 'pergola_01',
'the wreck must name its intact twin so A can pair the swap');
});
// The 0.65 story told by the wreck: a bolted structure fails PARTIALLY.
// The front beam and the rafters come down; the deck, the bolts and the
// back half stand. Measured, because "partial" is exactly the kind of word
// that erodes into "rubble" one art pass at a time.
t.test('the pergola fails partially: rafters hinge down, the deck stands, no anchor survives', () => {
const w = loaded.get('pergola_01_wrecked');
assert(w, 'pergola_01_wrecked did not load');
for (const n of ['pergola_anchor_01', 'pergola_anchor_02']) {
assert(!w.scene.getObjectByName(n),
`${n} survives the wreck — you cannot re-bolt a snapped post mid-storm, and an anchor `
+ 'that outlives its structure is the free-failure bug in a costume');
}
// The lean-to read: rafters still reach the surviving back beam AND rest
// on the deck — high at one end, low at the other, in the same node.
const down = new THREE.Box3().setFromObject(w.scene.getObjectByName('rafters_down'));
assert(down.max.y > 2.3,
`rafters_down tops out at y=${down.max.y.toFixed(2)} — they must still hang from the back beam`);
assert(down.min.y < 0.5,
`rafters_down bottoms out at y=${down.min.y.toFixed(2)} — the front ends should rest on the deck`);
const beamDown = new THREE.Box3().setFromObject(w.scene.getObjectByName('beam_down'));
assert(beamDown.max.y < 0.55,
`beam_down tops out at y=${beamDown.max.y.toFixed(2)} — the torn beam lies on the deck`);
// Intact control: the rafters were UP, or "down" above means nothing.
const up = new THREE.Box3().setFromObject(loaded.get('pergola_01').scene.getObjectByName('rafters'));
assert(up.min.y > 2.3,
`intact rafters bottom out at y=${up.min.y.toFixed(2)} — the control is broken`);
// The wreck does not stand taller, and the DECK did not move: it is the
// footing the rating is about, and the shared helper builds it once —
// byte-drift between the twins' decks means someone forked the helper.
const hi = new THREE.Box3().setFromObject(loaded.get('pergola_01').scene);
const hw = new THREE.Box3().setFromObject(w.scene);
assert(hw.max.y <= hi.max.y + 0.02,
`the wreck stands ${hw.max.y.toFixed(2)} m vs the intact ${hi.max.y.toFixed(2)} — taller is a bug`);
const deckA = new THREE.Box3().setFromObject(loaded.get('pergola_01').scene.getObjectByName('deck'));
const deckB = new THREE.Box3().setFromObject(w.scene.getObjectByName('deck'));
assert(deckA.min.distanceTo(deckB.min) < 0.01 && deckA.max.distanceTo(deckB.max) < 0.01,
'the two decks differ — intact and wrecked no longer share the helper, and they will drift');
});
// The glasshouse's one rule, pinned from every direction: NOTHING about it
// may adopt as an anchor. Not the ridge (denied in words), not any node by
// silence (a rating_hint anywhere on either variant is a red), not via the
// enum (no node enrols an anchor_type). The ridge bar is the most
// tie-off-shaped object in the game and that is exactly why the denial is
// load-bearing rather than decorative.
t.test('the glasshouse offers NOTHING: no rated node, no typed node, and the ridge denies', () => {
for (const name of ['glasshouse_01', 'glasshouse_01_wrecked']) {
const g = loaded.get(name);
assert(g, `${name} did not load`);
const rated = [], typed = [];
g.scene.traverse((o) => {
if (typeof o.userData?.rating_hint === 'number') rated.push(o.name);
if (o.userData?.anchor_type !== undefined) typed.push(o.name);
});
assert(rated.length === 0,
`${name} carries rating_hint on [${rated.join(', ')}] — a glasshouse node with a rating `
+ 'is adoptable, and 30 kg of glass is not a thing you add load to');
assert(typed.length === 0,
`${name} enrols [${typed.join(', ')}] in the anchor enum — nothing here is an anchor of any type`);
}
const ridge = loaded.get('glasshouse_01').scene.getObjectByName('ridge_anchor');
assert(ridge, 'ridge_anchor missing — the temptation must exist to be denied');
assert(ridge.userData?.tie_off === false,
'the ridge must carry tie_off:false — the most anchor-looking member says no ON the data');
assert(typeof ridge.userData?.why === 'string' && ridge.userData.why.length > 0,
'the denial must carry its reasoning — a bare flag is a rule, a why is a lesson');
});
// The apex is a price claim, so it is pinned as one: strictly the worst
// bill in the palette, keyed, and identical on the wreck. The billing
// EVENT is arc 2's (nothing in the sim wrecks a glasshouse yet — the value
// is inert data until then, which is why pricing it is not the bike bug);
// this pin is the chain being ready the day that yard is authored.
t.test('the glasshouse is the collateral apex: priced above everything, keyed, twin-priced', () => {
const g = loaded.get('glasshouse_01')?.scene.getObjectByName('glasshouse_01');
assert(g, 'glasshouse_01 root missing');
const cost = g.userData?.collateral_value;
assert(typeof cost === 'number', 'glasshouse carries no collateral_value');
assert(g.userData?.collateral_key === 'glasshouse',
'collateral_key must be explicit — the carport was one editor click from being FREE '
+ 'because its price resolved off a coincidence of naming');
const carport = loaded.get('carport_01')?.scene.getObjectByName('carport_01')?.userData?.collateral_value;
assert(cost > carport,
`the glasshouse (${cost}) must top the carport (${carport}) by construction — fourteen panes `
+ 'of glass against one sheet of Colorbond, or "apex" is just a word');
const w = loaded.get('glasshouse_01_wrecked')?.scene.getObjectByName('glasshouse_01_wrecked');
assert(w?.userData?.collateral_value === cost,
'the wrecked glasshouse must be priced the same as the one it used to be');
assert(w?.userData?.broken_variant_of === 'glasshouse_01',
'the wreck must name its intact twin so A can pair the swap');
});
// What glass DOES, measured: the intact panes are translucent (the fragile
// read is a material fact, not a vibe — turn the glass opaque and the tell
// is deleted), and the wreck EMPTIES rather than falls (frame at full
// height, shard field flat on the ground and thrown wider than the
// building ever stood).
t.test('the glasshouse reads fragile, and its wreck empties instead of falling', () => {
const panes = loaded.get('glasshouse_01')?.scene.getObjectByName('panes');
assert(panes, 'panes node missing');
let translucent = false;
panes.traverse((o) => {
const m = o.material;
if (m && m.transparent === true && m.opacity < 0.6) translucent = true;
});
assert(translucent,
'no pane material is transparent with opacity < 0.6 — the glass has gone opaque, and '
+ 'the only see-through walls in the palette no longer read as glass');
const hi = new THREE.Box3().setFromObject(loaded.get('glasshouse_01').scene);
const wreck = loaded.get('glasshouse_01_wrecked');
const hw = new THREE.Box3().setFromObject(wreck.scene);
assert(Math.abs(hi.max.y - hw.max.y) < 0.05,
`wreck ridge at y=${hw.max.y.toFixed(2)} vs intact ${hi.max.y.toFixed(2)} — the aluminium frame `
+ 'survives; a felled glasshouse is lying about what glass does');
const shards = new THREE.Box3().setFromObject(wreck.scene.getObjectByName('shards'));
assert(shards.max.y < 0.05,
`shards top out at y=${shards.max.y.toFixed(2)} — glass on the ground lies flat`);
const spreadX = (hw.max.x - hw.min.x) - (hi.max.x - hi.min.x);
const spreadZ = (hw.max.z - hw.min.z) - (hi.max.z - hi.min.z);
assert(spreadX > 0.6 && spreadZ > 0.6,
`the shard field adds only ${spreadX.toFixed(2)}/${spreadZ.toFixed(2)} m in plan — it must `
+ 'throw glass clear of the footprint or the wreck reads as a dirty window, not a catastrophe');
assert(wreck.scene.getObjectByName('panes_left'),
'panes_left missing — a wreck with zero surviving panes reads as demolition, not storm');
});
// The pool ring: fourteen posts, fourteen explicit nos. RULE 1's regex only
// guards *_anchor names, and these posts don't match it — which is exactly
// how they'd slip through as silent 1.0s the day a site names one (the
// `?? 1` default is generous, and silence is not neutral). So the denial is
// pinned per-post, by name, with the count exact: a fifteenth post added
// without its denial goes red here, not in a shipped yard.
t.test('the pool ring is fourteen honest nos — every fence post denies, nothing rates', () => {
const g = loaded.get('pool_kit_01');
assert(g, 'pool_kit_01 did not load');
const posts = [];
g.scene.traverse((o) => { if (/^fence_post_\d+$/.test(o.name)) posts.push(o); });
assert(posts.length === 14,
`found ${posts.length} fence posts, want exactly 14 — a post this count doesn't know `
+ 'is a post whose denial nobody checked');
for (const p of posts) {
assert(p.userData?.tie_off === false,
`${p.name} does not deny being a tie-off — silence here adopts at rating 1.0, the best `
+ 'steel in the game conjured out of a missing field');
assert(p.userData?.rating_hint === undefined,
`${p.name} carries a rating_hint, which makes it adoptable — the ring's whole design is `
+ 'that its steel is legally useless');
assert(typeof p.userData?.why === 'string' && p.userData.why.length > 0,
`${p.name} denies without reasoning`);
}
// Nothing anywhere on the kit rates or enrols — same sweep as the glasshouse.
const rated = [], typed = [];
g.scene.traverse((o) => {
if (typeof o.userData?.rating_hint === 'number') rated.push(o.name);
if (o.userData?.anchor_type !== undefined) typed.push(o.name);
});
assert(rated.length === 0 && typed.length === 0,
`pool_kit_01 offers [${[...rated, ...typed].join(', ')}] — the kit must offer nothing`);
// Compliance is the flavour, so its numbers are pinned as facts: 1.2 m
// posts, a self-closing gate, and no price until something can break it.
const post1 = new THREE.Box3().setFromObject(g.scene.getObjectByName('fence_post_01'));
assert(post1.max.y >= 1.15 && post1.max.y <= 1.32,
`fence stands ${post1.max.y.toFixed(2)} m — AS 1926.1 says 1.2, and the height IS the flavour`);
const gate = g.scene.getObjectByName('pool_gate');
assert(gate?.userData?.self_closing === true, 'the gate must be self-closing — compliance is data');
const root = g.scene.getObjectByName('pool_kit_01');
assert(root?.userData?.priced === false && typeof root?.userData?.unpriced_why === 'string',
'the kit must say OUT LOUD that it is unpriced and why (the bike ruling) — an unpriced '
+ 'prop with no reason is indistinguishable from a forgotten one');
assert(root?.userData?.collateral_value === undefined,
'pool_kit_01 carries a collateral_value — pricing an event the sim cannot produce is the '
+ 'lie the invoice exists to kill; when debris can take a panel out, price it THEN');
});
// One GLB carries three wilt states as siblings; Lane A toggles .visible
// rather than reloading, so all three have to be present at once.
t.test('garden_bed carries all 3 damage states in one GLB', () => {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.