F held the tag because my published atlas contract and my own loader were mutually
incompatible. That was mine. Closed, and published FIRST so G and E re-emit against it.
WHAT WAS WRONG: my R22 §7 invented `stock_shop_<godverseShopId>_index.json`; the loader
(stockpack.js:25) fetches `${base}stock_${type}_index.json`. An index named per the contract
can never be fetched by the code meant to load it. G built runtime-compatible (correct), E
built contract-compatible (also correct — the contract said so), and E's validator globbed
stock_shop_*_index.json -> zero files -> vacuous pass. Nobody was wrong; the contract was.
§7 REWRITTEN AGAINST THE CODE (LANE_C_PUB):
- Authority rule: stockpack.js IS the contract; doc vs loader disagreement = doc bug.
- Layout ratified as G shipped it: assets/stock_godverse/<godverseShopId>/stock_<type>_index.json
(`type` is load-bearing — SLOT_FOR picks the slot). stock_shop_* RETIRED.
-> E: the glob becomes stock_godverse/**/stock_*_index.json (the vacuous gate's actual cause).
- Runtime-read vs ignored stated exactly: reads atlases[] + items[{atlas,uv}] (uv top-left, the
loader flips V); ignores version/atlas_px/cell*/count/kind/tier/shop/crate/provenance.
- Provenance drift dies: runtime reads none of it, so the authority is E's validator + the 23
shipped town caches -> TOP-LEVEL `license` (US), attribution, generator, snapshot. British
`licence` retired; G lifts+renames those four, keeps its rich nested provenance{} as colour.
atlas_px stays REQUIRED (checked against real dims). cell* informational — C reads uv, never
cell; do NOT shrink the cell for VRAM (the dig renders covers close-up).
CODE (F's filed item): stockpack cache identity is now `${base}|${type}`;
getStockPack(type, base = DEFAULT_STOCK_BASE); new opts.stockBase on buildInterior = the seam F
wires. Omitted => town-wide packs, byte-identical to today. Pre-v5 callers untouched.
VERIFIED FRESH — and I fixed my own metric first: R23's "8/8 atlas-backed" only checked that a
map exists, but a parody canvas has a map too (same species as the vacuous gate). Re-asserted
falsifiably: every sleeve's texture source IS stock_godverse/3962749/stock_record_atlas_00/01.webp,
pack items == index items by id; negative control — the parody room's textures are (canvas, no src)
and touch the atlas zero times, so the metric discriminates. Collision FIXED (shop 120 + town 350
coexist). Fail-soft FIXED with a populated cache (missing base -> null -> parody). 98 draws with
real covers, pathOK, 0 carves, 0 console errors.
SCOPED NOT SLID: the LRU+dispose (§7.6) lands at v5.0-beta — one stocked shop can't accrete, and
safe eviction needs a room-exit hook (a live room holds the pack's shared material) that arrives
with F's per-shop preload. Written into the contract so it can't slide silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
925 lines
68 KiB
Markdown
925 lines
68 KiB
Markdown
# LANE C — INTERIORS · progress (PROCITY-C)
|
||
|
||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||
|
||
Last updated: 2026-07-17 (round 24) · owner: PROCITY-C · reviewer: Fable
|
||
|
||
---
|
||
|
||
## Update 2026-07-17 (round 24, THE ALPHA COMPLETION) — the contract fix (ledger #1, the root cause)
|
||
|
||
R24 §Lane C. **The root cause was mine and it's closed.** F held the tag because my published atlas
|
||
contract and my own loader were mutually incompatible; the ruling — **the runtime is the authority** — is
|
||
now the section's first line. Published FIRST (half-day rule; G and E re-emit against it).
|
||
|
||
**What was actually wrong.** My R22 §7 invented `stock_shop_<godverseShopId>_index.json`. The loader
|
||
(`stockpack.js:25`) fetches `` `${base}stock_${type}_index.json` ``. **An index named per my contract can
|
||
never be fetched by my own code.** So G built runtime-compatible (correct), E built contract-compatible
|
||
(also correct — the contract said so), and E's validator globbed `stock_shop_*_index.json` → **zero files
|
||
→ vacuous pass**. Nobody was wrong; the contract was. I wrote it, so I own it.
|
||
|
||
**§7 rewritten against the code (LANE_C_PUB):**
|
||
- **Authority rule:** `stockpack.js` IS the contract; if the doc and the loader disagree, the doc is a bug.
|
||
- **Layout ratified as G shipped it:** `assets/stock_godverse/<godverseShopId>/stock_<type>_index.json`.
|
||
`type` is load-bearing (it picks the slot via `SLOT_FOR`: record→sleeve). `stock_shop_*` is **retired**
|
||
→ **E: the validator glob becomes `stock_godverse/**/stock_*_index.json`** (that's the vacuous gate's
|
||
actual cause).
|
||
- **Runtime-read vs runtime-ignored, stated exactly.** Reads `atlases[]` + `items[{atlas, uv}]` (uv origin
|
||
**top-left**; the loader flips V — don't pre-flip). Ignores `version/atlas_px/cell*/count/kind/tier/shop/
|
||
crate/provenance` entirely.
|
||
- **The provenance drift dies (§7.3):** since the runtime reads none of it, the authority is E's validator
|
||
+ the 23 shipped town caches — both use **top-level `license` (US), `attribution`, `generator`,
|
||
`snapshot`**. British `licence` retired; **G lifts+renames those four**, keeps its rich nested
|
||
`provenance{}` (source/sha256/determinism) as colour. `atlas_px` stays REQUIRED (E checks it against real
|
||
dims — a genuine falsifiable check). `cell*` informational: **C reads `uv`, never `cell`** — and don't
|
||
shrink the cell for VRAM, the dig renders covers close-up.
|
||
|
||
**Code landed (the `type+base` keying, F's filed item):**
|
||
- `stockpack.js`: cache identity is now `` `${base}|${type}` ``; `getStockPack(type, base = DEFAULT_STOCK_BASE)`.
|
||
- `interiors.js`: new **`opts.stockBase`** — the seam F wires (`buildInterior(shop, THREE, { stock:'real',
|
||
stockBase: 'assets/stock_godverse/<id>/' })`). Omitted ⇒ town-wide packs, byte-identical to today.
|
||
- Pre-v5 callers untouched (`getStockPack('record')` still means the town pack).
|
||
|
||
**Verified fresh — and I had to fix my own metric first.** R23 I claimed "8/8 atlas-backed"; that checked
|
||
`material.map` exists — **but a parody canvas has a map too**. That's the same species as the vacuous gate
|
||
I'd just been handed. Re-asserted falsifiably:
|
||
- **Every sleeve samples G's atlas FILE** — texture sources are exactly `stock_godverse/3962749/
|
||
stock_record_atlas_00.webp` / `_01.webp`; pack items ≡ index items **by id**. Titles are real
|
||
(*Yoshitoshi Artists – In House We Trust / Deep Dish – Penetrate Deeper*).
|
||
- **Negative control (what makes it a gate):** the parody room's stock textures are `(canvas, no src)` and
|
||
touch the godverse atlas **zero** times — the metric discriminates.
|
||
- **Collision FIXED:** shop pack (120 items) + town pack (350) coexist as distinct objects.
|
||
- **Fail-soft FIXED with a populated cache:** missing base ⇒ `null` ⇒ parody (the R23 correctness bug —
|
||
an unstocked shop inheriting a cached crate — is gone).
|
||
- Ceilings: **98 draws** with real covers via `opts.stockBase`; `pathOK`; 0 carves; **0 console errors**.
|
||
|
||
**Scoped, not slid:** the **LRU + dispose** (§7.6) lands at **v5.0-beta**, not here — with one stocked shop
|
||
there's no accretion, and safe eviction needs a room-exit hook (a live room holds the pack's shared
|
||
material), which arrives with F's per-shop preload. Written into the contract so it can't slide silently.
|
||
|
||
**→ F:** the seam is `opts.stockBase`; §7.5 has the call. **→ G:** layout ratified, re-emit provenance field
|
||
names only (§7.3). **→ E:** the glob (§7.1) is the vacuous gate's cause.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 23, v5.0-alpha THE FIRST REAL CRATE) — the crate in the room (ledger #3)
|
||
|
||
R23 §Lane C. **Verdict: the contract holds — G's atlas needs no re-emit.** Verified against G's
|
||
`stock_godverse/3962749/` (120 real records from Monster Robot Party's DEEP HOUSE crate 550, 2 × 2048²,
|
||
cell 256²) in a fresh context. Ran **before** Fable's merge deliberately — if the contract failed, that
|
||
blocks the merge. It didn't.
|
||
|
||
**PASS — the first real crate renders in a PROCITY room:**
|
||
- **Resolves through the existing `stockAdapter` path** via a base override — no C code change needed for
|
||
the single-shop alpha. 120 items · 2 atlases · **8/8 stock meshes atlas-backed** (real covers). The
|
||
sleeves are legible on screen: *peperecordings · DEEPCHILD · "39" · mff FREAKS · The Angels Instinct feat
|
||
Charlie Ashcroft* — a real deep-house crate.
|
||
- **Ceilings hold:** **118 draws** with real covers (≤350 law), `pathOK`, 0 carves.
|
||
- **Fail-soft holds** (clean cache): missing atlas → `null` → parody (20 parody meshes, 110 draws); no
|
||
adapter (the `?noassets` shape) → parody, zero fetch. **0 console errors.**
|
||
- **G's index is compliant:** `items[]` sorted by id (my R22 determinism ask honoured), provenance present,
|
||
`uv ⊂ [0,1]`, 2 atlases = exactly my cap, `cell_w/cell_h` harmless (C reads `uv`, never `cell`).
|
||
|
||
**I got a number wrong, and the first real data caught it (LANE_C_PUB §7.2 CORRECTED).** My R22 ceiling
|
||
said "≤8 MB decoded" — derived from assuming a "typical shop" is ~16 items @1024². **A real record shop is
|
||
120 records.** At the contract's own 256² cell that's 2 × 2048² ≈ **32 MB**, 4× my figure. The *structural*
|
||
rules held (≤2 atlases; 2048² for 60+ items — G landed exactly on the cap); only the MB number was wrong,
|
||
because it described a shop that doesn't exist. **Corrected:** ≤2 atlases/shop @≤2048², **LRU = 1 shop's
|
||
atlas set resident ⇒ ≤32 MB peak** (still bounded; still far better than the 181 MB unbounded accretion E
|
||
measured). And explicitly: **don't shrink the cell below 256² to save VRAM** — the dig shows a cover
|
||
close-up; resolution is the one thing the player actually sees.
|
||
|
||
**G's ask #1 CONFIRMED — and worse than reported (LANE_C_PUB §8: C's spec, C's fix).** `stockpack.js` keys
|
||
`_packs`/`_resolved` by **`type` alone**, so `base` is **ignored on a cache hit**. Measured:
|
||
`preloadStockPack('record', {base:'…/3962749/'})` then `preloadStockPack('record', {base:'assets/models/'})`
|
||
**returned the identical object**. Consequences: (1) two stocked shops collide — the 2nd serves the 1st's
|
||
crate (blocks v5.0-beta, as G said); (2) **it breaks the fail-soft law** — an *unstocked* shop of a cached
|
||
type inherits that crate instead of parody. The second is a correctness bug, not a mix-up. Seam spec
|
||
published (cache key = `type + base`; per-shop preload on entry; LRU=1 + dispose; **C consumes whatever
|
||
identity field lands — C does not invent it**). **C owns and lands it** — the §7 promise coming due.
|
||
|
||
**Two gaps block F's gate #7a (neither is C's; both flagged):** (1) **the crate has no street** —
|
||
`redhill_real` doesn't exist and monsterrobot isn't in any town's `shops[]` (G's ask #2 → E/Fable);
|
||
(2) the shell's `interior_mode.js:192` calls `getStockPack(shop.type)` with `base` fixed to
|
||
`assets/models/`, so it can't reach a per-shop pack until §8's seam + the identity field land (C+F).
|
||
|
||
*Process note:* the shared tree was checked out on `lane-G-godverse` for Fable's review, so I committed
|
||
these C docs from a temporary `git worktree` on `main` — the shared checkout was never touched, and G's
|
||
branch keeps the disjoint namespace the two-studio merge law depends on.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 22, v4.0 THE EPOCH CLOSE) — 23-town door-soak (#5) + the G2a contract (#3)
|
||
|
||
R22 §Lane C, both items. Wave 3 — ran **after A pinned the 36 goldens** (`3face97`) and E's tri-diet fix
|
||
(`d81e499`), so this soaks the final v4.0 world.
|
||
|
||
### Ledger #5 — sampled door-soak across all 23 towns
|
||
|
||
The five-town harness scaled: **~10 doors/town, sample takes one of every type present then stride-fills**, so
|
||
every archetype is covered town-set-wide. **238 doors · 1,133 in-plan shops · 23 towns.**
|
||
|
||
| town | shops | doors | worst draws | | town | shops | doors | worst draws |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| adelaide | 110 | 11 | 158 | | katoomba | 72 | 10 | 149 |
|
||
| ballarat | 19 | 10 | 154 | | launceston | 30 | 10 | 137 |
|
||
| bendigo | 35 | 10 | 141 | | marrickville | 26 | 10 | 149 |
|
||
| bowral | 30 | 10 | 156 | | newcastle | 34 | 10 | 152 |
|
||
| braddon | 19 | 10 | 158 | | **newtown_godverse** | 18 | 10 | 134 |
|
||
| brunswick | 92 | 11 | 149 | | newtown | 67 | 11 | 149 |
|
||
| castlemaine | 23 | 10 | 150 | | northbridge | 58 | 11 | 149 |
|
||
| darwin | 12 | 10 | 154 | | toowoomba | 12 | 10 | 138 |
|
||
| daylesford | 26 | 10 | 153 | | westend | 41 | 10 | 146 |
|
||
| **fitzroy (mecca)** | **139** | 10 | **159** | | fremantle | 79 | 11 | 152 |
|
||
| geelong | 62 | 11 | 145 | | glebe | 51 | 11 | 151 |
|
||
| hobart | 78 | 11 | 157 | | | | | |
|
||
|
||
**Verdict: 23/23 PASS — 0 throws · 0 carves · 0 path-fails · 0 over-budget; worst 159 draws (fitzroy) ≤350.**
|
||
- **All 12 archetypes covered town-set-wide** (band_room, book, dept, milkbar, opshop, pawn, pub, record, rsl,
|
||
stall, toy, video) — the whole registry.
|
||
- **23/23 towns landed 3 venues** — A's cluster bias + district placement hold from the 12-shop thin tail
|
||
(darwin, toowoomba) to the 139-shop mecca (fitzroy). Fitzroy, 2× anything absorbed before, is the *cheapest*
|
||
end of nothing — worst draw 159, same band as everywhere else: interiors don't care about town scale.
|
||
- **Lane G's `newtown_godverse` passes** (18 shops, 8 types, worst 134) — the merged town needs nothing from C.
|
||
|
||
### Ledger #3 — the G2a per-shop atlas contract (C's half) → published in LANE_C_PUB **§7**
|
||
|
||
Answered Fable's priors + E's half. **All three priors hold; atlas-per-shop is strictly better for draws than
|
||
today's type-packs. The binding constraint is VRAM, not draws — and E's dispose ask is granted: it's my seam.**
|
||
- **Draws:** 1 atlas/shop = **1 draw** (shared material + baked UV → batch.js merges). Rule: 1, hard cap 2;
|
||
60+ item shops spill to the type-pack. Headroom is enormous (stock rooms 41–58 draws vs the ≤350 law).
|
||
- **VRAM (the real ceiling):** E's 181 MB audit is right and the cause is my own design note
|
||
(`stockpack.js:11` caches atlases "NOT per-room, like depot GLBs") — correct for 3 bounded type-packs,
|
||
**wrong for unbounded per-shop atlases**. Ruling: **≤2 resident shop atlases, ≤8 MB decoded, ≤1024² typical**
|
||
(2048² only for 60+ items); **C lands the LRU cap + dispose in `stockpack.js`**. Scheduling: the accretion is
|
||
pre-existing and `?stock=real` is **opt-in** (`index.html:196`) so it can't touch the default boot — **not a
|
||
v4.0 blocker**; I land it as **C's first v4.x item, before G2a ships**.
|
||
- **Schema:** E's index confirmed verbatim, keyed per shop; UV origin stays top-left (C flips V); C reads `uv`
|
||
never `cell` (so book's 176×256 portrait is fine). **Fail-soft/parody/`?noassets`:** unchanged law.
|
||
- **One ask back to G:** emit `items[]` in **deterministic order (sort by id)** — C's seeded per-bin pick
|
||
indexes into it, so a re-emit would silently reshuffle every crate.
|
||
|
||
**C→F: 23-town interior verdict GREEN** (nothing gates the tag from C). Docs-only. Both C items closed for v4.0.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 21, v4.0-beta close) — the five-town density door-soak (ledger #3)
|
||
|
||
R21 §Lane C (wave 2 — ran **after A's venue cluster-adjacency bias** landed, `110c828`, so the soak reads the
|
||
relocated world + A's re-pinned goldens). The carried R20 #6: **every one of the five beta towns at its new
|
||
density**, deterministically sampled ~25 doors/town — the sample takes **one of every type present** then
|
||
stride-fills, so the texture classes' first real appearance (milkbar/dept/stall on real shops) is always covered.
|
||
|
||
| town | shops (in plan) | sampled | venues | types (all sampled) | throws | carves | path-fails | over-budget | worst draws |
|
||
|---|---|---|---|---|---|---|---|---|---|
|
||
| `katoomba_real` | 72 | 25 | 3 | 10 | 0 | 0 | 0 | 0 | **157** |
|
||
| `newtown_real` | 67 | 25 | 3 | 11 | 0 | 0 | 0 | 0 | 155 |
|
||
| `fremantle_real` | 79 | 25 | 3 | 11 | 0 | 0 | 0 | 0 | 153 |
|
||
| `bendigo_real` | 35 | 25 | 3 | 10 | 0 | 0 | 0 | 0 | 154 |
|
||
| `castlemaine_real` | 23 | 23 (all) | 3 | 8 | 0 | 0 | 0 | 0 | 156 |
|
||
|
||
**123 doors · 0 throws · 0 carves · 0 path-fails · 0 over-budget · worst 157 draws (≤350).** In every town
|
||
`typesInTown === typesSampled` — every archetype present was entered, including all three venue kinds
|
||
(pub/band_room/rsl) and the texture classes. **All five towns landed 3 venues** → A's cluster bias + district
|
||
placement hold at density.
|
||
|
||
**§6 mapping verified live (John's SUBTLE directive, working by construction):** katoomba's cache is **20
|
||
heroes + 60 texture** — exactly E's 3× cap, heroes untouched. In-plan (after A's counted drops): 17 heroes
|
||
(opshop 10 · book 4 · record 2 · video 1) + 52 texture (milkbar 29 · dept 18 · stall 5) + 3 venues = 72.
|
||
The texture classes' **first real appearance** renders right: **"Blue Mountains Chocolate Company"** — a real
|
||
Katoomba `shop=chocolate` — now wears `milkbar` (70s geo wallpaper, cube shelves of price-tagged goods, counter,
|
||
wall rack): a believable confectionery counter shop. The widening reads as texture, not takeover.
|
||
|
||
*(Cache→plan deltas — e.g. katoomba 80→72 — are A's overflow/fragmentation drops, counted by cause in A's R20
|
||
accounting; not an interior concern, and every surviving door builds clean.)* **C→F: density verdict GREEN.**
|
||
Docs-only. C otherwise on-call; my §6 mapping + this soak close both C items for the beta.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 20, v4.0-beta FIVE TOWNS) — the class→archetype mapping (ledger #2, C's half)
|
||
|
||
R20 §Lane C (I lead — E builds to it, half-day rule): **published the OSM shop-class → interior archetype
|
||
mapping** in [LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) **§6** (new v4.0-beta amendment; the frozen venue
|
||
contract §0–§5 untouched). This is the density widening — real towns fetch **general retail**, not just the
|
||
thriftgod secondhand classes, so the main street holds its real bakery/cafe/chemist/clothes shops.
|
||
|
||
**Design (John's directive: SUBTLE — texture, not takeover):**
|
||
- **No new archetypes.** General retail wears **existing** recipes: food/grocer/newsagent/chemist → `milkbar`
|
||
(the canonical AU counter shop); clothes/hardware/big-retail → `dept`; produce/florist → `stall`.
|
||
- **Secondhand stays the hero** — charity/antiques→opshop, books→book, music→record, video/games→video,
|
||
pawn→pawn, toys→toy; never subsampled.
|
||
- **Unknown/unlisted → `opshop`** (R18-proven safe). Included a copy-paste JSON map for E's `build_towns.py`
|
||
+ a tasteful fetch list (skip vehicle/funeral/industrial/pure-services like hairdresser/bank).
|
||
- **Guardrail for E:** if a town's general retail would visually drown the secondhand strip, seed-subsample
|
||
the texture classes toward ~2–3× the secondhand count and count the drops in `norm.dropped`.
|
||
|
||
**Mechanic note:** `plan_osm` keeps only registry types (unknowns flatten to opshop), so E bakes a **registry
|
||
type** per the map into each cache's `shop.type` — no C code change, all targets are existing recipes/registry
|
||
entries, budgets unchanged. **C→E handshake: §6 is live.**
|
||
|
||
**Pre-validated the mapping (measurement over folklore, before E builds to it):** milkbar/dept/stall/opshop all
|
||
build valid at the katoomba lot (8.5×14) — 0 carves, ≤143 draws, pathOK. Rendered `dept` (my one flagged case
|
||
— the escalator centrepiece): reads as a clean **dressed-retail floor** (cube shelves of goods + clothes rack +
|
||
counter + "GROUND FLOOR/HABERDASHERY" signage) in the 7.9×11.7 hall, escalator does *not* dominate — so
|
||
`clothes`/`hardware` → `dept` is confirmed, not a concern.
|
||
|
||
**Pending (ledger #6, after E re-emits the 5 widened caches):** the five-town sampled door-soak — every town,
|
||
sampled doors at the new density, all types resolve, no carve, budgets; one table, five rows. I'll watch that
|
||
`dept` reads right on small real lots (the escalator centrepiece). Docs-only this commit; door-soak follows E.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 19, v4.0-alpha CLOSE) — re-soak katoomba on REAL roads (against A's in-flight pass)
|
||
|
||
R19 §Lane C (on-call, ledger #5): E's roads landed (`dbb52c2` — katoomba_real is now **schema v2, 484 real
|
||
roads**), so I re-ran the 20-door soak against the real street graph — closing the R18 "re-run when roads land"
|
||
caveat. **Honest caveat: the working tree also carried Lane A's UNCOMMITTED R19 fragmentation pass** (plan_osm.js
|
||
WIP — `JOIN_TOL`/`BRIDGE_MAX`, junction-protected Douglas–Peucker, shopless-island cull / shop-island bridge),
|
||
so the dev server served A's *in-progress* finalization, not a frozen state. Read this as "interiors survive
|
||
A's real-graph pipeline as it stands mid-round"; a clean confirm follows A's commit.
|
||
|
||
**Robust result (holds whatever A's WIP does):** every shop the plan produced builds a **valid interior** —
|
||
0 throws · 0 carves · 0 path-fails · 0 over-budget · worst **147** draws (min 115); 3 gig venues
|
||
(pub/band_room/rsl — **district placement holds on the real graph**); plan deterministic (byte-identical
|
||
re-gen). Lot dims **unchanged** (7.5–8.5 × 14) → interiors are dim-identical to R18 (road-agnostic confirmed;
|
||
my R18 degenerate-lot proof already spans anything the real graph can seat).
|
||
|
||
**Shop count observed = 19 (NOT filed as a defect).** The plan yielded 19 shops here, but against A's in-flight
|
||
fragmentation code that's expected, not a bug — A's ruling explicitly culls shopless islands and bridges/
|
||
drops-with-count shop-bearing ones, so the final count is A's to pin (could land at 20 connected or an explicit
|
||
19 with a `norm.dropped` count). I'm not attributing it to A as a finding; I only confirm interiors build clean
|
||
whichever way it resolves. (If A's committed pass surprises with a shop that *can't* build an interior, that
|
||
would be the finding — none did in this run.)
|
||
|
||
**Final C spot-confirm pending A's COMMITTED finalization + golden pin** (per Fable's sequencing, C confirms
|
||
after A). When A pins `katoomba_real`'s real-roads golden I'll re-run once against the frozen state. Docs-only
|
||
round for C.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 18, v4.0-alpha REAL ROADS) — interior spot-soak of katoomba_real (ledger #6)
|
||
|
||
R18 §Lane C (small): enter every `katoomba_real` shop and assert valid interiors + no carve + budgets.
|
||
Built the town plan the way the shell does — `registerTownCache(katoomba_real)` → `generatePlanFor(seed,
|
||
'osm', {town, gigs:true})` (gigs on = the default boot) → `buildInterior` per shop (lot dims resolved from
|
||
`plan.lots`). **All 20 doors PASS:**
|
||
|
||
| metric | result |
|
||
|---|---|
|
||
| shops entered | **20 / 20 built** |
|
||
| throws · carves · path-fails · over-budget | **0 · 0 · 0 · 0** |
|
||
| worst draws | **153** (shop 16, opshop/hall) ≤ 350 |
|
||
| types resolved | opshop · book · record · video + venues pub · rsl · band_room |
|
||
| gig venues (default boot) | 3 (ids 3/5/19) — all built valid gig interiors (stage + watchPoints) |
|
||
| draw range | 108–153 |
|
||
|
||
**Degenerate-lot stress (preempts A's real-road re-seating):** 32 combos (8 extreme lots {2×3, 1×1, 3×30,
|
||
40×4, 100×100, **0×0**, 4.2×4.1, 2.5×18} × 4 types incl. venues) → **0 throws · 0 carves · 0 path-fails**;
|
||
every room floors at **≥4×4 m** (`computeDims Math.max(4, lot−0.6)`). So whatever frontage the real-graph
|
||
lift seats shops on, the interior contract absorbs it.
|
||
|
||
**Caveat (honest):** audited against the CURRENT `katoomba_real.json` — schema v1, `roads: none`, so it boots
|
||
the **marched fallback** (E's katoomba roads cache isn't committed yet; A's lift `a78dc76` is in). Interiors
|
||
are **road-agnostic** (they consume lot dims + type + seed, never road geometry), and the degenerate-lot proof
|
||
covers any lot the real graph produces, so the conclusion is stable. **Interior lane contributes 0 failures to
|
||
the alpha's failure list.** A one-line re-run once E's `roads[]` cache lands (lot dims may shift with real
|
||
frontage) — I'll re-confirm if asked, but the interior contract already absorbs the range. C otherwise on-call.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 17, v3.2 THE TAIL) — backline handshake CLOSED + real-town type fallback
|
||
|
||
R17 §Lane C (ledger #3): close the R16-pending render-verify of D's wired amp. D landed the wiring
|
||
(`ab7a8d0`): `band.js _addBackline` reads `stage.backline[0]` with an immediate primitive `ampPrim()` fallback
|
||
(asset-free) + GLB upgrade. **Handshake CLOSED — verified fresh (seed 20261990), 3 archetypes × 2 modes:**
|
||
- amp plants at `backline[0]` **exactly** (offset 0.00 GLB / 0.01 prim — rounding); **1 amp per stage** (no
|
||
more C primitive → the double-amp is gone).
|
||
- **no interpenetration** — amp↔nearest-band AABB edge gaps all positive: pub **0.40/0.44 m**, band_room
|
||
**0.27/0.29 m** (tightest; the bass extends downstage away from the up-stage amp), rsl **0.54/0.57 m**
|
||
(GLB/primitive). Rendered band_room gig confirms the read (amp up-stage-right behind the bassist, clear).
|
||
|
||
**On-call (proactive, for A/E's real-map scout):** confirmed C's interior type fallback eats real OSM shop
|
||
types. 18 real `shop=` values (hairdresser/bakery/convenience/supermarket/chemist/butcher/… + ''/null/
|
||
undefined/`SHOP`/`Op_Shop`/`antiques`) all build with **0 throws, all pathOK**, resolving to `opshop`
|
||
(theme.js `canonicalType`) or a mapped alias — so a player entering any real-town shop gets a valid interior.
|
||
Venue types (pub/band_room/rsl) are recognized; everything else degrades to opshop. No C change needed for v4.
|
||
|
||
Docs-only round (verification confirmed shipped behavior). C otherwise on-call.
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 16, v3.1 THE FLIP) — the backline handoff (C's half of ledger #5)
|
||
|
||
R16 §Lane C (ledger #5, C's half): unify the amp. **`room.stage.backline[]`** now exposes 2 seeded up-stage
|
||
amp slots `{x,z,ry,y}` (pure data, no meshes), and C's **primitive ampStacks are dropped** from `placeStage` —
|
||
Lane D plants E's `guitar_amp` GLB at `backline[0]` (its own primitive fallback under `?noassets`), one amp
|
||
system. Shape published in [LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) §3 (v3.1 amendment marker; rest of the
|
||
frozen doc unchanged). **C→D handshake: backline shape is live.**
|
||
|
||
- `backline[0]` = stage-right `(stage.x + stage.w·0.36, deckY, stage.z − stage.d·0.22)` == D's R15-verified
|
||
amp pose exactly, so wiring D's plant to it moves **nothing** on screen. `backline[1]` = stage-left spare
|
||
(mirror). ry=π, y=deckY.
|
||
- **Verified fresh (port-isolated, seed 20261990):** both slots up-stage, on-deck, clear of all 4 bandPoses
|
||
(nearest-bass **0.87 m pub / 0.70 m band_room / 0.92 m rsl**) and all watchPoints (≥1.95 m); determinism
|
||
byte-identical (placement + stage + watchPoints); 0 leftover ampStack prop groups; drawSweep glb-off venues
|
||
**pub 122 · band_room 120 · rsl 131** (was 126/124/135 — the dropped primitives shaved ~4 draws each), worst
|
||
161 (opshop/hall, unchanged). ≤350 ✓.
|
||
- **Determinism/golden safety:** removing the ampStack loop only drops draws on the `stage` rng sub-stream,
|
||
which nothing downstream reads; the `placement` snapshot excludes ampStacks (never tracked), so it's
|
||
byte-identical. Interiors aren't hashed into any golden; the `?classic=1` boot has no venues → untouched.
|
||
- Disjoint from F's flip (interiors don't read flags) — landed early so D isn't blocked. **Verify with D
|
||
once wired** (ledger #5 D-half): re-render each archetype, confirm the GLB at `backline[0]` doesn't
|
||
interpenetrate (pending D's R16 commit).
|
||
|
||
**Code:** `layout.js` placeStage (backline[] + drop the amp loop), `interiors.js` API doc. `theme.js` untouched
|
||
(recipes carry no ampStack fitting — the primitives were placeStage-planted, not recipe fittings).
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 15, v3.0: closing the release) — verified D's amp fix
|
||
|
||
R15 §Lane C (one item): verify D's `guitar_amp` reposition (ledger #4). D moved the plant (commit `66aec0f`)
|
||
to C's §3 corrected pose exactly — `band.js:236 amp.position.set(stage.x + stage.w·0.36, deckY, stage.z −
|
||
stage.d·0.22)`. **Verified (fresh port-isolated context, seed 20261990, D's real GigCrew + GLB backline):**
|
||
matches §3 (both venues); **bass clearance 0.87 m (pub) / 0.92 m (rsl)** — the R14 interpenetration (0.29 m)
|
||
is gone; on-deck within footprint; flanks C's primitive ampStack (opposite side, 4.4 m pub / 3.5 m rsl) as a
|
||
clean up-stage backline; no clash with band/crowd/watchPoints. Rendered pub gig confirms the read (amp up-stage
|
||
behind the bassist, primitive up-stage-left, band clear). LANE_C_PUB §3 updated RESOLVED. Freeze otherwise
|
||
stands; no other C change this round. Nothing else blocks on C — F runs the release (continuity wiring + smoke,
|
||
alias deletion, week soak, tour, tag `v3.0`).
|
||
|
||
---
|
||
|
||
## Update 2026-07-16 (round 14, v3.0: the release) — venue audit + docs freeze (light round)
|
||
|
||
R14 §Lane C: interior release audit, the `guitar_amp` ruling, and the docs freeze. C's interior code was
|
||
**unchanged** since R13, so venue geometry is stable by construction; the audit re-verified it against the
|
||
current tree (D's R13 continuity + RY-fix + real instrument GLBs landed) and froze the contracts. Ran an
|
||
adversarial audit **workflow** (10 agents: conformance + D-integration/amp + docs-freeze edit list, then a
|
||
verify pass) — it earned its keep (caught the amp defect I'd first waved through + two stale comments).
|
||
|
||
**Empirical re-verify (fresh context, port-isolated server):**
|
||
- Venue contract **byte-identical to R13**: caps 8/8/12, deckY 0.32/0.20/0.40, riserY 0.48/0.36/0.56, gigKeys
|
||
gig-pubrock/grunge/covers, quiet-night = no gigKey, determinism true.
|
||
- `drawSweep` glb-off: band_room 124 · pub 126 · rsl 135 (worst 161 opshop/hall) ≤350 — unchanged.
|
||
- **Gig-night draws with D's real `GigCrew` + GLB instruments** (localdepot): pub **60** · band_room **52** ·
|
||
rsl **71** ≤350. Asset-free worst RSL 237. All 4 instruments load (guitar/bass/mic/drums).
|
||
- **Release-law confirmed** (workflow, CONFIRMED): D's continuity + the RY fix ride a disjoint rng namespace
|
||
(`rng(citySeed,kind,id)`) from C's `ctx.stream(salt)`; the RY fix is a constant Y-rotation in `buildFigure`
|
||
(no rng). Cannot perturb C's placement or any golden.
|
||
|
||
**`guitar_amp` ruling (debt for C) → Lane D:** **NOT blessed as-is — reposition up-stage.** D plants the GLB
|
||
at `(stage.x+stage.w·0.32, deckY, stage.z+0.15)` — that `+0.15` is *down-stage*, landing the 0.5 m amp 0.29 m
|
||
from the bass rig (**interpenetrates the bassist**, confirmed by render + 3 audit agents), and it's redundant
|
||
with C's own primitive ampStacks (`placeStage` plants 1–2 up-stage at `(±stage.w·0.28, deckY, stage.z −
|
||
stage.d·0.18)`). Corrected pose in [LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) §3: move up-stage-right ≈
|
||
`(stage.w·0.36, deckY, stage.z − stage.d·0.22)`, clear of the bass and offset from C's primitives → a clean
|
||
flanking backline. Full amp unification (C exposes `stage.backline[]`, GLB replaces primitive) = v3.1.
|
||
Cosmetic + `?gigs=1` opt-in → does not block the tag.
|
||
|
||
**Tri observation → E/F (not a C gate):** E's decimated instrument GLBs are **14k tris each** (5 on a stage =
|
||
70k; peds ~2.7k each), so a settled RSL gig-night interior is ~**182k tris**. The interior gate is *draws*
|
||
(passes at ≤71); the 200k tris is the *street* budget, so no violation. But F's gig-night smoke reports ~34k
|
||
because it measures **before** the async instrument GLBs load — the real settled scene is ~5× heavier. A
|
||
camera-distance instrument LOD is a v3.1 candidate.
|
||
|
||
**Docs freeze (v3.0-FROZEN markers added):**
|
||
- `LANE_C_PUB.md` — retitled + frozen marker; §0 ry convention reframed past-tense (the +Z fleet was fixed at
|
||
the source in D's `rigs.js buildFigure`, R13 — no per-consumer flips); 4-piece stated as shipped (veto
|
||
language removed); the amp ruling added (§3); budgets updated to R14; in-flight handoffs removed.
|
||
- `LANE_C_AUDIO.md` — frozen marker + a scope note (this covers base interior audio; gig audio `gigKey`
|
||
lives in LANE_C_PUB); the deferred `audioEmitter` idea re-targeted to v3.1.
|
||
|
||
**Code (comment-only, zero runtime/golden/rng impact):** fixed two stale R12 inline return-shape comments in
|
||
`interiors.js` (`watchPoints` 6..8 → 8/8/12 caps; `stage` `bandPoses[3]` → `[4]` + `riserY`) + one in
|
||
`layout.js`; added a note at the gigKey hand-build explaining it's deliberately standalone (byte-identical to
|
||
citygen's `gigKeyFor`, not imported — keeps the interiors lib citygen-free). `theme.js` untouched.
|
||
|
||
---
|
||
|
||
## Update 2026-07-15 (round 13, v3.0-beta: the district) — two more rooms + the drummer
|
||
|
||
R13 §Lane C. The district: two new venue archetypes + the 4-piece band + the RY-facing audit. Interfaces
|
||
(re)published in [docs/LANES/LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) — **read §0 first (the pinned ry
|
||
convention + the keeper/browser back-to-front finding for Lane D).** Committed C-only by pathspec (the tree
|
||
had E/B/D changes landing concurrently — [[procity-shared-tree-hazard]]).
|
||
|
||
**Shipped (C-owned files only): `theme.js`, `layout.js`, `interiors.js`.**
|
||
- **`band_room` + `rsl` recipes** (`theme.js`, both `venue:true`), reusing the pub venue machinery with a new
|
||
`venueSpec` field. `band_room` = small/scuzzy tin shed (cosy/pokey bias, low 3.4 m ceiling, deck barely a
|
||
riser `deckY 0.2`, near-empty standing floor). `rsl` = carpet-and-flock club (hall/wide bias, tall 4.0 m,
|
||
proper stage `deckY 0.4`, bistro tables, the biggest crowd cap). A converts a shop with `shop.type = kind`;
|
||
C keys the recipe off it. `SHOP_TYPES` now auto-enrols both in `drawSweep`/`soak`.
|
||
- **4-piece band (debt #3):** `placeStage` grows `stage.bandPoses` to **4** — front-line trio
|
||
(guitar/vocal/bass, `y=deckY`) + a **drummer** (`role:'drums'`, `seated:true`, up-stage on the drum riser,
|
||
`y=riserY=deckY+0.16`). Each pose now carries its own `y`; `stage.riserY` added. Applies to all three
|
||
archetypes (the pub gains a drummer too). Pure geometry — no new rng draw, determinism intact.
|
||
- **Per-kind venue tuning:** `placeStage`/`placeWatchPoints` read `recipe.venueSpec` → deck size/height +
|
||
**crowd cap** (pub/band_room **8**, rsl **12** — the crowd-cap stress case). Pub values == the R12
|
||
constants, so the pub stage geometry is byte-identical (bar the new drummer).
|
||
- **Robust gigKey (debt #1):** `audio.gigKey = 'gig-'+genreKey`, genre resolving `opts.gig.genreKey` →
|
||
`shop.genreKey` (A sets it) → a kind default. Matches E's renamed manifest beds `gig-pubrock`/`gig-grunge`/
|
||
`gig-covers` exactly (verified: manifest has all three, zero stale `pubrock-live`). One key, no mapping.
|
||
|
||
**RY-facing audit (debt #4) — see LANE_C_PUB.md §0 for the full write-up + fix. Headline:**
|
||
- Canonical convention pinned: **rig-front = local −Z at ry=0** (every C pose + the box placeholder use it).
|
||
- **The GLB fleet mesh faces +Z (opposite).** Verified in a fresh context two ways: toe-bone probe
|
||
(`toe−foot = (0,+0.99)` on 5 peds) and dotting a fleet rig planted at `counter.stand.ry` against the
|
||
intended customer-facing (`−0.99` = 180° backward; `+π` → `+0.99`).
|
||
- **Finding: keepers + browsers have been facing 180° backward whenever the GLB fleet is loaded** —
|
||
`keepers.js` spawns at `pose.ry` with no flip, and the mesh front is +Z. The R12 band reads right only
|
||
via `band.js RY_FLIP=π`. Placeholder (−Z) and GLB (+Z) are opposite, so it's inconsistent both ways.
|
||
- **Recommended fix for Lane D (visual-only, no golden moves):** normalise at the source — rotate the clone
|
||
`inner` by π in `rigs.js buildFigure` so every GLB rig faces −Z; then `keepers.js` stays flip-free (now
|
||
correct) and `band.js` DROPS `RY_FLIP`. Repro below.
|
||
|
||
**→ Lane F (flags_check.py): two smokes are RED by design — bump `band == 3` → `== 4`.** My 4th bandPose
|
||
makes the band a 4-piece (D's `band.js` already iterates `bandPoses`, so it spawns 4 today), which trips F's
|
||
R12 assertions verbatim as the round-13 instructions anticipated ("F's band==3 smoke moves to 4"):
|
||
- `tools/flags_check.py:674` — `if night['band'] == 3` → `== 4`.
|
||
- `tools/flags_check.py:794` — `if na['inRoom'] and na['band'] == 3 and na['crowd'] > 0` → `== 4`. *(The
|
||
noassets gig still happens — band 4, crowd 8, 0 fetches, 0 console errors; silent-and-happy holds. It only
|
||
"fails" because 3 is hardcoded.)* These are the ONLY qa reds from C; the other 5 gates pass.
|
||
|
||
**→ Lane D:** lift each band member to `pose.y` (front line `deckY` unchanged; drummer sits at `riserY`);
|
||
`seated:true` on the drums pose → seated bob; raise `band.js CROWD_CAP` to the watchPoint cap so the RSL's 12
|
||
fill (I measured the full 12). And apply the §0 facing fix. Repro for the audit (paste in the interior test page):
|
||
```js
|
||
const rigs = await import('./js/citizens/rigs.js'); const THREE = await import('three');
|
||
const fleet = rigs.loadPedFleet('models/peds/'); await fleet.whenReady;
|
||
const sp = rigs.spawnRig(fleet.all[2], { ry: 0, height: 1.75 }); sp.fig.updateWorldMatrix(true, true);
|
||
let foot, toe, wp = new THREE.Vector3();
|
||
sp.inner.traverse(o => { if (o.isBone){ o.getWorldPosition(wp);
|
||
if (/LeftFoot$/i.test(o.name)) foot = wp.clone(); if (/LeftToeBase$/i.test(o.name)) toe = wp.clone(); }});
|
||
console.log('fleet forward at ry=0:', toe.clone().sub(foot).setY(0).normalize()); // → (0, +1) = +Z
|
||
```
|
||
|
||
**Budgets (measured, fresh context, seed 20261990):**
|
||
- Empty shells (`drawSweep` glb-off): band_room 124 · pub 126 · rsl 135 (worst town-wide 161 opshop/hall) ≤350.
|
||
- **Gig-night incl. full crowd + 4-piece:** worst **RSL 237 draws** asset-free (16 rigs) / 77 GLB, 40.9k tris.
|
||
pub 176/56, band_room 173/53. ≤350 draws / 200k tris. ✓
|
||
- 90-build venue soak: 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo0/tex0. `soak(60)` in
|
||
the page: PASS 60 rooms, leak 0/0, determinism identical, worst draws 85. qa figure-scale gate: 24 figs
|
||
human-sized. Shot: [docs/shots/laneC/rsl_gig_r13.jpg](docs/shots/laneC/rsl_gig_r13.jpg) — RSL, 4-piece
|
||
facing the crowd, crowd facing the stage (facing reads correct via band.js's flip).
|
||
|
||
**Drummer veto (John):** default is Fable's 4-piece ruling (executed). To revert to a front-line trio, drop
|
||
`bandPoses[3]` in `layout.js placeStage` (1 line) + F reverts the smoke to `== 3` — say so before D wires it.
|
||
|
||
---
|
||
|
||
## Update 2026-07-15 (round 12, v3.0-alpha) — the pub venue: archetype + watchPoints
|
||
|
||
R12 §Lane C (venues & gigs, behind `?gigs=1`). Interface published in
|
||
[docs/LANES/LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) — the "publish interface first" deliverable D/F build on.
|
||
|
||
- **`pub` archetype** (`theme.js`, `venue:true`): low **stage** + PA cabinets + amps at the back wall (new
|
||
`stage`/`paSpeaker`/`ampStack` fittings, GLB slots `stage`/`pa_speaker`/`amp` for E), bar counter (`corner`
|
||
— the barkeep is just a keeper), sticky-carpet floor, the blocked back doorway = the green room.
|
||
- **`room.watchPoints[]`** — 6–8 seeded audience floor poses `{x,z,ry,dance,slotIndex}` facing the stage
|
||
centre (`counter.stand` ry convention). `dance` seeded **~⅓ true** (John: "a bit of both"). Walkable,
|
||
reachable, clear of keeper/browse/stage. **`room.stage`** = `{x,z,w,d,deckY,frontZ,bandPoses[3]}` so D
|
||
drops the band on the deck.
|
||
- **`room.audio.gigKey`** — F passes `opts.gig` (gig state); a gig-on pub returns `gigKey='gig-'+genreKey`
|
||
(`gig-pubrock` for the alpha), else a quiet-night interior. F prefers gigKey over musicKey. (→ E: name the
|
||
live bed `gig-pubrock`.)
|
||
- **Flags-off byte-identical**: venue code runs only for `recipe.venue`, on its own `stage`/`watch` sub-
|
||
streams — non-pub rooms have `watchPoints:[]`, `stage:null`, unchanged audio + placement.
|
||
- Verified: 40-seed pub sweep — 0 throws · **0 carves** (PA `areaFree`-guarded off the counter strip) · 0
|
||
path-fails · 8 watchPoints each · dance 0.38. 60-room mixed soak (incl. pub) — 0 throws/det/path fails,
|
||
**leak geo0/tex0**, worst 19 ms. Non-pub rooms confirmed unchanged.
|
||
|
||
---
|
||
|
||
## Update 2026-07-15 (round 11, audio) — `room.audio` contract
|
||
|
||
R11 §Lane C ("one contract answer"). **Confirmed** Fable's proposal: `buildInterior` now returns
|
||
`room.audio = { musicKey, toneKey }`, seeded per shop — F's interior_mode plays it, B's audio.js
|
||
resolves the keys. Full contract + F wiring recipe: [docs/LANES/LANE_C_AUDIO.md](docs/LANES/LANE_C_AUDIO.md).
|
||
|
||
- `toneKey` → a `manifest.audio.ambience` interior room-tone (always set); `musicKey` → a
|
||
`manifest.audio.music` bed, or **null** (no music). Keys, not files — buildInterior stays synchronous
|
||
and asset-free; silence on missing/`?mute=1`/`?noassets=1` (house audio law).
|
||
- Maps mirror the manifest's per-type `types` arrays (one source of truth): record/milkbar/video/dept/
|
||
arcade always play their bed; opshop/book/toy/pawn/stall are room-tone-only, except a seeded ~1-in-3
|
||
keeps a general radio on (the `general`-tagged milk-bar bed).
|
||
- Verified (9 types × 8 seeds): 0 bad keys, 0 nondeterminism, every key resolves + matches manifest
|
||
`types` membership; deterministic per shop. `qa.sh --strict` green.
|
||
- Optional visual-source prop (`userData.audioEmitter`) deferred to v2.2 (in the note) — out of round scope.
|
||
|
||
---
|
||
|
||
## Update 2026-07-15 (round 9) — ship v2.0: browse points (C→D→F seam) + buy-anywhere
|
||
|
||
Round-9 §Lane C. The round hangs off C1 (two lanes build on it), so it shipped first as a handshake +
|
||
working code; C2 widens buying beyond the dig.
|
||
|
||
**C1 — browse points (the round's one cross-lane seam).** `layout.js placeBrowsePoints()` exposes
|
||
`room.browsePoints[]` — 1–3 deterministic floor poses in front of browsable fittings, facing the goods,
|
||
where Lane D stands browser rigs (F wires occupancy → points). Same coord frame + `ry` convention as
|
||
`counter.stand`, so D's fleet clones drop straight in. Pure data — **no meshes, no draws**; the ≤350 law
|
||
is untouched by C1. Guarantees (all asserted headless): walkable (occ-free, not reserved), reachable from
|
||
spawn (the door→counter flood-fill), ≥0.6 m apart + clear of the keeper stand, deterministic per
|
||
`shop.seed`, ≥1 point in every archetype that has a browsable fitting. A rich candidate ring (4 sides × 2
|
||
gaps + diagonals) + a nearest-free-cell fallback covers tight/mismatched rooms. Interface published in
|
||
[LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md) **before** code (decision #1 half-day handshake) → committed
|
||
first so D/F unblock. Validated: **180 type×archetype×seed combos → 0 empties** (177×3 pts, 3×1 via
|
||
fallback), determinism 0 fails, min-sep 0.7 m, 0 keeper clashes. Test page: cyan browse-post markers on the
|
||
`path` overlay + a HUD count.
|
||
|
||
**C2 — buy-anywhere (decision #2).** Book spines + toy boxes get pull-and-buy over the **same wallet**;
|
||
records keep the deep dig. New `stockpack.buildBuyableShelf()` builds each real shelf as merged,
|
||
per-item-**addressable** meshes — one mesh per atlas (all covers on a shelf drawn from a single seeded
|
||
atlas → **one draw/shelf**), tagged `noBatch` so `batch.js` leaves them intact. Aiming a spine/box raises a
|
||
pull card (title / author-or-maker / $price / band from the pack index); BUY debits the wallet, adds to
|
||
inventory, and `collapseBuyItem()` zero-areas that item's quad in place (**removed from the shelf**, one
|
||
draw, no geometry churn). Parody stock has no buy mesh → **no card** ("not for sale"). Fail-soft per pack.
|
||
Validated: raycast→card→BUY works end-to-end (item collapses to its centre, cash/inventory update, card
|
||
closes); **buy-soak 6/6 bought, `collapseOk`, leak geo 0 / tex 0**; draws with `?stock=real` **record 41 ·
|
||
book 58 · toy 57** (≤350); full drawSweep unchanged (GLB-off 168 · GLB-on 344, both pass); placement
|
||
determinism 0 fails. Shot: [browse_buy_r9.jpg](docs/shots/laneC/browse_buy_r9.jpg).
|
||
|
||
**→ D**: consume `room.browsePoints` (contract in LANE_C_NOTES) — stand rigs at the first
|
||
`min(occupancy, 3)` points. **→ F**: two new warn smokes (hooks in LANE_C_NOTES): browse-points present +
|
||
valid, and shelf-buy (aim → card → wallet debit → collapse). C's soak covers keeper+stock+dig+buy together.
|
||
|
||
---
|
||
|
||
## Update 2026-07-15 (round 8) — the town comes alive: buy loop v0 + book/toy real stock
|
||
|
||
Round-8 §Lane C, theme "the town comes alive". Two deliverables + a verify pass.
|
||
|
||
**C1 — buy loop v0** (new `web/js/interiors/wallet.js`). A shallow, **runtime-only** economy: seeded
|
||
starting cash, purchases, an in-memory bag. Reload resets; world generation is **untouched** (purchases
|
||
never enter the plan or the seeded room build — the wallet is a separate object the shell reads, never
|
||
writes back into gen).
|
||
- `createWallet(seed)` → `{ cash, start, canBuy, buy(offer), inventory, count, onChange }`. Start is
|
||
seeded ~$60–200 (`mulberry32`). `buy` deducts + records on success, **returns false unchanged if broke**.
|
||
`onChange` lets F's street shell subscribe the same wallet to a cash chip.
|
||
- **Test page** (`interior_test.html`): a `#cashChip` HUD (`$163 · 🛍 N`, street + interior) + an
|
||
inventory panel toggled with **I**. The dig's pull card gets a **BUY IT** button (price + band); buying
|
||
pulls the sleeve into the bag, decrements cash, updates the chip, and **depletes that bin** (bought bins
|
||
offer fewer sleeves on re-riffle). Broke → the button no-ops (can't buy). `digSoak` now buys up to 2/visit.
|
||
- Offer shape is the dig offer (`{a,t,price,s}`) OR a pack item (`{title,artist,price,price_band}`) — one
|
||
`buy()` eats both.
|
||
|
||
**C2 — book spines + toy boxes wired to `?stock=real`.** E shipped the generalized packs
|
||
(`stock_book_*` 311 items / 4 atlases, `stock_toy_*` 273 items / 5 atlases, alongside `stock_record_*`
|
||
350/6). `stockpack.js` now keys each pack to its slot via `SLOT_FOR = {record:'sleeve', book:'spine',
|
||
toy:'box'}`; `stock.js shelfLine` consults the adapter and builds real cover planes for book spines
|
||
(bookshelves) and toy boxes (cube shelves) — **static shelf stock, no dig** for those, fail-soft per pack.
|
||
Every real mesh samples its atlas material with a baked per-item UV, so a whole shelf still batches to one
|
||
draw per atlas.
|
||
|
||
**C3 — validated** (all three packs live, `?stock=real`):
|
||
- **Draws ≤350** (the law): record **41**, book **42**, toy **51** worst over seeds {1990,7,42(,101)}.
|
||
- **Determinism**: same seed → identical draw calls + mesh count, all three types (0 fails).
|
||
- **Leak-free**: 0 geometry/texture delta over 20 build/dispose cycles; **buy-soak** 12 record-dig visits,
|
||
5 bought (wallet depletes to broke → exercises the can't-buy path), `leakGeo 0 / leakTex 0`; resting geo
|
||
back to baseline after close. Atlas textures shared/cached (not per-room); per-item geo ctx-tracked & disposed.
|
||
- **Reload resets** the wallet (fresh boot → seeded start again); **flag-off + `?noassets` untouched**.
|
||
- Shot: [buyloop_r8.jpg](docs/shots/laneC/buyloop_r8.jpg) — toy boxes, book spines, record dig → BUY card.
|
||
|
||
**→ F**: two new warn-level smokes (hooks in [LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md)): (1) **buy loop** —
|
||
`createWallet` deduct/broke/inventory is pure and headless-testable; (2) **book/toy packs** — enter book or
|
||
toy shop with `?stock=real`, assert `isStock` meshes on a shared atlas material (drawSweep ≤350). Buy UI is
|
||
test-page-local; the reusable piece for the street shell is `wallet.js` + `onChange`.
|
||
|
||
---
|
||
|
||
## Update 2026-07-14 (round 7) — `?stock=real`: real GODVERSE sleeves in the crates
|
||
|
||
Round-7 §Lane C: the `stockAdapter` seam built in v1 finally eats. Lane E's GODVERSE stock pack (parody-
|
||
transformed Discogs metadata + cover atlas) feeds record-shop bins + the dig riffle behind `?stock=real`.
|
||
- **Contract handshake** (task 1, unblocked E): confirmed E's index schema as-is in
|
||
[LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md) — `{ items:[{id,title,artist,price,price_band,atlas,uv}] }`,
|
||
UV origin top-left (C flips V for WebGL), fail-soft, per-type packs.
|
||
- **New `stockpack.js`**: `preloadStockPack`/`getStockPack` (cached) + `makeStockAdapter`. Atlas texture +
|
||
material are shared/cached (like depot GLBs); sleeves are cover PLANEs sampling the atlas at each item's
|
||
baked UV → all real sleeves in a room **batch to one draw** (record: 59 draws).
|
||
- **Consumers**: `stock.js binFan` builds real cover planes (else parody canvas); `dig.js` riffles real
|
||
covers and the pull card shows **title / artist / $price / band**; `interiors.js` wires `opts.stock='real'`
|
||
through the seam (warn-once + parody fallback if the pack is absent); test page reads `?stock=real`.
|
||
- **Validated (sample 24-pack)**: determinism 0 fails/30, leak-free rooms AND real dig (0/0 over 25 cycles —
|
||
atlas shared, sleeve geo disposed), draws ≤139 all rooms (record 59), fail-soft on no-pack types (opshop
|
||
ok → parody), `?dig=1&stock=real` leak-free. Flag-off + `?noassets` untouched (procedural path byte-identical).
|
||
- Built against E's **staged sample** (real index format, synthetic covers); real Discogs covers drop in
|
||
unchanged when E gets the DB (external — John/Fable). Shot: [stock_real_r7.jpg](docs/shots/laneC/stock_real_r7.jpg).
|
||
|
||
**→ F**: new smoke for your strict harness — `?stock=real` enter record shop + open dig → assert real
|
||
(non-parody) sleeves; the record bin's real sleeves are cover planes with `userData.isStock` on a shared
|
||
atlas material (drawSweep stays ≤350). Hook + adapter in LANE_C_NOTES.
|
||
|
||
---
|
||
|
||
## Update 2026-07-14 (round 6) — C1 interior draw batching (≤350 draws/room is law)
|
||
|
||
Round-6 §Lane C task 1 (the review debt: book room was 1,245 draws GLB-on). **Diagnosed** the two
|
||
drivers — (a) multi-material stock boxes cost one draw PER face (6×), (b) one mesh per stock item —
|
||
then batched (new `web/js/interiors/batch.js`):
|
||
- **stock.js**: boxes are now single-material; every stock mesh tagged `userData.isStock`.
|
||
- **batch.js** (`batchRoom`, called from interiors.js before the GLB upgrade): two-tier merge —
|
||
room-level merge of all `isStock` across every fitting (grouped by material, world transforms baked)
|
||
+ per-fitting merge of fixture frames (kept in-group so bins stay raycast targets and the GLB
|
||
upgrade can hide the fixture). `mergeGeometries` from vendored BufferGeometryUtils.
|
||
- **glb.js**: frame-hide is now by `isStock` tag (not frame-count), so batched fixtures hide and the
|
||
room-level stock rides on the GLB.
|
||
|
||
**Result** (drawSweep, all 9 types × 6 archetypes, seed 1990):
|
||
- GLB-off worst **152** (opshop/hall), GLB-on worst **313** (opshop/hall) — book 1,245 → **189**. **≤350 ✓**.
|
||
- Determinism 0 fails / 810 builds · leak-free · dig still riffles batched bins (bins keep frame as hit
|
||
target; dig regenerates its own sleeves) · worst warm build 38ms (<50) · visually identical.
|
||
- **Sweep now ASSERTS ≤350** (`drawSweep()` in the test page, folded into the soak; `window.PROCITY_C.
|
||
drawSweep` for F's harness — see LANE_C_NOTES for the hook).
|
||
|
||
**C2 — map the 3 orphaned fittings** (published R5, unconsumed). In `glb.js`: `glassCase→glass_case`,
|
||
`magazineRack→magazine_rack`, and a NEW `crateStack` primitive (stack of crates) → `crate_stack`.
|
||
Recipe placements: crateStack in record + opshop (back-room dressing), glassCase added to milkbar,
|
||
magazineRack added to pawn (+ moved milkbar's magazineRack window→wall to dodge the door-counter).
|
||
Also **fixed `placeNearCounter`** — it picked one random side and gave up, so counter-zone fittings
|
||
(the glass case) rarely landed; now it tries both sides + offsets, then falls back to a free aisle spot.
|
||
All 3 load + render planted/scaled/facing right (yaw 0). Appearance: glassCase **12/12** (toy/pawn/milkbar),
|
||
magazineRack **12/12** (milkbar/pawn), crateStack ~11/12 (record/opshop). Contact sheet:
|
||
[glb_orphans_r6.jpg](docs/shots/laneC/glb_orphans_r6.jpg). drawSweep still ≤350 with them on (worst 177
|
||
GLB-on). **C3**: dig soak re-run after batching + new fittings — riffle works on batched bins, leak-free.
|
||
|
||
**Round 6 complete for Lane C**: C1 (batching, `35981ad`) + C2/C3 (orphans + dig re-verify) committed.
|
||
Draw law satisfied (≤313 was the tightest; ≤177 after C2), determinism/leak/dig all green, qa strict green.
|
||
|
||
---
|
||
|
||
## Update 2026-07-14 (round 5) — C1 v1.1 re-map (record_crate + counter_till) · **F can tag v1.1**
|
||
|
||
Round-5 §Lane C task 1 (the v1.1 gate). Lane E's re-exports mapped + validated in `glb.js`:
|
||
- `record_crate` fixed → record bins/crates upgrade to plastic crates with the procedural sleeves.
|
||
- `counter → 'counter_till'` (the real ~1.6 m counter-with-till). Keeper-stand pose unaffected.
|
||
- **Double-till resolved**: cash_register attach dropped when counter_till is live (it has its own till);
|
||
falls back on the primitive; milkshake_mixer stays on the milk-bar bench (lifted to the GLB height).
|
||
- Sweep + soak (GLB on): `0 throws / 0 path / 0 det / leak-free / worst 10.4ms`; **0 double tills**.
|
||
Primitive path unchanged (glb.js-only edit). Contact sheet: [glb_v1_1_r5.jpg](docs/shots/laneC/glb_v1_1_r5.jpg).
|
||
- Report + decision detail: [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md); F note in
|
||
[LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md). **→ F: C1 committed, v1.1 can be tagged.**
|
||
|
||
**C2 — `?dig=1` crate-riffle (v2, behind flag).** Ported 90sDJsim `dig.js` → `web/js/interiors/dig.js`:
|
||
walk to a record bin, press **E**, riffle its sleeves (scroll/drag), click the front to pull, **BUY IT**
|
||
(STUBBED — toast + remove; getCash constant; real economy is round 6+). Riffle items are seeded per bin
|
||
via the `stockAdapter(shop,'sleeve')` seam (procedural fallback now — parody 90s-AU sleeves). Own scene +
|
||
camera, procedural fwip/thunk audio, **leak-free** (per-open resources freed on close → dig soak 20×
|
||
`leakGeo 0 / leakTex 0`). Flag-off (`?dig=0`) is **byte-identical** — `buildInterior` is untouched; the dig
|
||
is wired only in the test page (F owns the shell seam — hook documented in LANE_C_NOTES §"input/mode hook").
|
||
Shot: [dig_riffle_r5.jpg](docs/shots/laneC/dig_riffle_r5.jpg).
|
||
|
||
**C3 — interior re-measure after Lane E's bake (`c522464`).** E baked the 6 heavy hero props to ≤8k tris.
|
||
Re-measured every interior (cache-busted to bypass the dev browser's stale HTTP cache): **record 43.6k**
|
||
(the C3 target, ~273k pre-bake), **milkbar 38.9k** (was the flagged 468k → over-budget flag CLOSED), all
|
||
rooms ≤107k tris. `book` is the draw-call outlier (1,245 draws from ~9 bookshelf GLBs + primitive spines) —
|
||
tris fine, interiors render one-at-a-time so gate 3 unaffected; instancing spines is the future lever.
|
||
Full table in [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md §C3).
|
||
|
||
**Round 5 complete for Lane C**: C1 (v1.1 re-map) + C2 (`?dig=1`) + C3 (re-measure) all committed.
|
||
|
||
---
|
||
|
||
## Update 2026-07-14 (round 4) — hero props into rooms
|
||
|
||
Round-4 task (ROUND4_INSTRUCTIONS §Lane C): map the 4 new hero props + cash_register (manifest now 14
|
||
fittings), sweep/soak with them on, screenshot each, delete the stale `_local_glb/`, measure a hero
|
||
room. **All done.** Full report for Lane E: [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md);
|
||
contact sheet [docs/shots/laneC/glb_heroprops_r4.jpg](docs/shots/laneC/glb_heroprops_r4.jpg).
|
||
|
||
- **New mappings** (`glb.js`): `arcade_cabinet`→new `arcadeCabinet` primitive (video focal prop),
|
||
`listening_booth`→`listeningCorner` (record), `drinks_fridge`→`fridge` (milkbar). All load, `glb_law`
|
||
compliant, yaw 0 (no correction). A new **counter-top attach** (`attachCounterTop`) puts `cash_register`
|
||
on every counter's till slot (hides the primitive till) and `milkshake_mixer` on the milk-bar benchtop —
|
||
the counter primitive + keeper stand are untouched.
|
||
- **Focal-prop reliability fix**: the door-counter path loop was pulling priority-4 focal props (arcade
|
||
appeared in only 5/24 video rooms). Raised the 3 focal props to priority 6 + placed-first →
|
||
**24/24 appearance**, still 0 carves / 0 path-fails.
|
||
- **Validation**: primitive sweep 810 builds `{throws 0, pathFail 0, detFail 0}`; GLB-on soak
|
||
`{throws 0, path 0, det 0, leakGeo 0, leakTex 0, worst 7.3ms}`; all 5 new props confirmed in-room.
|
||
- **Room tris measured** (task 4): milk-bar GLB-on ≈ **468k tris** (over budget) driven by `drinks_fridge`
|
||
(77k) + `milkshake_mixer` (73k) — **reported to Lane E** as the #1 bake-pass target (not fixed, per
|
||
integrator decision #2; interiors render one-at-a-time so gate 3 / the street budget is unaffected).
|
||
- **Cleanup**: deleted `web/assets/_local_glb/` (R3 validation copy); test page now loads GLBs via the
|
||
live `depot:` path (`?localdepot=1` still works for offline). `localBase` hook kept in `glb.js`.
|
||
- **Standby (C task 5)**: when Lane E re-exports `record_crate` (broken) + a ~2 m counter-with-till, I
|
||
re-map both. Not landed at commit time.
|
||
|
||
---
|
||
|
||
## Update 2026-07-14 (round 3) — GLB hero-prop visual validation
|
||
|
||
Round-3 task (ROUND3_INSTRUCTIONS §Lane C): flip `useGLB` on and validate Lane E's fitting GLBs in real
|
||
rooms. Done — full report for Lane E in [docs/LANES/LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md).
|
||
|
||
**Verdict:** 5 of 9 fitting GLBs are good and now live behind `useGLB` (wire_shelf, clothes_rack,
|
||
bookshelf, cube_shelf_wide, work_table); 2 fall back to primitive with a report filed
|
||
(`record_crate` **broken — won't load**; `counter` **too long/no till** → primitive keeps the till +
|
||
keeper stand). record bins stay primitive until Lane E re-exports the crate.
|
||
|
||
**Soak with `useGLB` on** (50 rooms, all types): `throws 0 · pathFail 0 · determinismFail 0 · leakGeo 0 ·
|
||
leakTex 0 · worst build 10.5ms · 256 upgrades applied` — GLB resources dispose leak-free on room exit.
|
||
|
||
**glb.js changes (my file):**
|
||
- `manifest.localBase` — load GLBs from a served local dir for offline validation (production still uses
|
||
`depot:<file>`; forward-compatible with Lane E's local-depot mode).
|
||
- **Real-metre scale, clamped** to the reserved footprint (was fit-to-width, which squashed tall props);
|
||
matches `glb_law` (metres). Per-kind **yaw correction** (`KIND_TO_YAW`) for the Z-major assets
|
||
(bookshelf/cube/work_table need +90°) per the manifest `facing` convention ("Lane B/C rotate directional props").
|
||
- `room.glbReady` (interiors.js) — a promise that resolves when GLB swaps settle, so callers can await
|
||
before screenshot/leak-check (Lane F can use it in the §3.4 wiring).
|
||
- Test page (`interior_test.html`) gains a **GLB props** toggle + a `GLB props: N upgraded` HUD line.
|
||
Local validation needs the GLBs copied in: `cp pipeline/_normalized/procity_fit_*.glb web/assets/_local_glb/`
|
||
(untracked; real GLBs live on the depot). Screenshot grid: [docs/shots/laneC/glb_validation_grid.jpg](docs/shots/laneC/glb_validation_grid.jpg).
|
||
|
||
---
|
||
|
||
## Update 2026-07-14 (b) — keeper stand pose (resolves Lane F §8 · unblocks §3.2/§3.3)
|
||
|
||
Lane F's runbook asked (LANE_F_NOTES §8): *"does `places` tag the counter slot with a stable
|
||
`kind:'counter'` so keepers find it?"* **Yes** — the counter interactable has always carried
|
||
`mesh.userData.kind === 'counter'` (+ `interactable:true`). But Lane D's keeper needs more than a
|
||
tag: `keepers.spawn(group,{x,z,ry})` needs *where the shopkeeper stands and which way it faces*
|
||
(keepers.js:28 — "`ry` = the counter's outward facing"). I computed the customer-facing side
|
||
internally but never surfaced it — so Lane D would have read the bench's raw transform and stood a
|
||
keeper embedded in the counter, possibly facing a wall. Closed that gap (my files only, purely additive):
|
||
|
||
- **New: `room.counter = { mesh, pose:{x,z,ry}, stand:{x,z,ry} }`** (interiors.js). `pose` is the
|
||
bench; **`stand` is the keeper pose** — behind the bench on the service side, facing the customer,
|
||
clamped inside the room, in **room-local space** (the same space Lane D adds the keeper into via
|
||
`interior.group`). `ry` uses the rig-front = local −Z convention keepers.js expects.
|
||
- **The same `{x,z,ry}` also rides the counter interactable**: `place.userData.keeperStand` — for the
|
||
`places`-iteration path Lane D/F described. Deep-equal to `room.counter.stand`.
|
||
|
||
**Lane D/F wiring (drop-in for §3.2/§3.3):**
|
||
```js
|
||
const room = buildInterior(shop, THREE);
|
||
const s = room.counter.stand; // or: room.places.find(p=>p.userData.kind==='counter').userData.keeperStand
|
||
keepers.spawn(room.group, { ...s, shopId: shop.id, type: shop.type });
|
||
// on exit: keepers.remove(handle) / keepers.disposeAll(), then room.dispose()
|
||
```
|
||
|
||
Verified in-browser (9 types × 4 seeds = 36 builds, + 50-room soak):
|
||
`stand in-bounds ✓ · faces-customer ✓ (dot>0 for all) · places-carries-keeperStand ✓ (deep-equal) ·
|
||
stand deterministic ✓ · placement determinism unchanged ✓ · soak avg 3.6ms / worst 7.7ms · leak geo0 tex0 ·
|
||
pathFail 0 · every room has a counter+stand`. No change to layout/placement of existing rooms.
|
||
|
||
**Minor known cosmetic (corner counters):** a corner counter sits ~0.55 m off the east wall, so the
|
||
service gap (~0.25 m) is narrower than a person — the keeper stands *at* the till with a small hidden
|
||
overlap into the solid waist-high bench (torso reads above the counter, legs behind it). Facing and
|
||
side are correct; it reads as "keeper at the till." If Lane D/Fable want real standing clearance I can
|
||
nudge corner counters ~0.2 m off the wall — but that changes those rooms' committed placement +
|
||
screenshots, so it's a decision, not a bug.
|
||
|
||
**Two contract-drift corrections for the F runbook** (§2's Lane-C line was written against an earlier
|
||
draft of my API): (1) there is **no `opts.registry`** — registry override is the one-time global
|
||
`mergeRegistry(registry)` (a per-build registry mutation broke same-seed determinism; removed in the
|
||
review pass). (2) `recipe.counterPos` is a **placement-strategy string** (`door|forward|back|corner`),
|
||
**not** a world position — use `room.counter.stand` for the keeper pose.
|
||
|
||
---
|
||
|
||
## What shipped
|
||
|
||
All files under my lane's ownership (`web/js/interiors/*`, `web/interior_test.html`) — no other lane's
|
||
files touched. Lane A's `core/registry.js` hadn't landed, so the lane is **fully standalone** (its 9
|
||
type recipes live in `theme.js`) with a `mergeRegistry()` hook for Lane F to wire the registry later.
|
||
|
||
| file | role |
|
||
|---|---|
|
||
| [interiors.js](web/js/interiors/interiors.js) | **public API** — `buildInterior(shop, THREE, opts)` |
|
||
| [theme.js](web/js/interiors/theme.js) | 9 shop-type recipes (archetype/wallpaper/floor bias, clutter, counter pos, fittings mix, stock kind, signage) + aliasing + registry-override hook |
|
||
| [shell.js](web/js/interiors/shell.js) | room shell: floor/walls/ceiling from lot×archetype, glazed shopfront + street backdrop, blocked back doorway, interior lighting |
|
||
| [fittings.js](web/js/interiors/fittings.js) | parametric fittings kit (ported from 90sDJsim + extended): bins, crates, racks, 4 shelf types, VHS aisle, glass case, counter+till, fridge, magazine/spinner racks, armchair, escalator, pegboard, barred window, returns slot, art frames |
|
||
| [layout.js](web/js/interiors/layout.js) | the placer: per-archetype zones, shuffled wall-slot system, occupancy grid, **guaranteed door→counter flood-fill path** |
|
||
| [stock.js](web/js/interiors/stock.js) | v1 visual stock: pooled canvas-texture sleeves/spines/boxes/garments/snacks with price stickers (dig.js trick) + `stockAdapter` hook for BaseGod data |
|
||
| [context.js](web/js/interiors/context.js) | build/dispose/seed backbone: independent seeded sub-streams, shared-geometry cache, leak-free `disposeAll()` |
|
||
| [glb.js](web/js/interiors/glb.js) | **optional** GLB hero-prop upgrade — reads Lane E's `manifest.json`, swaps primitives for depot GLBs, placeholder-persists. OFF by default; primitive fallback always |
|
||
| [interior_test.html](web/interior_test.html) | standalone test page (below) |
|
||
|
||
## Public API (the contract Lane B/F build against)
|
||
|
||
```js
|
||
import { buildInterior } from './js/interiors/interiors.js';
|
||
import * as THREE from 'three';
|
||
|
||
const room = buildInterior(shop, THREE, opts?);
|
||
scene.add(room.group);
|
||
// … player walks; on hitting room.exits[i], fire procity:exitShop …
|
||
room.dispose(); // frees ALL per-room GPU resources; removes group from parent
|
||
|
||
// room = {
|
||
// group, // THREE.Group, self-contained (lights included)
|
||
// spawn: { x, z, ry }, // enter just inside the door, facing −Z (into the shop)
|
||
// exits: [ { x, z, w, toStreet } ], // door back to street
|
||
// places: [ …meshes w/ userData ], // interactables: counter, bin, case, fridge, returns, exit
|
||
// counter: { mesh, pose, stand }, // pay point. stand:{x,z,ry} = keeper (Lane D) spawn pose
|
||
// dims: { W, D, H, archetype, type },
|
||
// placement, // deterministic summary (deep-equal per seed)
|
||
// pathOK, buildMs, counts(), dispose()
|
||
// }
|
||
```
|
||
|
||
`shop` is tolerant — recognized fields `{ id, type, name, seed, storeys, lot:{w,d} }`:
|
||
- **seed** (uint32) is the ONLY randomness source; falls back to a hash of id/name/type if absent.
|
||
- **type** — any CITY_SPEC type (`record opshop toy book video pawn milkbar dept stall`) **or** a
|
||
thriftgod/Overpass alias (`music charity pawnbroker market video_games …`); defaults to `opshop`.
|
||
- **lot** `{w,d}` metres — the room adapts to it (never exceeds it); absent → archetype ranges decide.
|
||
- **storeys** drives ceiling height 3.2–4.5m.
|
||
- `opts`: `{ archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, useGLB?, manifest?, manifestUrl? }`.
|
||
Registry override is a **one-time global** — `import { mergeRegistry } from interiors.js; mergeRegistry(registry)`
|
||
at init (NOT a per-build opt — a per-call mutation of the shared recipe table would break same-seed determinism).
|
||
|
||
**Coordinate convention** (shared with a future Lane B wiring): `+Z` = door/street side, `−Z` = back,
|
||
origin = room centre, floor `y = 0`. Everything is in the returned `group`'s local space.
|
||
|
||
### For Lane B (door wiring, later)
|
||
`buildInterior` is a pure function — call it on `procity:enterShop`, add `room.group` to the scene,
|
||
teleport the player to `room.spawn`. On `procity:exitShop` (player reaches `room.exits[0]`) call
|
||
`room.dispose()` and restore the street. The street window fake is Lane B's; I own the inside view
|
||
(a bright canvas street backdrop plane is already behind the glazing).
|
||
|
||
### For content phase (BaseGod / GODVERSE)
|
||
Pass `opts.stockAdapter = (shop, slotKind) => ({ texture }) | ({ mesh }) | null`. Returning a texture
|
||
skins that stock item's face; a mesh replaces the item; null → procedural placeholder. No layout/
|
||
fittings changes needed. `slotKind ∈ { sleeve, spine, box, snack, magazine, garment, treasure }`.
|
||
|
||
## Test page — `web/interior_test.html`
|
||
|
||
Serve: `cd web && python3 -m http.server 8130` → open `/interior_test.html`.
|
||
- seed input · type dropdown (9) · archetype override (auto + 5) · lot `w×d`/auto · re-roll · seed ±
|
||
- first-person walk (PointerLock + WASD) with occupancy-grid collision + room clamp
|
||
- debug toggles: **wireframe** (F), **occupancy grid** (G), **path** (spawn/counter markers)
|
||
- **▶ 50-room soak test**: builds+disposes 50 seeded rooms, asserts ms/room, leaked geometries vs
|
||
baseline, and determinism (same seed → identical placement)
|
||
|
||
## Acceptance — all met
|
||
|
||
| requirement | result |
|
||
|---|---|
|
||
| Same seed twice → identical room (deep-equal placement) | ✅ 0 mismatches / 810 builds |
|
||
| All 9 types × 5 archetypes render sensibly | ✅ contact sheet `docs/shots/laneC/_grid_9types_5archetypes.jpg` |
|
||
| Build < 50ms/room | ✅ steady-state ~4ms, clean-soak worst ~8ms (first-ever build ~60ms one-time cold-JIT; occasional ~60ms GC pauses only under back-to-back torture loops — not a real per-room cost) |
|
||
| Dispose leak-free (info.memory → baseline) | ✅ soak leak: geo 0, tex 0 |
|
||
| Flood-fill door→counter always exists | ✅ 0 path-fails, 0 carves / 810 builds |
|
||
| Runs with zero assets & zero network | ✅ seeded flat-colour fallback under every texture; map only assigned on load |
|
||
|
||
**Full sweep** (9 types × {auto+5 archetypes} × 3 seeds × {null,4×4,5×14,20×20,3×3} lots = 810 builds):
|
||
`throws 0 · pathFail 0 · determinismFail 0 · carved 0`. Warm perf: all < 50ms (worst 33.2ms).
|
||
**Soak** (50 rooms): `avg 4.0ms · worst 9.6ms · leakGeo 0 · leakTex 0 · detFail 0 · pathFail 0`.
|
||
|
||
## Key decisions & notes for review
|
||
|
||
- **90sDJsim `fittings.js` ported** (it was written to be lifted): `clothesRack`, `shelfUnit`,
|
||
`recordBin`, `table`, `counter`, `pegboard`, garment-silhouette canvas trick. Extended with the
|
||
fittings CITY_SPEC's registry needs (cube/metal/book/VHS shelving, glass case, fridge, spinner,
|
||
magazine rack, armchair, escalator prop, returns slot, barred window, listening corner).
|
||
- **thriftgod `buildShop` ported**: the 5 archetypes, the shuffled wall-slot system, bins with fanned
|
||
sleeves wearing covers, corner counter+till, crooked wall art, blocked back doorway.
|
||
- **Determinism** via independent seeded sub-streams keyed by a salt string (`ctx.stream('wallpaper')`
|
||
etc.) so adding a fitting in one subsystem never shifts another subsystem's picks.
|
||
- **Leak-free** via a tracked build context: per-room geometry/material/CanvasTexture/tiled-file-texture
|
||
are freed in `dispose()`; globally-cached file textures (`core/loaders.loadTex`) are shared and not
|
||
disposed (bounded cache). Dispose-before-texture-load race is guarded (`_disposed`).
|
||
- **Perf**: shared-geometry cache (`ctx.boxGeo/planeGeo/cylGeo`) collapses the hundreds of identical
|
||
stock items in a room down to ~80–110 geometries; spine density tuned so a book barn is ~600 meshes.
|
||
- **GLB upgrades**: `web/assets/manifest.json` (Lane E) is read if present, primitive fallback always.
|
||
Not present yet → 100% primitives, which is why the test page runs asset-free.
|
||
|
||
## Integration with sibling lanes (landed in parallel)
|
||
- **Lane A `core/registry.js`** — its `SHOP_TYPE_IDS` match my 9 recipe keys exactly. I kept the
|
||
interior recipes local (the registry doesn't carry archetype-bias/clutter/counter-pos/stock detail),
|
||
with `mergeRegistry()` as the one-time override seam for Lane F.
|
||
- **Lane E `web/assets/manifest.json`** — its `fittings` section (9 depot GLBs with footprints) is now
|
||
wired through `glb.js` (opt-in `useGLB`, primitive fallback). Depot GLBs may not be uploaded yet;
|
||
verified the fallback (unreachable depot → primitives persist, no crash, no leak).
|
||
- **Lane D `citizens/keepers.js`** — its `keepers.spawn(group,{x,z,ry,shopId,type})` now has a clean
|
||
source: `room.counter.stand` (also on `place.userData.keeperStand`). See the *Update (b)* section at
|
||
the top. Verified the pose is on the service side, faces the customer, and is deterministic.
|
||
|
||
## Adversarial review — 5 findings, all fixed
|
||
|
||
Ran a 5-dimension adversarial multi-agent review (determinism / disposal-leaks / path-guarantee /
|
||
three-api / edge-robustness) with a per-finding verify pass. 5 real defects surfaced; all fixed and
|
||
re-verified (830-build sweep + 50-room soak green after each fix):
|
||
|
||
| # | sev | file | defect | fix |
|
||
|---|---|---|---|---|
|
||
| 1 | med | interiors.js | per-build `opts.registry` permanently mutated the shared recipe table → latent same-seed determinism break across differing registry states | removed per-build option; registry override is now a one-time global `mergeRegistry()` (Lane F setup) |
|
||
| 2 | low | interiors.js | explicit `null` opts threw (`opts = {}` default only covers `undefined`) | normalize `opts = opts \|\| {}` |
|
||
| 3 | low | shell.js | empty `archetypeBias` array → `bias[0][0]` on `[]` throws | fall back to uniform bias when missing **or** empty |
|
||
| 4 | med | layout.js | fittings pulled by the path loop left phantom interactables in `places` (dangling after dispose) | collect `places` from survivors at the end, not incrementally at placement |
|
||
| 5 | low | layout.js | `rebuildOcc` re-stamped wall-mounted fittings that `placeAtWall` never stamped → phantom floor obstacles after a blocker-pull | mark wall-mounted `noStamp`; skip in `rebuildOcc` **and** exclude from the removable pool |
|
||
|
||
Verify evidence (post-fix): `nullOpts ok · phantomPlaces 0/332 · determinismAcrossBuilds identical ·
|
||
emptyBias ok · sweep 810 {throws 0, pathFail 0, detFail 0} · soak {avg 4.2ms, worst 8ms, leakGeo 0,
|
||
leakTex 0}`. (Reviewers also confirmed no disposal/leak or three.js-contract defects.)
|
||
|
||
## Not done (out of scope for v1 / depends on other lanes)
|
||
- Real item data / economy (content phase — `stockAdapter` hook is ready).
|
||
- GLB hero props are **wired** (`glb.js`, opt-in) but not visually validated end-to-end — needs the
|
||
depot GLBs actually uploaded (Lane E) + `useGLB` flipped on by Lane B/F. Primitive path is the tested baseline.
|
||
- Interior-mapping shader for street windows (Lane B stretch goal).
|
||
- Back-room beyond the blocked doorway (v2).
|