Merge remote-tracking branch 'origin/lane/e'
# Conflicts: # THREADS.md
This commit is contained in:
commit
186e75839d
163
THREADS.md
163
THREADS.md
@ -7543,3 +7543,166 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
|||||||
flagged, levers named) — file the feel, we retune the numbers, not the mechanism.
|
flagged, levers named) — file the feel, we retune the numbers, not the mechanism.
|
||||||
|
|
||||||
Pushed lane/a: bfe8763 (seam contract), 71278da (the ledger), this entry.
|
Pushed lane/a: bfe8763 (seam contract), 71278da (the ledger), this entry.
|
||||||
|
[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.
|
||||||
|
|||||||
@ -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": []
|
||||||
|
|||||||
@ -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 |
@ -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": [
|
||||||
|
|||||||
@ -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
|
* 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
|
* steel, no footing, rating_hint 0.45. Same lesson as the carport, which was
|
||||||
* also nearly smuggled in as a post.
|
* 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([
|
export const ANCHOR_TYPE = Object.freeze([
|
||||||
'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame',
|
'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame', 'pergola',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -100,6 +100,22 @@ const ASSETS = [
|
|||||||
nodes: ['frame', 'crossbar', 'swings', 'frame_anchor_01', 'frame_anchor_02'] },
|
nodes: ['frame', 'crossbar', 'swings', 'frame_anchor_01', 'frame_anchor_02'] },
|
||||||
{ name: 'swing_set_01_wrecked', h: [0.80, 1.05],
|
{ name: 'swing_set_01_wrecked', h: [0.80, 1.05],
|
||||||
nodes: ['frame', 'crossbar', 'swings'] },
|
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) {
|
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'],
|
for (const [intact, broken] of [['garden_gnome_01', 'garden_gnome_01_broken'],
|
||||||
['fence_panel', 'fence_panel_snapped'],
|
['fence_panel', 'fence_panel_snapped'],
|
||||||
['swing_set_01', 'swing_set_01_wrecked'],
|
['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]) {
|
for (const n of [intact, broken]) {
|
||||||
const box = new THREE.Box3().setFromObject(loaded.get(n).scene);
|
const box = new THREE.Box3().setFromObject(loaded.get(n).scene);
|
||||||
assert(Math.abs(box.min.y) < 0.03,
|
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`);
|
`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
|
// One GLB carries three wilt states as siblings; Lane A toggles .visible
|
||||||
// rather than reloading, so all three have to be present at once.
|
// rather than reloading, so all three have to be present at once.
|
||||||
t.test('garden_bed carries all 3 damage states in one GLB', () => {
|
t.test('garden_bed carries all 3 damage states in one GLB', () => {
|
||||||
|
|||||||
BIN
web/world/models/glasshouse_01_v1.glb
Normal file
BIN
web/world/models/glasshouse_01_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/glasshouse_01_wrecked_v1.glb
Normal file
BIN
web/world/models/glasshouse_01_wrecked_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/pergola_01_v1.glb
Normal file
BIN
web/world/models/pergola_01_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/pergola_01_wrecked_v1.glb
Normal file
BIN
web/world/models/pergola_01_wrecked_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/pool_kit_01_v1.glb
Normal file
BIN
web/world/models/pool_kit_01_v1.glb
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user