Merge remote-tracking branch 'origin/lane/e'
# Conflicts: # THREADS.md
This commit is contained in:
commit
ecd88f0183
288
THREADS.md
288
THREADS.md
@ -10313,3 +10313,291 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
lull and the veer fire TOGETHER, which is not a contradiction — it is a southerly change,
|
||||
going quiet before it comes back from the other quarter. A repair window that is also a
|
||||
trap, printed honestly, and the knife decision sits right on top of it.
|
||||
[E] 2026-07-25 — 🪟 **GATE 4 LANDED: THE GLASSHOUSE TIER — and the gate's premise had already moved.**
|
||||
**READ THIS FIRST: SPRINT16 ALREADY SHIPPED THE GLASSHOUSE, AND GATE 4'S THREE LITERAL ASKS
|
||||
WERE ALREADY GREEN.** `glasshouse_01` / `_wrecked` are built, exported, in A's palette, and
|
||||
pinned by e.test since S16 gate 5: fragility readable at a glance (translucent panes, pinned as
|
||||
a MATERIAL fact so a repaint that turns the glass opaque goes red), $320 against the gnome's $25
|
||||
(12.8×), and a wreck that EMPTIES instead of falling (aluminium frame at full height, 34 shards
|
||||
thrown wider than the footprint). I checked before building anything, because the cheapest way to
|
||||
ship nothing is to rebuild what exists.
|
||||
**THE REAL GAP WAS WRITTEN IN MY OWN TEST FILE, IN ITS OWN WORDS** (e.test, S16, above the apex
|
||||
pin): *"nothing in the sim wrecks a glasshouse yet — the value is inert data"*. `main.js:877`
|
||||
bills collateral off a **broken rig corner's** baked key (`world.anchor(c.anchorId).collateral`),
|
||||
and a glasshouse correctly offers **no anchors** — the one rule pinned from every direction, and
|
||||
it should stay that way. So the most expensive object in the game **could not cost anybody a
|
||||
cent.** That is RULE 2's mirror image: RULE 2 catches a key with no price; nothing caught a price
|
||||
with no key that can reach it. **It does now, and that assert is the gate.**
|
||||
**WHAT DESIGN.md LINE 179 ACTUALLY ASKED FOR:** *"Jobs: hail nets and mesh sails rigged OVER
|
||||
glass, where your own failure is the worst debris of all."* You do not tie to the glasshouse. You
|
||||
tie to the frame that STRADDLES it. So the tier is one new asset pair, and the glasshouse itself
|
||||
is untouched (zero churn on both its GLBs — the control that proves I only added).
|
||||
|
||||
**`hail_net_frame_01` / `hail_net_frame_01_wrecked` — 328 / 480 tris, 3.42 × 3.02 × 2.77 m.**
|
||||
A galvanised-then-powder-coated portal frame: two portals clear of the gable ends (±0.30 m), legs
|
||||
clear of the long walls (±0.45 m), head beams at 2.75 m so a net clears the 2.28 m ridge, eave
|
||||
rails, a thin tensioning wire down the middle, and the hail net **rolled** along one eave —
|
||||
rolled, because the JOB is to rig it, and a deployed net is a finished yard.
|
||||
|
||||
**THE TELL IS A POSITION, AND THAT IS THE NEW THING.** Every other trap in this game punishes you
|
||||
with a bad number on the object: the carport beam at 0.22, the fascia at 0.35, the corroded post
|
||||
at 0.55 whose real capacity you cannot read. **This frame's numbers are GOOD** — engineered
|
||||
portal, four real footings, welded factory eyes — and it is the most dangerous anchor in the game
|
||||
because of WHERE IT STANDS. So the tell is pinned as GEOMETRY off two shipped GLBs, not as paint
|
||||
on one: the frame's plan footprint **strictly contains** the glasshouse's on both axes
|
||||
(x ±1.71 vs ±1.32, z ±1.51 vs ±0.98) and its head **clears the ridge by 0.47 m** (2.766 vs
|
||||
2.30), and all four eyes sit **above** the ridge. "It stands over the glass" is a measurement.
|
||||
**THE 20 M READ IS METALNESS, NOT LIGHTNESS — S17's lesson, applied deliberately this time.**
|
||||
S17 measured that galvanised (metalness 0.90) renders **near-black** at yard distance because a
|
||||
near-mirror with no environment map has nothing to mirror — rgb(49,51,49). So every steel the
|
||||
player is allowed to trust is DARK. This frame is powder-coat, i.e. **paint**: metalness 0.05,
|
||||
diffuse, base luminance 0.903 against galvanised 0.734. **Measured off the shipped GLB, then
|
||||
LOOKED AT in the game's own renderer** (`tools/assetcheck/look.html`, cam at eye height 1.7 m):
|
||||
at 20 m the honest sail post is a near-black line, the frame is a **white cage**, the glasshouse
|
||||
is visibly see-through. At 11–16 m the rolled net reads as a hard **dark bar** along one eave
|
||||
(lum 0.254) — which Blender's own contact-sheet view transform had washed out to near-white, the
|
||||
jacaranda lesson again: a colour judgement made in Blender is a judgement about Blender.
|
||||
|
||||
**THE RUNG — 0.80. I OWN IT AS A PROPOSAL, A RULES.** Ladder: 0.22 carport beam < 0.30 carport
|
||||
post < 0.35 fascia < 0.45 swing frame < 0.55 corroded post < 0.65 pergola < **0.80 hail_net_frame**
|
||||
< 1.00 sail post. Two halves to the argument:
|
||||
· **It EARNS the highest rung below the post.** A pergola is a deck screwed to a house and it
|
||||
flexes; the swing frame stands on grass; the corroded post's capacity is unknowable. This is an
|
||||
engineered frame on four footings and **the only structure in the palette that was built to be
|
||||
tied to.** De-rating honest steel for looking dangerous is the corroded post's lie inverted.
|
||||
· **It is not 1.00 because of SECTION, not condition.** 32 mm sized for knitted hail mesh —
|
||||
~0.2 kg/m² and near-zero drag, because mesh *spills* wind. A 25 m² impermeable sail in a
|
||||
southerly is an order of magnitude more load. 0.80 is a 20% de-rate for "engineered for a load
|
||||
you are about to exceed", and the reasoning is baked on the root as `rated_for`.
|
||||
**AND 0.80 IS CHOSEN SO THAT IT USUALLY HOLDS. That is the tier, not a softness in it.** A corner
|
||||
that lets go one night in five costs you a shackle on a sail post and **$320** here. The player
|
||||
who learned "0.80 is fine" on every other yard is exactly the player this yard is for. The lesson
|
||||
is not "don't trust this steel" — the steel is fine. It is **"the same failure has a different
|
||||
price depending on what is underneath it"**, which is the sentence line 179 has been waiting for
|
||||
an asset to say.
|
||||
**COLLATERAL: the frame is UNPRICED, BY RULING** (the bike/jacaranda pattern). Its anchors bill
|
||||
the **glass** at $320 (unchanged, A's number, endorsed). A folded portal frame is obviously also
|
||||
a repair bill, but one broken corner can name exactly one collateral key, and inventing a second
|
||||
bill with no mechanism to split it is the guess the invoice exists to refuse. Price it when a
|
||||
break can bill two things.
|
||||
**MANIFEST — UNAMBIGUOUS.** `anchor_ratings.gen.js` regenerated: `hail_net_frame` → rating 0.80,
|
||||
`collateral: "glasshouse"`, **four agreeing sources**, not in `ambiguous`. So a yard can place
|
||||
this **node-less** (`factoryExtras` carries both the 0.80 AND the 'glasshouse' key off the type)
|
||||
exactly like the corroded post — D does not have to name nodes to author a glasshouse yard.
|
||||
It is the second type whose collateral names a **different** structure than itself; `house` →
|
||||
`"gutter"` is the precedent, and the shape is the same: **an anchor names what its failure TAKES,
|
||||
not what it is.**
|
||||
|
||||
**NEGATIVE CONTROLS — five, each witnessed RED in the real harness at :8828, my own fronted tab,
|
||||
then green. Batched only where the mutations hit disjoint tests, so every red is attributable.**
|
||||
· **M4 — the anchors stop naming the glass (the PRE-SPRINT state).** Witnessed free, and it is
|
||||
the headline: on the run where the two GLBs were built but not yet in e.test's own ASSETS list,
|
||||
the apex pin went red with exactly *"NOTHING in the palette can bill "glasshouse" ($320)"*.
|
||||
**That is the hole gate 4 was really about, printed by the test that closes it.**
|
||||
· **M1 — the frame stops bracketing the glass** (CLEAR_X/Y negative): BRACKETS red, *"the frame
|
||||
spans x −0.86..0.86 and the glasshouse −1.32..1.32"*. **Caught TWICE** — the factory's own dims
|
||||
band rejects the build outright, so this control needed `--no-verify` to even export. Belt and
|
||||
braces, and I'll take it.
|
||||
· **M2 — powder-coat becomes galvanised** (steel_gal at metallic 0.90): METALNESS red, *"the frame
|
||||
paints at metalness 0.8999…"*. The tell dying **silently** is the whole risk here: the frame
|
||||
stays white in the palette and goes black on screen.
|
||||
· **M3 — the frame CLAIMS the glass's key** (`collateral_key = "glasshouse"` on the root): apex
|
||||
pin red on its second half. This is gate 5's landmine **on my own asset**, which is why the
|
||||
omission is commented at the bake site rather than merely done.
|
||||
· **M5 / M6 — the gate-5 site controls**, below.
|
||||
Counts held at 533 through every control run (530 passed + 3 failed), so no test silently
|
||||
vanished while I was reddening others.
|
||||
**DETERMINISM — the rule I set in S17, held.** Three consecutive full factory runs; runs 2 and 3
|
||||
are **56/56 byte-identical** (39 GLBs, 4 verbatim debris copies, 8 textures, `asset_report.json`,
|
||||
`contact_sheet.png`, `anchor_ratings.gen.js`).
|
||||
|
||||
⚠️ **BLENDER VERSION IS PART OF THE OUTPUT — READ BEFORE YOU BUILD. This box is WRONG.**
|
||||
The shipped GLBs were exported by **Blender 5.1.2** (glTF I/O v5.1.20); `asset_report.json`
|
||||
records it in its `blender` field. This box has **5.0.1** (I/O v5.0.21). A full run here rewrote
|
||||
**all 45 binaries** — every GLB and every texture — and both differences are cosmetic:
|
||||
· GLB accessor min/max drift in the **7th decimal** (~6e−8 m, 60 nanometres). Node graph, mesh
|
||||
topology, materials and every custom prop are **bit-identical** (verified chunk by chunk).
|
||||
· The generated PNGs **lose their sRGB/gAMA/cHRM chunks** — exactly 73 bytes each. IDAT pixel
|
||||
data decompresses **bit-identically**; 5.0.x simply omits the colour-space chunks 5.1.x writes.
|
||||
**I reverted all 45 and committed only my two new files.** Shipping that churn would have been a
|
||||
45-binary diff nobody could review, hiding a real (if small) colour-management regression, to add
|
||||
two assets — my S17 words for this exact shape were *"a corrupt artifact, not a decision"*. It is
|
||||
now documented at the top of `build_yard_assets.py` so the next E session doesn't ship the
|
||||
downgrade by accident: if `git status` shows every GLB modified after you added one asset, that is
|
||||
this. **Better: build on 5.1.2 and the churn is zero.** I did NOT chase a second machine for it —
|
||||
adding a box to the asset pipeline to chase exporter metadata is an unforced variable, and the
|
||||
difference is provably semantically nil.
|
||||
|
||||
**THE WRECK — and I changed it because I LOOKED.** First cut racked the failed portal 0.30 m in
|
||||
and kicked its feet 0.16 m out. In the render the top frame **still read as a clean level
|
||||
rectangle**: a 0.43 m drop on a 2.77 m frame of 32 mm tube is invisible in perspective, and a
|
||||
wreck you cannot see is not a wreck. Now 0.45 each way — head IN (dragged by the sail), feet OUT
|
||||
— so the portal **racks into a parallelogram**, the carport's failure mode on a lighter section:
|
||||
~22° of lean, one foot clear of its pad, one **orphaned footing**, the top frame a visible
|
||||
trapezoid, and the net off its lashings draped down over the glass and heaped past the footing.
|
||||
Verified at 16 m in the game's renderer: unmistakably broken at a glance.
|
||||
The head beam rests **ACROSS the ridge** (2.33) rather than below it, deliberately: the
|
||||
glasshouse's own wreck keeps its aluminium frame standing at full height, so a beam that ended up
|
||||
under the ridge line would be *inside* it. **The wreck carries NO GLASS** (pinned): the glasshouse
|
||||
swaps independently, and shards in both would double-draw the night both swap — and would quietly
|
||||
claim the glass's failure as this asset's. **No anchor survives the wreck** (pinned; fascia rule,
|
||||
sixth application). Same height band as the intact frame, because a portal frame folds at one end
|
||||
— it does not lie down.
|
||||
|
||||
**PLACEMENT FACTS, three.js coords, measured off the exported GLBs (not off the builder):**
|
||||
· `hail_net_frame_01` — footprint **3.42 × 3.02 m**, height **2.766 m**. Four eyes,
|
||||
`net_anchor_01..04`, all at **y = 2.690**, on the corners of a 3.20 × 2.80 m rectangle:
|
||||
**(−1.600, 2.690, +1.400) · (+1.600, 2.690, +1.400) · (+1.600, 2.690, −1.400) ·
|
||||
(−1.600, 2.690, −1.400)**. Sized around `glasshouse_01` (2.65 × 1.95 × 2.30): **place both at
|
||||
the same x/z and the frame straddles it correctly with no fiddling.**
|
||||
· `hail_net_frame_01_wrecked` — **WRECK APRON: reserve ~0.9 m of clear ground on −X** beyond the
|
||||
footing line (x reaches −2.59 against the intact −1.71; the **net heap** sets that bound, not
|
||||
the racked feet). Height 2.783 m. It is the far side from the standing portal.
|
||||
|
||||
**📋 A / INTEGRATOR — EDITOR PALETTE HUNK, APPLY VERBATIM** (editor.js is A's; leadFor precedent,
|
||||
eighth application). Insert next to the glasshouse. A's own palette test uses `PALETTE.find`, not
|
||||
a length assert, so this adds no rows to a.test:
|
||||
|
||||
```js
|
||||
{
|
||||
// SPRINT18 gate 4, E. The glasshouse tier's LIVE half — the frame you rig a
|
||||
// hail net FROM, straddling the glass you must never rig TO. DESIGN.md line
|
||||
// 179: "hail nets and mesh sails rigged over glass, where your own failure
|
||||
// is the worst debris of all." Place it at the same x/z as a glasshouse and
|
||||
// it straddles correctly; the eyes land at y=2.690, clear of the 2.28 ridge.
|
||||
//
|
||||
// 0.80, and it USUALLY HOLDS — that is the tier, not a softness in it. Every
|
||||
// other trap here punishes you with a bad number; this is good steel
|
||||
// (engineered portal, four footings, welded eyes) and the hazard is WHERE IT
|
||||
// STANDS. The corner failure that costs a shackle on a sail post costs $320.
|
||||
//
|
||||
// ⚠️ `_v1` is LOAD-BEARING and its absence is SILENT: adoptAnchor does
|
||||
// `rating_hint ?? 1`, so a typo does not fail — it rates these four eyes
|
||||
// 1.00, the best steel in the game, on the highest-collateral anchor there
|
||||
// is. (S14, D's cold pass.)
|
||||
// ⚠️ THIS ITEM WRITES NO collateralKey, ON PURPOSE. The anchors bill
|
||||
// 'glasshouse'; the GLASSHOUSE owns that key. structFor() returns the FIRST
|
||||
// structure matching a key, so a frame that claimed it would bill the panes
|
||||
// and then wreck the FRAME — both papers agreeing and both wrong.
|
||||
// ⚠️ PLACE IT OVER A GLASSHOUSE. With no glasshouse in the site,
|
||||
// collateralFor('glasshouse') returns null and these four corners are a FREE
|
||||
// failure — the gutter bug. e.test pins that per-yard and names the anchor.
|
||||
kind: 'structure', label: 'Hail-net frame (0.80 — over the glass, $320 under it)',
|
||||
model: 'hail_net_frame_01_v1',
|
||||
requires: ['hail_net_frame'],
|
||||
hint: 'four good eyes at 2.7 m, rated 0.80 — and everything they drop lands on the glasshouse',
|
||||
make: (id, x, z) => ({
|
||||
id, model: 'hail_net_frame_01_v1',
|
||||
wreckedModel: 'hail_net_frame_01_wrecked_v1',
|
||||
x, z, rotYDeg: 0, solid: true,
|
||||
anchors: [1, 2, 3, 4].map((i) => ({
|
||||
id: `${id}_a${i}`, node: `net_anchor_0${i}`, type: 'hail_net_frame', work: 'cloth',
|
||||
})),
|
||||
}),
|
||||
},
|
||||
```
|
||||
|
||||
⚠️ **A — ANCHOR_TYPE WIDENED** (contracts.js is yours; carport / swing_frame / pergola /
|
||||
corroded_post precedent, fifth application). I added `'hail_net_frame'` with its reasoning beside
|
||||
it, including why it must NOT be typed `post`: "post" promises a corner whose failure costs a
|
||||
shackle. Revert-and-tell-me standard; your own "the widened enum is CHECKED" test covers it and
|
||||
is green.
|
||||
|
||||
**🪟 A — THE TIER IS PLACEABLE, AND THE ONE-VARIABLE LAW IS YOURS TO ENFORCE.** Both GLBs are
|
||||
landed, deterministic and manifest-resolvable **node-less**, so a glasshouse yard can be authored
|
||||
now. **Per SPRINT18's own hard gate: this must NOT debut on the same night as the emergency
|
||||
callout**, however well line 29 pairs them — the glasshouse debuts as an ordinary
|
||||
rig-for-tonight job, and the flogging-sail-over-glass sentence is the payoff, a later week. I am
|
||||
supplying it and saying so; the ordering is yours.
|
||||
|
||||
[E] 2026-07-25 — 🔑 **GATE 5 HALF-LANDED: THE SECOND-RUST-POST LANDMINE — my half, and the exact
|
||||
wiring A needs. DISARMED AT AUTHOR TIME; the runtime fix is one `??` and it is A's.**
|
||||
D's filing, with D's measured receipt ($115 on the offer card vs $45 on the failure route). The
|
||||
mechanism, re-read at source and confirmed: `world.js:526` `structFor(key)` iterates
|
||||
`structures.values()` and **returns the first match**; `main.js:877` dedupes bills by key;
|
||||
`scorecard.js:355` `collateralExposure` dedupes by key again; `board.js:330` `exposureOf` counts
|
||||
per **STRUCTURE**. All four are individually correct and they disagree the moment two structures
|
||||
share a key. **The pool yard dodges it by shipping exactly one corroded post on purpose — that is
|
||||
a yard author remembering, which is not a defence.**
|
||||
**AND GATE 4 JUST MADE IT LIKELIER, which is why these two gates are one seam.** A glasshouse
|
||||
yard now carries a structure that **owns** the key and a frame whose anchors **point at** it. Two
|
||||
glasshouses with a frame each trips this on the first night. So my own asset carries the omission
|
||||
deliberately (no `collateral_key` on the frame root) with the reasoning commented at the bake.
|
||||
|
||||
**MY HALF — two pins in e.test, both witnessed red then green:**
|
||||
· **`no shipped yard gives two structures the same collateral key, and ids are unique`.** Resolves
|
||||
each structure's key exactly the way `structKey()` does (site JSON → baked GLB key → id as last
|
||||
resort), across all four shipped yards. **M5 control — I added a second corroded post to the
|
||||
pool yard** and it went red naming both. It also pins **duplicate structure ids**, which is a
|
||||
separate silent death I found while reading: `structures.set(st.id, …)` means a duplicate id
|
||||
**silently replaces** the first structure — it never renders, never bills, never wrecks, and
|
||||
nothing anywhere says a word. `validateSite` checks duplicate ANCHOR ids and not structure ids.
|
||||
· **`every collateral an authored anchor names is OWNED by exactly one thing in that yard`.** The
|
||||
runtime twin of RULE 2: RULE 2 proves a key is priced *somewhere in the palette*, this proves it
|
||||
is owned *in the yard that names it*. **M6 control — I placed the net frame in the pool yard,
|
||||
which has no glasshouse** → red: *"nf1_a1 will bill "glasshouse" on failure, and 0 things in
|
||||
this yard own that key"*. **Zero owners is the gutter bug reborn**, and it is now caught before
|
||||
a yard ships rather than by a player breaking a corner for free.
|
||||
|
||||
**🔧 A — THE WIRING, EXACT. Two hunks in world.js (yours) and one in validateSite (yours).**
|
||||
The reason a site cannot currently just give two posts different keys: `structKey()` already
|
||||
prefers `spec.collateralKey`, so the STRUCTURE half works today. The **anchor** half does not —
|
||||
`adoptAnchor` takes the key off the baked GLB node, identical for every instance, and
|
||||
`factoryExtras(type)` does the same for node-less anchors. So renaming the structures' keys makes
|
||||
it **worse, not better**: both anchors still say `'corroded_post'`, which now matches neither
|
||||
`'corroded_post_a'` nor `'corroded_post_b'`, `collateralFor` returns null, and both corners
|
||||
become **FREE**. Hence:
|
||||
1. **`world.js:541`** (structure anchor creation) — let the site name its own target, applied
|
||||
AFTER `factoryExtras` so it wins:
|
||||
```js
|
||||
anchors.push(Object.assign(
|
||||
makeStaticAnchor(a.id, a.type ?? 'post', new THREE.Vector3(st.x, heightAt(st.x, st.z) + 2.4, st.z)),
|
||||
{ work: a.work },
|
||||
a.node ? null : factoryExtras(a.type ?? 'post'),
|
||||
a.collateral ? { collateral: a.collateral } : null, // SPRINT18 gate 5
|
||||
));
|
||||
```
|
||||
2. **`world.js:690`** (`adoptAnchor`) — one `??`, so the bake is the DEFAULT and never clobbers an
|
||||
override:
|
||||
```js
|
||||
anchor.collateral = anchor.collateral ?? node.userData?.collateral ?? null;
|
||||
```
|
||||
Every shipped site is byte-unchanged by both hunks: with no override the chain is identical.
|
||||
3. **`validateSite`** — the loud author-time refusal, mirroring my pin, next to the existing
|
||||
duplicate-anchor-id check: reject two structures resolving to one collateral key, and reject
|
||||
duplicate structure ids.
|
||||
**WHAT THAT BUYS, PRECISELY.** A yard can then ship two corroded posts: `collateralKey`
|
||||
`corroded_post_a` / `_b` on the structures, and the matching `collateral` on each structure's
|
||||
anchors. `collateralFor` resolves each to its own structure (the VALUE lives on the site spec, so
|
||||
$45 each), the right post wrecks, and **the two harnesses agree**: `exposureOf` counts two
|
||||
structures = $90, `collateralExposure` sees two keys = $90. **D's $115-vs-$45 split closes to
|
||||
$115 vs $90** — and the residual **$25 is exactly the gnome**, which `collateralExposure` excludes
|
||||
because no anchor reaches it. That is not a bug, it is **your ruling to make**: does an unreachable
|
||||
priced prop belong in the offer card's exposure total? I have no vote; I am handing you a number
|
||||
with one variable left in it instead of two.
|
||||
**AND THE SAME WIRING IS WHAT THE GLASSHOUSE TIER WILL WANT** for two-glasshouse yards, which is
|
||||
why I did not invent a second mechanism for it.
|
||||
|
||||
[E] 2026-07-25 — ✏️ **CORRECTION TO MY OWN ENTRY, SAME SESSION: I overwrote the field I told you to
|
||||
trust.** The gate-4 entry above says *"the shipped GLBs were exported by Blender 5.1.2;
|
||||
`asset_report.json` records it in its `blender` field — check it before you build."* **That
|
||||
instruction is now wrong, and I made it wrong.** My run regenerated the report, so the committed
|
||||
`asset_report.json` reads **`"blender": "5.0.1"`** — this box — while 37 of the 39 GLBs beside it
|
||||
are still the **5.1.2** exports I deliberately reverted. Read literally, the field now tells the
|
||||
next session "you are on the right Blender" at the exact moment they are not, which is the same
|
||||
shape as every trap this sprint went hunting: a source of truth that quietly starts answering a
|
||||
different question.
|
||||
**What is actually true of the committed tree:** `hail_net_frame_01_v1.glb` and
|
||||
`hail_net_frame_01_wrecked_v1.glb` were built by **5.0.1**; the other 37 GLBs and all 8 textures
|
||||
are **5.1.2** artifacts, untouched. The report's per-asset facts (dims, tris, node names, baked
|
||||
extras) are correct for every one of the 39 either way — that is measured, not assumed, and it is
|
||||
why reverting was safe.
|
||||
**So `asset_report.json:blender` names THE LAST MACHINE THAT RAN THE FACTORY, not the machine
|
||||
that built the GLBs you are looking at.** Those are the same thing only after a churn-free run.
|
||||
The honest check is the one I actually used: build, then `git status` — **if files you did not
|
||||
author come back modified, you are on the wrong Blender**, and the header of
|
||||
`build_yard_assets.py` tells you what the differences mean. I am leaving the generated file alone
|
||||
rather than hand-editing it (it says "do NOT hand-edit" for good reasons, and a doctored report is
|
||||
worse than a mislabelled one); the fix is this paragraph, and the mixed-exporter state going into
|
||||
the integrator's eyes on purpose. If someone runs the full factory on a 5.1.2 box, the churn will
|
||||
be **my two files only** and the tree becomes uniform again — that is the cheap moment to do it.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"blender": "5.1.2",
|
||||
"blender": "5.0.1",
|
||||
"assets": [
|
||||
{
|
||||
"name": "ref_capsule",
|
||||
@ -817,6 +817,73 @@
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
{
|
||||
"name": "hail_net_frame_01",
|
||||
"dims": [
|
||||
3.42,
|
||||
3.02,
|
||||
2.766
|
||||
],
|
||||
"tris": 328,
|
||||
"nodes": [
|
||||
"footings",
|
||||
"hail_net_frame_01",
|
||||
"legs",
|
||||
"net_anchor_01",
|
||||
"net_anchor_02",
|
||||
"net_anchor_03",
|
||||
"net_anchor_04",
|
||||
"net_roll",
|
||||
"top_frame"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "net_anchor_01",
|
||||
"anchor_type": "hail_net_frame",
|
||||
"rating_hint": 0.8,
|
||||
"collateral": "glasshouse"
|
||||
},
|
||||
{
|
||||
"node": "net_anchor_02",
|
||||
"anchor_type": "hail_net_frame",
|
||||
"rating_hint": 0.8,
|
||||
"collateral": "glasshouse"
|
||||
},
|
||||
{
|
||||
"node": "net_anchor_03",
|
||||
"anchor_type": "hail_net_frame",
|
||||
"rating_hint": 0.8,
|
||||
"collateral": "glasshouse"
|
||||
},
|
||||
{
|
||||
"node": "net_anchor_04",
|
||||
"anchor_type": "hail_net_frame",
|
||||
"rating_hint": 0.8,
|
||||
"collateral": "glasshouse"
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
{
|
||||
"name": "hail_net_frame_01_wrecked",
|
||||
"dims": [
|
||||
4.3007,
|
||||
3.02,
|
||||
2.7832
|
||||
],
|
||||
"tris": 480,
|
||||
"nodes": [
|
||||
"footings",
|
||||
"hail_net_frame_01_wrecked",
|
||||
"legs",
|
||||
"net_down",
|
||||
"top_frame"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
{
|
||||
"name": "pool_kit_01",
|
||||
"dims": [
|
||||
@ -955,5 +1022,42 @@
|
||||
"problems": []
|
||||
}
|
||||
],
|
||||
"debris": []
|
||||
"debris": [
|
||||
{
|
||||
"file": "BlueCrate_v2.glb",
|
||||
"dims": [
|
||||
0.36,
|
||||
0.36,
|
||||
0.29
|
||||
],
|
||||
"sane": true
|
||||
},
|
||||
{
|
||||
"file": "BlackTub_v2.glb",
|
||||
"dims": [
|
||||
0.36,
|
||||
0.54,
|
||||
0.2
|
||||
],
|
||||
"sane": true
|
||||
},
|
||||
{
|
||||
"file": "WhiteTub_v2.glb",
|
||||
"dims": [
|
||||
0.36,
|
||||
0.54,
|
||||
0.2
|
||||
],
|
||||
"sane": true
|
||||
},
|
||||
{
|
||||
"file": "WoodenBin_v2.glb",
|
||||
"dims": [
|
||||
0.35,
|
||||
0.36,
|
||||
0.31
|
||||
],
|
||||
"sane": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -20,6 +20,25 @@ Outputs (resolved from this file, so no absolute home paths):
|
||||
tools/blender/contact_sheet.png verification render vs the 1.7 m capsule
|
||||
tools/blender/asset_report.json measured dims / tris / node names
|
||||
|
||||
⚠️ BLENDER VERSION IS PART OF THE OUTPUT (SPRINT18, E — measured, not guessed).
|
||||
The shipped GLBs were exported by Blender **5.1.2** (glTF I/O v5.1.20), which
|
||||
`asset_report.json` records in its `blender` field — check it before you
|
||||
build. On Blender 5.0.1 (I/O v5.0.21) this script is still perfectly
|
||||
deterministic — three consecutive full runs gave 56/56 byte-identical
|
||||
artifacts — but its output DIFFERS from the shipped files in two ways, and
|
||||
both are cosmetic:
|
||||
· GLB accessor min/max drift in the 7th decimal (~6e-8 m, i.e. 60 nm).
|
||||
Node graph, mesh topology, materials and every custom prop are
|
||||
bit-identical; only float formatting of the bounds moves.
|
||||
· The generated PNGs lose their sRGB/gAMA/cHRM chunks — 73 bytes per file.
|
||||
IDAT pixel data decompresses bit-identically; the newer exporter simply
|
||||
writes colour-space chunks that 5.0.x omits.
|
||||
So a full run on the wrong Blender rewrites all 45 binaries with no change
|
||||
anybody asked for, and quietly DROPS the colour-space chunks. If your `git
|
||||
status` shows every GLB and texture modified after adding one asset, that is
|
||||
this and not your work: `git checkout --` the ones you did not author, and
|
||||
commit only the new files. Better: build on 5.1.2 and the churn is zero.
|
||||
|
||||
Idiom follows ~/Documents/Destroyulater/3D-STORE/racks_to_glb.py:
|
||||
reset_to_empty() per asset -> build under a root empty AT THE ORIGIN ->
|
||||
join by group -> stamp custom props -> export_scene.gltf(export_yup=True,
|
||||
@ -150,6 +169,23 @@ PAL = {
|
||||
# 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
|
||||
"net_frame_white": "#E6E7E1", # SPRINT18 gate 4 — the hail-net frame's
|
||||
# powder-coat, and the BRIGHTEST entry in this
|
||||
# palette on purpose. The reason is metalness,
|
||||
# not lightness — S17's measured lesson applied
|
||||
# deliberately this time. Galvanised ships at
|
||||
# metallic 0.90, and with no environment map a
|
||||
# near-mirror has nothing to mirror, so every
|
||||
# steel you are ALLOWED to trust renders
|
||||
# near-black at yard distance (measured: rgb
|
||||
# 49,51,49 at 20 m). Powder-coat is paint —
|
||||
# metallic 0.05, diffuse — so this frame reads
|
||||
# as a white cage standing over the glass from
|
||||
# the far fence, and can never be misread as
|
||||
# the galvanised steel of an honest sail post.
|
||||
"net_mesh": "#3A4340", # knitted HDPE hail mesh: dark, matte, and the
|
||||
# only large dark mass any yard carries up at
|
||||
# 2.7 m. Rolled it is a bar; down it is a heap.
|
||||
}
|
||||
|
||||
# Rainwater caught in a sail is not swimming-pool blue. It's shallow, murky, it
|
||||
@ -1916,6 +1952,283 @@ def build_glasshouse_01_wrecked(name):
|
||||
return root
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THE HAIL-NET FRAME (SPRINT18 gate 4 — the glasshouse tier's LIVE half)
|
||||
# ---------------------------------------------------------------------------
|
||||
# SPRINT16 shipped the glasshouse and priced it at 320. SPRINT18 read the gate
|
||||
# and found the thing the price was waiting on, written in e.test's own words:
|
||||
# "nothing in the sim wrecks a glasshouse yet — the value is inert data". The
|
||||
# apex could not be reached. main.js bills collateral off a BROKEN RIG CORNER
|
||||
# (`world.anchor(c.anchorId).collateral`), and a glasshouse deliberately offers
|
||||
# no anchors — correctly, it is the one rule pinned from every direction — so
|
||||
# the most expensive object in the game could not cost anybody a cent. That is
|
||||
# the free-failure bug inverted: not a hazard billing nothing by accident, but
|
||||
# a hazard that cannot bill at all by construction.
|
||||
#
|
||||
# DESIGN.md line 179 already named the missing asset: "Jobs: hail nets and mesh
|
||||
# sails rigged OVER glass, where your own failure is the worst debris of all."
|
||||
# You do not tie to the glasshouse. You tie to the frame that STRADDLES it —
|
||||
# and every corner of that frame is a corner whose failure drops steel, cloth
|
||||
# and hardware through fourteen panes.
|
||||
#
|
||||
# So this is the tier, and its teaching is new. Every other trap in this game
|
||||
# punishes you with a bad number: the carport beam at 0.22, the fascia at 0.35,
|
||||
# the corroded post at 0.55 whose real capacity you cannot read. This one is
|
||||
# GOOD hardware — purpose-built, plumb, on its own footings, factory eyes —
|
||||
# and it is the most dangerous anchor in the game because of WHERE IT STANDS.
|
||||
# The rating is not the hazard. The position is.
|
||||
NET_FRAME = dict(
|
||||
CLEAR_X=0.30, # portal stands this far clear of each gable end
|
||||
CLEAR_Y=0.45, # legs stand this far outside the long walls, on soil
|
||||
HEAD_H=2.75, # head-beam height. MUST clear the ridge (2.28) — a frame
|
||||
# that fouled the glass it protects is a joke, and
|
||||
# e.test measures the clearance off the two shipped GLBs.
|
||||
TUBE=0.032, # 32 mm net-frame section
|
||||
PAD=0.22, # footing pad
|
||||
)
|
||||
|
||||
# The rung. PROPOSAL — Lane A rules the number, same as every rung before it.
|
||||
#
|
||||
# Ladder as shipped: 0.22 carport beam < 0.30 carport post < 0.35 house fascia
|
||||
# < 0.45 swing frame < 0.55 corroded post < 0.65 pergola < 1.00 sail post.
|
||||
# This goes at 0.80, and the argument has two halves:
|
||||
#
|
||||
# · WHY IT OUT-RATES THE PERGOLA (0.65) BY A DISTANCE. The pergola is a deck
|
||||
# structure screwed to a house and it flexes; the corroded post's capacity
|
||||
# is unknowable; the swing frame stands on grass. This is an engineered
|
||||
# portal frame on four real footings with welded eyes, and it is the only
|
||||
# structure in the palette that was BUILT to be tied to. Rating it near the
|
||||
# bad steel would be the lie the corroded post exists to punish, inverted:
|
||||
# honest steel de-rated for looking dangerous.
|
||||
# · WHY IT IS NOT THE SAIL POST (1.00). The section is 32 mm sized for hail
|
||||
# mesh — a couple of hundred grams per square metre, and near-zero drag
|
||||
# because mesh SPILLS wind rather than catching it. A 25 m² impermeable
|
||||
# shade sail in a southerly is a load one order of magnitude larger. 0.80
|
||||
# is a 20% de-rate for "engineered for a load you are about to exceed".
|
||||
#
|
||||
# And 0.80 is chosen so that IT USUALLY HOLDS. That is the point of the tier,
|
||||
# not a softness in it: a corner that fails one night in five costs you nothing
|
||||
# on a sail post and 320 dollars here. The player who learns "0.80 is fine" on
|
||||
# every other yard is exactly the player this yard is for. The lesson is not
|
||||
# "don't trust this steel" — the steel is fine. It is "the same failure has a
|
||||
# different price depending on what is underneath it", which is the sentence
|
||||
# DESIGN.md line 179 has been waiting for an asset to say.
|
||||
NET_FRAME_RATING = 0.80
|
||||
|
||||
|
||||
def _net_frame(name, root, steel, conc, mesh_m, *, wrecked=False):
|
||||
"""The portal frame, shared by intact and wreck so the two cannot drift.
|
||||
|
||||
The wreck fails at the -x portal ONLY: its feet kick out of their pads and
|
||||
its head beam comes down onto the glasshouse ridge. +x stands, holding the
|
||||
eave rails up, which is why the wreck keeps the intact height — a hail-net
|
||||
frame does not fall over, it folds at one end, and the end it folds is the
|
||||
end that empties the glass."""
|
||||
G, N = GLASSHOUSE, NET_FRAME
|
||||
hx = G["W"] / 2 + N["CLEAR_X"] # 1.60 — clear of the gable ends
|
||||
hy = G["D"] / 2 + N["CLEAR_Y"] # 1.40 — clear of the long walls
|
||||
H, r, pad_h = N["HEAD_H"], N["TUBE"] / 2, 0.06
|
||||
|
||||
def foot(sx, sy):
|
||||
"""Where the leg meets the ground. Wrecked -x feet are OUT of the pad —
|
||||
a foot beside its own footing is the most legible steel failure there
|
||||
is, and it costs one offset to say."""
|
||||
if wrecked and sx < 0:
|
||||
return (sx * hx - 0.45, sy * hy, pad_h + (0.14 if sy > 0 else 0.0))
|
||||
return (sx * hx, sy * hy, pad_h)
|
||||
|
||||
def head(sx, sy):
|
||||
"""Where the leg meets the top frame. The wrecked -x portal has come
|
||||
down and is RESTING ON THE RIDGE (2.28) rather than passing through it:
|
||||
the glasshouse's own wreck keeps its aluminium frame standing at full
|
||||
height, so a beam that ended up below the ridge line would be inside
|
||||
it. Lying ACROSS the ridge is both true and non-interpenetrating.
|
||||
|
||||
The head comes IN (+x, dragged by the sail) while the feet kick OUT
|
||||
(-x) — the portal RACKS into a parallelogram, the carport's failure
|
||||
mode on a lighter section. The offsets are deliberately large (0.45
|
||||
each way, ~22° of lean over 2.3 m) because I looked at 0.30/0.16 in
|
||||
the render and the top frame still read as a clean level rectangle:
|
||||
a 0.43 m drop on a 2.77 m frame of 32 mm tube is invisible in
|
||||
perspective, and a wreck you cannot see is not a wreck."""
|
||||
if wrecked and sx < 0:
|
||||
return (sx * hx + 0.45, sy * hy * 0.90, G["RIDGE_H"] + 0.05)
|
||||
return (sx * hx, sy * hy, H)
|
||||
|
||||
def mid(sx):
|
||||
a, b = head(sx, -1), head(sx, 1)
|
||||
return ((a[0] + b[0]) / 2, 0.0, (a[2] + b[2]) / 2)
|
||||
|
||||
pads, legs = [], []
|
||||
for sx in (-1, 1):
|
||||
for sy in (-1, 1):
|
||||
pads.append(add_box(f"{name}_pad_{sx}_{sy}", (N["PAD"], N["PAD"], pad_h),
|
||||
(sx * hx, sy * hy, pad_h / 2), conc))
|
||||
legs.append(add_tube_between(f"{name}_leg_{sx}_{sy}",
|
||||
foot(sx, sy), head(sx, sy), r, steel, verts=8))
|
||||
join_group(pads, "footings", root)
|
||||
join_group(legs, "legs", root)
|
||||
|
||||
top = []
|
||||
for sx in (-1, 1): # portal head beams, spanning y
|
||||
top.append(add_tube_between(f"{name}_head_{sx}", head(sx, -1), head(sx, 1),
|
||||
r, steel, verts=8))
|
||||
for sy in (-1, 1): # eave rails, spanning x
|
||||
top.append(add_tube_between(f"{name}_eave_{sy}", head(-1, sy), head(1, sy),
|
||||
r, steel, verts=8))
|
||||
# The tensioning wire down the middle, deliberately thinner than the frame:
|
||||
# at 20 m the gauge difference is the "made for a net" read, and it is the
|
||||
# honest reason the whole thing rates 0.80 instead of 1.00.
|
||||
top.append(add_tube_between(f"{name}_ridge_line", mid(-1), mid(1),
|
||||
r * 0.35, steel, verts=6))
|
||||
join_group(top, "top_frame", root)
|
||||
|
||||
if not wrecked:
|
||||
# Rolled along the +y eave, because the JOB is to rig it. A deployed
|
||||
# net would be a finished yard, and this asset exists to be worked on.
|
||||
join_group([add_tube_between(f"{name}_roll", (-hx, hy, H - 0.11),
|
||||
(hx, hy, H - 0.11), 0.085, mesh_m, verts=10)],
|
||||
"net_roll", root)
|
||||
else:
|
||||
# YOUR debris, not the storm's. The net has come off its lashings at the
|
||||
# collapsed end and is draped down over the glass and heaped out past
|
||||
# the footing — deterministic scatter, so the same wreck twice.
|
||||
rng = rng_for(name)
|
||||
drape = []
|
||||
pts = [(hx * 0.85, hy, H - 0.10),
|
||||
(hx * 0.10, hy * 0.92, H - 0.78),
|
||||
(-hx * 0.55, hy * 0.80, G["RIDGE_H"] * 0.55),
|
||||
(-hx - 0.28, hy * 0.55, 0.24),
|
||||
(-hx - 0.80, hy * 0.20, 0.06)]
|
||||
for i in range(len(pts) - 1):
|
||||
drape.append(add_tube_between(f"{name}_net_sag_{i}", pts[i], pts[i + 1],
|
||||
0.055, mesh_m, verts=6))
|
||||
for i in range(9):
|
||||
a = rng.uniform(0, math.tau)
|
||||
rr = rng.uniform(0.05, 0.45)
|
||||
drape.append(add_box(
|
||||
f"{name}_net_heap_{i}",
|
||||
(rng.uniform(0.22, 0.50), rng.uniform(0.16, 0.40), rng.uniform(0.03, 0.09)),
|
||||
(-hx - 0.58 + math.cos(a) * rr, hy * 0.20 + math.sin(a) * rr * 0.8,
|
||||
rng.uniform(0.02, 0.07)),
|
||||
mesh_m, rot=(0, 0, rng.uniform(0, math.pi))))
|
||||
join_group(drape, "net_down", root)
|
||||
|
||||
return hx, hy
|
||||
|
||||
|
||||
def build_hail_net_frame_01(name):
|
||||
"""The hail-net frame — four good anchors in the worst place in the game.
|
||||
|
||||
The glasshouse tier's live half (SPRINT18 gate 4). This is what DESIGN.md
|
||||
line 179 means by "hail nets and mesh sails rigged OVER glass": a portal
|
||||
frame that straddles the glasshouse, clear of it on all four sides, with a
|
||||
head height that clears the ridge and a rolled mesh net waiting to go up.
|
||||
|
||||
THE TELL, and it is a position rather than a defect. Every reading of every
|
||||
other trap in this game is a bad number on the object. This object's numbers
|
||||
are GOOD — 0.80, plumb, four footings, welded eyes — and what the player has
|
||||
to read instead is the plan view: the frame BRACKETS the glass. e.test pins
|
||||
that geometrically off the two shipped GLBs (frame footprint strictly
|
||||
contains the glasshouse footprint; head height strictly clears the ridge),
|
||||
because "it stands over the glass" has to be a measurement, not a vibe.
|
||||
The 20 m read is the powder-coat: white, matte, metallic 0.05, against a
|
||||
palette where every trustworthy steel is metallic 0.90 and therefore renders
|
||||
near-black. A white cage over a glass box, visible from the far fence.
|
||||
|
||||
WHAT THE ANCHORS SAY. All four bake collateral="glasshouse". They are the
|
||||
only nodes in the palette whose collateral names a DIFFERENT structure than
|
||||
the one they sit on (the house's fascia→"gutter" is the precedent, and it is
|
||||
the same shape: the anchor names what its failure TAKES, not what it is).
|
||||
Break a corner here and the sail, the shackle and the turnbuckle go through
|
||||
fourteen panes: your own failure is the worst debris of all, wired as data.
|
||||
|
||||
AND WHAT THIS ROOT DELIBERATELY DOES NOT CARRY: collateral_key. See below —
|
||||
it is the whole of gate 5 in one omission.
|
||||
"""
|
||||
root = add_empty(name)
|
||||
# Powder-coat is PAINT, so metallic stays near zero. This is not a style
|
||||
# choice, it is the S17 measurement: high metalness with no environment map
|
||||
# renders near-black, and this asset's entire long-range tell is that it is
|
||||
# the brightest thing in the yard.
|
||||
steel = get_material("Mat_NetFrame", PAL["net_frame_white"], 0.50, metallic=0.05)
|
||||
mesh_m = get_material("Mat_HailNet", PAL["net_mesh"], 0.85, metallic=0.0)
|
||||
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
|
||||
|
||||
hx, hy = _net_frame(name, root, steel, conc, mesh_m)
|
||||
|
||||
# The four eyes, at the four corners of the top frame — where a net corner
|
||||
# ties, and therefore where a sail corner will be tied instead.
|
||||
for i, (sx, sy) in enumerate(((-1, -1), (1, -1), (1, 1), (-1, 1))):
|
||||
e = add_empty(f"net_anchor_{i + 1:02d}",
|
||||
(sx * hx, sy * hy, NET_FRAME["HEAD_H"] - 0.06), root, size=0.18)
|
||||
e["anchor_type"] = "hail_net_frame"
|
||||
e["rating_hint"] = NET_FRAME_RATING
|
||||
e["collateral"] = "glasshouse"
|
||||
e["why"] = ("sound steel, and the most expensive corner on the board: it is "
|
||||
"rated for hail mesh, and everything it drops lands on glass")
|
||||
|
||||
stamp(root, name, "structure")
|
||||
root["site_hint"] = "glasshouse"
|
||||
root["is_anchor_trap"] = True
|
||||
root["rated_for"] = ("knitted hail mesh, ~0.2 kg/m2, near-zero drag — mesh spills "
|
||||
"wind. A 25 m2 impermeable sail is an order of magnitude more.")
|
||||
# ⚠️ NO collateral_key ON THIS ROOT, AND THAT IS THE POINT (gate 5).
|
||||
#
|
||||
# world.js resolves a collateral key to a structure with structFor(), which
|
||||
# returns the FIRST structure in site order whose key matches. If this frame
|
||||
# claimed collateral_key="glasshouse" — which looks like the helpful thing
|
||||
# to do, since its anchors bill that key — then in a yard where the frame is
|
||||
# authored before the glasshouse, every failure would bill "the glasshouse"
|
||||
# for 320 dollars and then WRECK THE FRAME: the player watches their own
|
||||
# frame fold while the invoice charges them for panes that are still in
|
||||
# their rebates. The bill and the wreck would agree with each other and
|
||||
# both be wrong, which is the only kind of wrong this repo cannot see.
|
||||
#
|
||||
# So exactly ONE structure owns a key, and it is the one that owns the
|
||||
# price. This frame POINTS at the key and never claims it. The pointer
|
||||
# lives in a field nothing resolves, so it can document intent without
|
||||
# arming anything.
|
||||
root["collateral_target"] = "glasshouse"
|
||||
root["rigs_over"] = "glasshouse_01"
|
||||
return root
|
||||
|
||||
|
||||
def build_hail_net_frame_01_wrecked(name):
|
||||
"""The frame after the corner you trusted let go.
|
||||
|
||||
The -x portal has folded: both its feet are out of their pads, and its head
|
||||
beam is lying across the glasshouse ridge with the net draped down over the
|
||||
glass and heaped out past the footing. The +x portal stands, holding the
|
||||
eave rails at full height — so this wreck is the same height as the intact
|
||||
frame, which is the honest read: a portal frame folds at one end, it does
|
||||
not lie down.
|
||||
|
||||
It carries NO GLASS. The glasshouse has its own wreck variant and swaps
|
||||
independently; a net-frame wreck that shipped shards would draw them twice
|
||||
the moment both swapped on the same night, and would also quietly claim
|
||||
the glass's failure as its own. e.test pins the absence.
|
||||
|
||||
UNPRICED, BY RULING — the bike/jacaranda pattern. A folded portal frame is
|
||||
obviously a repair bill, but the player is being taught about the glass, and
|
||||
one broken corner can only name one collateral key. Inventing a second bill
|
||||
with no mechanism to split it between them is exactly the guess the invoice
|
||||
exists to refuse. Price it when a break can bill two things."""
|
||||
root = add_empty(name)
|
||||
steel = get_material("Mat_NetFrame", PAL["net_frame_white"], 0.50, metallic=0.05)
|
||||
mesh_m = get_material("Mat_HailNet", PAL["net_mesh"], 0.85, metallic=0.0)
|
||||
conc = get_material("Mat_Concrete", PAL["concrete"], 0.95)
|
||||
|
||||
_net_frame(name, root, steel, conc, mesh_m, wrecked=True)
|
||||
|
||||
stamp(root, name, "structure")
|
||||
root["broken_variant_of"] = "hail_net_frame_01"
|
||||
# No anchor survives the wreck — the fascia rule, sixth application. An
|
||||
# anchor that outlives its structure is the free-failure bug in a costume.
|
||||
root["collateral_target"] = "glasshouse"
|
||||
return root
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# THE POOL KIT (SPRINT16 gate 5 — the compliant ring whose steel all says no)
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -3824,6 +4137,25 @@ ASSETS = [
|
||||
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"]),
|
||||
# SPRINT18 gate 4 — the glasshouse tier's live half. Every band here is a
|
||||
# design claim about the glass underneath it, which is why they are tight:
|
||||
# x and y floors must both EXCEED the glasshouse's ceilings (2.8 / 2.1), or
|
||||
# the frame has stopped straddling the thing it bills; z must clear the
|
||||
# ridge (2.40 ceiling) or a frame built to protect glass is resting on it.
|
||||
dict(name="hail_net_frame_01", fn=build_hail_net_frame_01,
|
||||
dims=((3.30, 3.55), (2.90, 3.15), (2.70, 2.85)),
|
||||
nodes=["footings", "legs", "top_frame", "net_roll",
|
||||
"net_anchor_01", "net_anchor_02", "net_anchor_03", "net_anchor_04"]),
|
||||
# Folded at one end, not felled: the SAME z band as the intact frame,
|
||||
# because the +x portal stands and holds the eave rails up. If z ever
|
||||
# collapses, someone has laid a portal frame down flat and it never does
|
||||
# that. x runs WIDE on the -x side — the net heaped out past the footing,
|
||||
# and the wreck apron a yard author has to keep clear.
|
||||
# 4.10 floor, not 4.30: the measured span is 4.30 and it is set by the NET
|
||||
# HEAP, not by the racked feet, so it must not sit on the band boundary.
|
||||
dict(name="hail_net_frame_01_wrecked", fn=build_hail_net_frame_01_wrecked,
|
||||
dims=((4.10, 5.30), (2.90, 3.15), (2.70, 2.85)),
|
||||
nodes=["footings", "legs", "top_frame", "net_down"]),
|
||||
# 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,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.8 MiB After Width: | Height: | Size: 5.0 MiB |
@ -80,6 +80,16 @@ export const FACTORY_ANCHOR_RATINGS = Object.freeze({
|
||||
"sail_post_corroded/top_anchor"
|
||||
]
|
||||
},
|
||||
"hail_net_frame": {
|
||||
"collateral": "glasshouse",
|
||||
"rating_hint": 0.8,
|
||||
"sources": [
|
||||
"hail_net_frame_01/net_anchor_01",
|
||||
"hail_net_frame_01/net_anchor_02",
|
||||
"hail_net_frame_01/net_anchor_03",
|
||||
"hail_net_frame_01/net_anchor_04"
|
||||
]
|
||||
},
|
||||
"house": {
|
||||
"collateral": "gutter",
|
||||
"rating_hint": 0.35,
|
||||
|
||||
@ -100,10 +100,26 @@ export const PHASES = ['forecast', 'prep', 'storm', 'aftermath'];
|
||||
* can't inspect the inside of). One rating for the whole type, per the
|
||||
* manifest rule. The GLB wears the tell (rust, a sagging pad eye); the type
|
||||
* string is the player's pre-rig read of the same fact.
|
||||
*
|
||||
* `hail_net_frame` (SPRINT18 gate 4, E): the frame that straddles a glasshouse
|
||||
* so a hail net can be rigged OVER the glass (DESIGN.md line 179). It is the
|
||||
* first anchor word in this enum whose danger is not in its own number.
|
||||
* rating_hint 0.80 — the highest rung below the honest post, and it EARNS it:
|
||||
* an engineered portal frame on four real footings with welded eyes, the only
|
||||
* structure in the palette that was built to be tied to. The 20% de-rate is
|
||||
* for section, not condition (32 mm sized for hail mesh, which spills wind;
|
||||
* a 25 m² impermeable sail is an order of magnitude more load).
|
||||
* What makes it the most dangerous anchor in the game is that its baked
|
||||
* `collateral` names the GLASSHOUSE, not itself — the `house`→"gutter"
|
||||
* precedent, and the same shape: an anchor names what its failure TAKES, not
|
||||
* what it is. Break a corner here and the sail, the shackle and the turnbuckle
|
||||
* go through fourteen panes at $320. The type string is the player's pre-rig
|
||||
* read of a hazard that is a POSITION, and the reason it must not be typed
|
||||
* `post`: "post" promises a corner whose failure costs a shackle.
|
||||
*/
|
||||
export const ANCHOR_TYPE = Object.freeze([
|
||||
'house', 'tree', 'post', 'carport', 'carport_post', 'swing_frame', 'pergola',
|
||||
'corroded_post',
|
||||
'corroded_post', 'hail_net_frame',
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -113,6 +113,16 @@ const ASSETS = [
|
||||
nodes: ['frame', 'panes', 'door', 'ridge_anchor'] },
|
||||
{ name: 'glasshouse_01_wrecked', h: [2.15, 2.40],
|
||||
nodes: ['frame', 'door', 'panes_left', 'shards'] },
|
||||
// SPRINT18 gate 4 — the glasshouse tier's live half. The height floor is a
|
||||
// design claim about the asset next to it: 2.70 sits ABOVE the glasshouse's
|
||||
// 2.40 ceiling, because a net frame that does not clear the ridge is resting
|
||||
// on the glass it exists to protect. The wreck shares the band — the +x
|
||||
// portal stands, so a portal frame folds at one end rather than falling.
|
||||
{ name: 'hail_net_frame_01', h: [2.70, 2.85],
|
||||
nodes: ['footings', 'legs', 'top_frame', 'net_roll',
|
||||
'net_anchor_01', 'net_anchor_02', 'net_anchor_03', 'net_anchor_04'] },
|
||||
{ name: 'hail_net_frame_01_wrecked', h: [2.70, 2.85],
|
||||
nodes: ['footings', 'legs', 'top_frame', 'net_down'] },
|
||||
{ name: 'pool_kit_01', h: [1.12, 1.32],
|
||||
nodes: ['pool_shell', 'pool_water', 'fence_rails', 'pool_gate',
|
||||
'fence_post_01', 'fence_post_14'] },
|
||||
@ -1436,5 +1446,313 @@ export default async function run(t) {
|
||||
'vacuous: no node-less declared anchors found across the shipped sites — this pin checked nothing');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SPRINT18 gate 4 — THE GLASSHOUSE TIER. Sprint 16 shipped the glass and
|
||||
// priced it at 320; the gate asked for the tier, and reading the gate found
|
||||
// that the price could not be REACHED. The comment two hundred lines up says
|
||||
// it in this file's own words: "nothing in the sim wrecks a glasshouse yet —
|
||||
// the value is inert data". main.js bills off a broken rig corner's baked
|
||||
// collateral string, and a glasshouse correctly offers no anchors, so the
|
||||
// most expensive object in the game could not cost anybody a cent.
|
||||
//
|
||||
// DESIGN.md line 179 named the missing asset: hail nets rigged OVER glass.
|
||||
// You tie to the frame that straddles it. The tier's teaching is new — the
|
||||
// frame's numbers are GOOD, and it is the most dangerous anchor in the game
|
||||
// because of where it stands. So the tell being pinned below is a POSITION,
|
||||
// measured off two GLBs, not a defect painted on one.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
t.test('the hail-net frame BRACKETS the glass — the tell is a position, and it is measured', () => {
|
||||
const frame = loaded.get('hail_net_frame_01');
|
||||
const glass = loaded.get('glasshouse_01');
|
||||
assert(frame && glass, 'hail_net_frame_01 or glasshouse_01 did not load');
|
||||
const f = new THREE.Box3().setFromObject(frame.scene);
|
||||
const g = new THREE.Box3().setFromObject(glass.scene);
|
||||
// Both assets are built at the origin, so a shared-origin placement is the
|
||||
// authored case and these comparisons are the real geometry.
|
||||
assert(f.min.x < g.min.x && f.max.x > g.max.x,
|
||||
`the frame spans x ${f.min.x.toFixed(2)}..${f.max.x.toFixed(2)} and the glasshouse `
|
||||
+ `${g.min.x.toFixed(2)}..${g.max.x.toFixed(2)} — the frame must STRADDLE the glass on x. `
|
||||
+ 'A frame that no longer brackets it is a frame whose anchors bill a building they are '
|
||||
+ 'not standing over, and the $320 becomes a number with no geometry behind it');
|
||||
assert(f.min.z < g.min.z && f.max.z > g.max.z,
|
||||
`the frame spans z ${f.min.z.toFixed(2)}..${f.max.z.toFixed(2)} and the glasshouse `
|
||||
+ `${g.min.z.toFixed(2)}..${g.max.z.toFixed(2)} — it must straddle on z as well; `
|
||||
+ 'legs standing ON the glass would be the joke version of this asset');
|
||||
assert(f.max.y > g.max.y + 0.35,
|
||||
`the frame heads at y=${f.max.y.toFixed(2)} and the ridge is at ${g.max.y.toFixed(2)} — `
|
||||
+ 'a net frame built to protect glass must CLEAR the ridge with room for the net to '
|
||||
+ 'drape, or it is resting on the thing it exists to keep hail off');
|
||||
// Every eye is above the ridge: the load path arcs OVER the glass, which is
|
||||
// the whole reason a failure here lands on panes.
|
||||
for (const n of ['net_anchor_01', 'net_anchor_02', 'net_anchor_03', 'net_anchor_04']) {
|
||||
const o = frame.scene.getObjectByName(n);
|
||||
assert(o, `${n} missing`);
|
||||
const p = new THREE.Vector3().setFromMatrixPosition(o.matrixWorld);
|
||||
assert(p.y > g.max.y,
|
||||
`${n} sits at y=${p.y.toFixed(2)}, below the ridge (${g.max.y.toFixed(2)}) — every corner `
|
||||
+ 'of this frame must be above the glass or the tier is not about the glass');
|
||||
}
|
||||
});
|
||||
|
||||
t.test('the frame\'s 20 m read is METALNESS, not lightness — S17\'s lesson, applied on purpose', () => {
|
||||
// S17 measured this the hard way on the corroded post: galvanised ships at
|
||||
// metalness 0.90, and with no environment map a near-mirror has nothing to
|
||||
// mirror and renders NEAR-BLACK (rgb 49,51,49 at 20 m). So every steel the
|
||||
// player is allowed to trust is dark at yard distance. This asset's entire
|
||||
// long-range tell is being the opposite of that, and the mechanism is
|
||||
// metalness — pin the mechanism, or the docstring gets "fixed" the wrong
|
||||
// way by whoever repaints it next.
|
||||
const pick = (asset, node) => {
|
||||
let m = null;
|
||||
loaded.get(asset)?.scene.getObjectByName(node)?.traverse((o) => { if (o.material) m = o.material; });
|
||||
return m;
|
||||
};
|
||||
const paint = pick('hail_net_frame_01', 'top_frame');
|
||||
const gal = pick('sail_post', 'post');
|
||||
assert(paint && gal, 'could not reach the frame paint or the honest post steel');
|
||||
assert(paint.metalness <= 0.15,
|
||||
`the frame paints at metalness ${paint.metalness} — powder-coat is PAINT, and above ~0.15 `
|
||||
+ 'it starts mirroring an environment that does not exist and goes dark like every other '
|
||||
+ 'steel. The tell dies silently: the frame is still white in the palette and black on screen');
|
||||
assert(gal.metalness >= 0.6,
|
||||
`the honest post's steel is at metalness ${gal.metalness} — the control for this comparison `
|
||||
+ 'is that trusted steel IS a near-mirror; if that changed, re-measure the whole claim');
|
||||
const lum = (c) => c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722;
|
||||
assert(lum(paint.color) > lum(gal.color) * 1.3,
|
||||
`frame paint luminance ${lum(paint.color).toFixed(3)} vs galvanised ${lum(gal.color).toFixed(3)} — `
|
||||
+ 'the powder-coat must also be the brighter base colour, so the two halves of the tell '
|
||||
+ 'agree instead of fighting');
|
||||
});
|
||||
|
||||
t.test('the hail-net frame is a NEW RUNG at 0.80: good steel, worst place, resolvable node-less', () => {
|
||||
const T = FACTORY_ANCHOR_RATINGS.types;
|
||||
const entry = T.hail_net_frame;
|
||||
assert(entry, 'hail_net_frame is not in the ratings manifest — the type cannot resolve '
|
||||
+ 'node-less, and a site declaring it plainly would play at the default hint (the '
|
||||
+ 'sail_post bug shape, on the highest-collateral anchor in the game)');
|
||||
assert(!FACTORY_ANCHOR_RATINGS.ambiguous.hail_net_frame,
|
||||
'hail_net_frame went manifest-AMBIGUOUS — its four eyes disagree, and the runtime will '
|
||||
+ 'refuse to guess; they are one frame and must bake one number');
|
||||
// Pinned against the manifest, never a literal — a literal would stay green
|
||||
// through a whole reshuffle of the ladder.
|
||||
assert(entry.rating_hint > T.pergola.rating_hint,
|
||||
`the frame rates ${entry.rating_hint} and the pergola ${T.pergola.rating_hint} — the frame `
|
||||
+ 'must out-rate it by a distance: a deck screwed to a house flexes, this is an engineered '
|
||||
+ 'portal on four footings. De-rating honest steel for looking dangerous is the corroded '
|
||||
+ 'post\'s lie inverted');
|
||||
assert(entry.rating_hint < T.post.rating_hint,
|
||||
`the frame rates ${entry.rating_hint} against the honest post's ${T.post.rating_hint} — at or `
|
||||
+ 'above the post there is no de-rate left for "32 mm sized for mesh, not for 25 m² of sail"');
|
||||
// THE FIELD THAT MAKES IT A TIER: what it takes with it is the GLASS.
|
||||
assert(entry.collateral === 'glasshouse',
|
||||
`the manifest says this type bills ${JSON.stringify(entry.collateral)} — it must bill `
|
||||
+ '"glasshouse". That string is the entire tier: the anchor is fine, the thing underneath '
|
||||
+ 'it is $320, and a node-less declaration has to carry that or the yard is free to break');
|
||||
assert(entry.sources.length === 4,
|
||||
`the manifest cites ${entry.sources.length} eyes; the frame has four corners and a net `
|
||||
+ 'ties at all of them — a missing source means a corner baked something different');
|
||||
});
|
||||
|
||||
// ⚠️ THE GATE, AS ONE MEASUREMENT. Before this sprint this assert was RED by
|
||||
// construction: no node anywhere in the palette named the glasshouse's key,
|
||||
// so the collateral apex was unbillable and the 320 was decoration. It is the
|
||||
// free-failure bug's mirror image — RULE 2 catches a key with no price, and
|
||||
// this catches a price with no key that can reach it.
|
||||
t.test('⚠️ the collateral APEX is reachable: some anchor in the palette can actually bill the glass', () => {
|
||||
const g = loaded.get('glasshouse_01')?.scene.getObjectByName('glasshouse_01');
|
||||
assert(g, 'glasshouse_01 root missing');
|
||||
const key = g.userData?.collateral_key;
|
||||
assert(typeof key === 'string', 'the glasshouse carries no collateral_key to reach');
|
||||
const billers = [];
|
||||
for (const [asset, o] of allNodes()) {
|
||||
const u = o.userData ?? {};
|
||||
if (u.collateral !== key) continue;
|
||||
if (typeof u.rating_hint !== 'number' || u.tie_off === false) continue;
|
||||
billers.push(`${asset}/${o.name}`);
|
||||
}
|
||||
assert(billers.length > 0,
|
||||
`NOTHING in the palette can bill "${key}" ($${g.userData?.collateral_value}). The apex is `
|
||||
+ 'priced and unreachable: main.js bills collateral off a BROKEN RIG CORNER\'s baked key, '
|
||||
+ 'a glasshouse rightly offers no anchors, so the most expensive thing in the game costs '
|
||||
+ 'nobody anything. It needs an adoptable anchor whose collateral names the glass — that '
|
||||
+ 'is what the hail-net frame is for (SPRINT18 gate 4)');
|
||||
// ...and the corollary that keeps gate 5 disarmed on my own asset: the
|
||||
// frame POINTS at the key and must never CLAIM it. structFor() returns the
|
||||
// FIRST structure matching a key, so a frame carrying collateral_key
|
||||
// "glasshouse" would, in any yard authored frame-first, bill $320 for the
|
||||
// panes and then wreck THE FRAME — the bill and the wreck agreeing with
|
||||
// each other and both wrong, which is the only kind of wrong this repo
|
||||
// cannot see from the outside.
|
||||
const claimants = [];
|
||||
for (const [asset, gltf] of loaded) {
|
||||
const root = gltf.scene.getObjectByName(asset);
|
||||
if (root?.userData?.collateral_key === key) claimants.push(asset);
|
||||
}
|
||||
for (const c of claimants) {
|
||||
assert(c.startsWith('glasshouse_01'),
|
||||
`${c} claims collateral_key "${key}", and it is not the glasshouse. Exactly one structure `
|
||||
+ 'may OWN a key — the one that owns the price. structFor() takes the first match, so a '
|
||||
+ 'second claimant means some yard bills the glass and wrecks something else');
|
||||
}
|
||||
assert(claimants.length > 0, `vacuous: nothing owns "${key}" at all`);
|
||||
});
|
||||
|
||||
t.test('the net-frame wreck folds at ONE end: keeps its height, carries no glass, offers nothing', () => {
|
||||
const w = loaded.get('hail_net_frame_01_wrecked');
|
||||
const i = loaded.get('hail_net_frame_01');
|
||||
assert(w && i, 'the net frame or its wreck did not load');
|
||||
const bw = new THREE.Box3().setFromObject(w.scene);
|
||||
const bi = new THREE.Box3().setFromObject(i.scene);
|
||||
assert(Math.abs(bw.max.y - bi.max.y) < 0.10,
|
||||
`the wreck tops out at y=${bw.max.y.toFixed(2)} vs the intact ${bi.max.y.toFixed(2)} — the +x `
|
||||
+ 'portal stands and holds the eave rails, so a portal frame folds at one end. A wreck lying '
|
||||
+ 'flat is claiming a failure mode this thing does not have');
|
||||
assert((bw.max.x - bw.min.x) > (bi.max.x - bi.min.x) + 0.6,
|
||||
`the wreck spans ${(bw.max.x - bw.min.x).toFixed(2)} m on x against the intact `
|
||||
+ `${(bi.max.x - bi.min.x).toFixed(2)} — the net has to end up OUT past the footing, or your `
|
||||
+ 'own debris is tidier than the storm\'s and the tier stops teaching what it is for');
|
||||
// No glass here. The glasshouse has its own wreck variant and swaps
|
||||
// independently; shards in both would draw twice the night both swap, and
|
||||
// would quietly claim the glass's failure as this asset's.
|
||||
const glassy = [];
|
||||
w.scene.traverse((o) => {
|
||||
const m = o.material;
|
||||
if (m && m.transparent === true && m.opacity < 0.6) glassy.push(o.name);
|
||||
});
|
||||
assert(glassy.length === 0,
|
||||
`the net-frame wreck ships translucent geometry on [${glassy.join(', ')}] — the glass belongs `
|
||||
+ 'to glasshouse_01_wrecked, which swaps on its own; shards in both double-draw');
|
||||
// No anchor survives the wreck (the fascia rule, sixth application).
|
||||
const offers = [];
|
||||
w.scene.traverse((o) => {
|
||||
if (typeof o.userData?.rating_hint === 'number' || o.userData?.anchor_type !== undefined) {
|
||||
offers.push(o.name);
|
||||
}
|
||||
});
|
||||
assert(offers.length === 0,
|
||||
`the wreck still offers [${offers.join(', ')}] — you cannot clip to a folded frame, and an `
|
||||
+ 'anchor that outlives its structure is the free-failure bug in a costume');
|
||||
const wr = w.scene.getObjectByName('hail_net_frame_01_wrecked');
|
||||
assert(wr?.userData?.broken_variant_of === 'hail_net_frame_01',
|
||||
'the wreck must name its intact twin so A can pair the swap');
|
||||
// UNPRICED BY RULING — the bike/jacaranda pattern. One broken corner can
|
||||
// only name one collateral key, and the player is being taught about the
|
||||
// glass. Splitting a bill between the frame and the panes with no mechanism
|
||||
// to split it is exactly the guess the invoice exists to refuse.
|
||||
assert(wr?.userData?.collateral_value === undefined,
|
||||
'the net frame is UNPRICED BY RULING — build the mechanism that can bill two things off '
|
||||
+ 'one broken corner before pricing the second one');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SPRINT18 gate 5 — THE SECOND-RUST-POST LANDMINE (D's filing, E+A).
|
||||
//
|
||||
// Two structures in one yard sharing a collateral key is currently ARMED and
|
||||
// not firing: world.js's structFor() returns the FIRST structure whose key
|
||||
// matches, main.js's billing loop dedupes by key, and C's scorecard envelope
|
||||
// dedupes by key again — so the second one is invisible to the bill and to
|
||||
// the wreck, while board.js's exposureOf() counts per STRUCTURE and prices
|
||||
// both. D measured the split on a real two-post yard: $115 on the offer card
|
||||
// against $45 on the failure route.
|
||||
//
|
||||
// The pool yard dodges it by shipping exactly one corroded post ON PURPOSE.
|
||||
// That is a yard author remembering, which is not a defence — and gate 4 just
|
||||
// made it likelier, because a glasshouse yard carries a structure that OWNS
|
||||
// the key and a frame whose anchors POINT at it. Two glasshouses with a frame
|
||||
// each trips this on the first night.
|
||||
//
|
||||
// So: the asset/manifest half is to make it impossible to SHIP quietly. The
|
||||
// runtime half (a per-instance key a site can override) is A's and the exact
|
||||
// wiring is filed in THREADS.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const shippedSites = [];
|
||||
for (const name of ['backyard_01', 'site_02_corner_block', 'site_03_swing_lawn',
|
||||
'site_04_pool_yard']) {
|
||||
shippedSites.push({ name, site: await loadSite(name) });
|
||||
}
|
||||
|
||||
t.test('⚠️ no shipped yard gives two structures the same collateral key, and ids are unique', () => {
|
||||
// Mirrors world.js structKey() exactly: site JSON wins, then the baked
|
||||
// GLB key, then the structure's own id as a last resort.
|
||||
const resolvedKey = (s) => {
|
||||
if (s.collateralKey) return s.collateralKey;
|
||||
const base = String(s.model ?? '').replace(/_v1$/, '');
|
||||
return loaded.get(base)?.scene.getObjectByName(base)?.userData?.collateral_key ?? s.id;
|
||||
};
|
||||
let checked = 0;
|
||||
for (const { name, site } of shippedSites) {
|
||||
const structs = site.structures ?? [];
|
||||
const ids = structs.map((s) => s.id);
|
||||
const dupeIds = ids.filter((v, i) => ids.indexOf(v) !== i);
|
||||
assert(dupeIds.length === 0,
|
||||
`${name}: duplicate structure ids [${[...new Set(dupeIds)].join(', ')}]. world.js keys its `
|
||||
+ 'structures Map by id, so the second one SILENTLY REPLACES the first — it never renders, '
|
||||
+ 'never bills, and never wrecks, and nothing anywhere says a word');
|
||||
const keys = structs.map(resolvedKey);
|
||||
const dupeKeys = keys.filter((v, i) => keys.indexOf(v) !== i);
|
||||
assert(dupeKeys.length === 0,
|
||||
`${name}: two structures resolve to the same collateral key `
|
||||
+ `"${[...new Set(dupeKeys)].join('", "')}" (keys: [${keys.join(', ')}]). structFor() `
|
||||
+ 'returns the FIRST match, so the bill and the wreck would both land on the wrong one, '
|
||||
+ 'agreeing with each other; meanwhile exposureOf() prices per structure and the offer '
|
||||
+ 'card would quote a total the failure route can never reach (D measured $115 vs $45). '
|
||||
+ 'Give each instance its own key in the site JSON — and until A lands the per-anchor '
|
||||
+ 'override, the anchors bake the shared key, so the second one cannot be made to bill '
|
||||
+ 'at all: see THREADS [E] SPRINT18 gate 5');
|
||||
checked += keys.length;
|
||||
}
|
||||
assert(checked > 0, 'vacuous: no structures found across the shipped yards');
|
||||
});
|
||||
|
||||
// The other half of the same seam, from the asset side: an anchor names the
|
||||
// collateral it TAKES, and in a real yard something has to own that name. The
|
||||
// hail-net frame is the first asset where those are different objects, so a
|
||||
// frame placed in a yard with no glasshouse would bill a building that is not
|
||||
// there — collateralFor() returns null and the corner is FREE. RULE 2 proves
|
||||
// the key is priced SOMEWHERE in the palette; this proves it is owned in
|
||||
// every yard that names it.
|
||||
t.test('⚠️ every collateral an authored anchor names is OWNED by exactly one thing in that yard', () => {
|
||||
const types = FACTORY_ANCHOR_RATINGS.types;
|
||||
let checked = 0;
|
||||
for (const { name, site } of shippedSites) {
|
||||
const owners = new Map();
|
||||
const own = (key, what) => {
|
||||
if (typeof key !== 'string') return;
|
||||
owners.set(key, (owners.get(key) ?? 0) + 1);
|
||||
void what;
|
||||
};
|
||||
const houseBase = String(site.house?.model ?? '').replace(/_v1$/, '');
|
||||
own(site.house?.collateralKey
|
||||
?? loaded.get(houseBase)?.scene.getObjectByName(houseBase)?.userData?.collateral_key,
|
||||
'house');
|
||||
for (const s of site.structures ?? []) {
|
||||
const base = String(s.model ?? '').replace(/_v1$/, '');
|
||||
own(s.collateralKey
|
||||
?? loaded.get(base)?.scene.getObjectByName(base)?.userData?.collateral_key ?? s.id, s.id);
|
||||
}
|
||||
// What every authored anchor on a structure will actually bill: the baked
|
||||
// node's key when it names a node, else the manifest's key for its type.
|
||||
for (const s of site.structures ?? []) {
|
||||
const base = String(s.model ?? '').replace(/_v1$/, '');
|
||||
for (const a of s.anchors ?? []) {
|
||||
const baked = a.node
|
||||
? loaded.get(base)?.scene.getObjectByName(a.node)?.userData?.collateral
|
||||
: types[a.type]?.collateral;
|
||||
if (typeof baked !== 'string') continue; // bills nothing, by design
|
||||
assert(owners.get(baked) === 1,
|
||||
`${name}/${a.id} will bill "${baked}" on failure, and ${owners.get(baked) ?? 0} things `
|
||||
+ 'in this yard own that key. Zero means collateralFor() returns null and breaking '
|
||||
+ 'this corner is FREE (the gutter bug, RULE 2\'s runtime twin); more than one means '
|
||||
+ 'structFor() picks the first and the wreck lands on the wrong structure');
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(checked > 0,
|
||||
'vacuous: no structure anchor in any shipped yard names collateral — this pin checked nothing');
|
||||
});
|
||||
|
||||
for (const { world } of siteWorlds) world.dispose();
|
||||
}
|
||||
|
||||
BIN
web/world/models/hail_net_frame_01_v1.glb
Normal file
BIN
web/world/models/hail_net_frame_01_v1.glb
Normal file
Binary file not shown.
BIN
web/world/models/hail_net_frame_01_wrecked_v1.glb
Normal file
BIN
web/world/models/hail_net_frame_01_wrecked_v1.glb
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user