Compare commits

...

6 Commits

Author SHA1 Message Date
m3ultra
e466491300 Fable (integrator): round-15 instructions — close the v3.0 release (F's carried brief + poster-face fix + amp fix)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 09:21:39 +10:00
m3ultra
9a65f51a88 Lane D R14 (v3.0): identity continuity — the ped you followed is in the crowd
The last charter promise. A per-venue tonight roster (sim.js _venueRoster) of
who entered a gig venue tonight — surge occupants self-record in _enter, queue
admits relay via citizens.recordVenueEntry — is consumed by GigCrew.spawn: the
crowd's front slots BECOME the roster identities (same pedIndex, same height),
seeded strangers fill the rest.

- No golden move: both goldens are plan-gen hashes; each crowd slot is an
  independently-keyed rng stream, so a roster override touches only that slot and
  an EMPTY roster is byte-identical to R13 (verified). Roster heights 1.55-1.95m
  stay inside the no-giants [1.4,2.0] gate. Pure bookkeeping: 0 rng, 0 GPU.
- Roster persists across interior exit/re-enter, bounded, auto-clears on the
  setGig(id,false) transition (per-night). Deduped by stable key.
- queue.js: admitOne() returns {pedIndex,height,key} + fires an onAdmit callback
  (auto-drain AND explicit) so F relays every walked-in punter to the roster;
  _spawnAt stashes the identity. band.js: _make returns pedIndex; members carry
  it for F's continuity smoke; crew.spawn is now null-gig-safe.
- Verified: surge 8/8 + queue 4/4 real identities land in the crowd; empty-roster
  == R13; 7-night x 3-venue soak leak geo0/tex0, roster bounded, no identity
  churn; interior 64 draws <=350 (0 new rigs); ?noassets placeholders, 0 fetch.
- F seam in LANE_D_NOTES: crew.spawn({roster}) + queue onAdmit relay + the
  tonightRoster(venueShopId) accessor.
- Debt #2 (sit clip): investigated, deferred to v3.1 — the clip converts+binds
  (tailnet Sitting_Idle.fbx -> 394KB sit.glb) but a real seated pose is ~1.2m
  stature (fails F's no-giants floor) and needs a _rotWithHips loader + replant
  (shared rig-stack) + an F smoke exemption; not a freeze-week change. SEAT_DROP
  hack kept (reads seated from the money-shot front).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 09:04:06 +10:00
m3ultra
eee60e05a5 Lane B (Streetscape) round 14: v3.0 release — alpha-alias retirement + tour audit + queue_night hero (?gigs=1)
Release round: no new systems, no rng, goldens frozen (0x3fa36874 / gig 0x1f636349 both unmoved).
Files: audio.js, venue.js, dbg.js (+ LANE_B_NOTES). Verified fresh Chromium, seed 20261990.

- Alpha-alias readers dropped (debt #1, B first): audio.js venueGigState() loses the alias hop
  (g.venueShopId===id -> g.state); byVenue + clock fallback only. venue.js stateGetter() loses the
  bare-string arm (F passes gigState.byVenue since R13, confirmed index.html:379); seed = update(null).
  Verified live: byVenue {116:on,404:on,490:quiet} at NIGHT drives the marquee with no string arm;
  audio resolves gig-<genreKey> per venue with no alias hop. F is UNBLOCKED to delete the getters.
- Bookmark/tour audit under ?gigs=1: all 19 bookmarks resolve, frame un-letterboxed, 0 page errors,
  <=300 draws. District venue-conversions hijacked none (night_neon->Video Regal, dig_real->Retro
  Groove). No pose fixes needed. (Orphan-chunk teleport inflates tris across sequential shots -- fresh
  counts <=153k; tram_stop's real ~293k is close-range Lane D GLB ped rigs, framing fine, not a venue block.)
- queue_night bookmark added (dbg.js): John's release hero -- close 3/4 on the pub door at seg 5
  ('on'), biased to the queue side; new poseForVenueQueue() recomputes venue.js frontage geometry.
  Frames marquee + window glow + streetlamp pools + the queue (spawned live by the shell loop; F's
  gig_shot.py drives it live -- verified by manually spawning D's VenueQueue: 2 punters at the door).
- Poster-repetition call (debt #4): FINE, no ask to E -- 2 gigs tonight => 2 distinct designs
  (screenprint/Screaming Utes x7, xerox/The Dead Ringers x7); variety scales with venues playing.
- Town-wide streetlamp pools: v3.1 candidate per Fable ruling (venue-scoped for v3.0; no code).

-> Lane F: cross-lane alias sweep is CLEAN (no A/C/D/E reads it); only your own 20 tool lines in
   flags_check.py + gig_shot.py remain -- migrate to *Of(id)/venueShopIds[0] when you delete the
   gig_state.js:141-151 getters + the CITY_SPEC alias line (with A).
-> Lane A (now gates John's hero-shot poster): buildPosters item 1 still seats each venue's own
   frontage poster on local -Z (back wall, 15.9m from the +Z street facade). Flip to +sin,+cos and it
   seats by the door -- in the existing queue_night frame, no pose change. Per "don't touch world
   code," B did not work around it. Re-flagged from R13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:43:22 +10:00
m3ultra
62e38c5df8 Lane C R14 (v3.0 release): venue audit + docs freeze + guitar_amp ruling
Light release round — C interior code unchanged since R13 (venue geometry stable by
construction); this is the audit, the amp ruling, and the v3.0-FROZEN docs.

- Re-verified vs the current tree (D's continuity + RY-fix + real instrument GLBs landed):
  contract byte-identical (caps 8/8/12, deckY/riserY, gigKeys, determinism), drawSweep <=350
  (worst 161), gig-night draws pub 60 / band_room 52 / rsl 71 <=350. Release-law confirmed:
  D's rng namespace is disjoint from C's ctx.stream; no golden can move.
- guitar_amp ruling (LANE_C_PUB §3): NOT blessed as-is. D's pose (stage.z+0.15, down-stage)
  interpenetrates the bass rig by 0.29 m; move it up-stage-right ~ (stage.w*0.36, deckY,
  stage.z - stage.d*0.22), clear of the bass + C's primitive ampStacks. Full amp unification
  = v3.1. Cosmetic + ?gigs opt-in -> not a tag blocker. C->D handshake.
- Docs freeze: LANE_C_PUB.md + LANE_C_AUDIO.md marked v3.0-FROZEN; RY §0 reframed past-tense
  (the +Z fleet was fixed at source in D's buildFigure, R13 - no per-consumer flips); budgets
  updated to R14; in-flight handoffs removed.
- Comment-only code (zero runtime/golden/rng change): fixed stale R12 inline return-shape
  comments (watchPoints 6..8 -> 8/8/12; stage bandPoses[3] -> [4] + riserY) in interiors.js +
  layout.js; documented the standalone gigKey hand-build. theme.js untouched.

Note for E/F: settled gig-night interior is ~182k tris (E instruments 14k each x5); F's smoke
reports ~34k measuring before the async GLBs load. Interior draw gate passes; v3.1 LOD candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:40:42 +10:00
m3ultra
a3911a4590 Lane A round 14: v3.0 release — 400-seed district invariant sweep + CITY_SPEC §v3 frozen
Release round: harden + freeze, no new systems. No golden moved (synthetic 0x3fa36874, gig
0x1f636349 both re-verified frozen); selfcheck.js is the only code touched, so no seeded generation
stream gained a draw (release-round law).

- selfcheck.js: factored the district checks out of gigSuite into districtInvariants(plan,label)
  (behaviour-preserving — hero-seed gigSuite runs byte-identical), then added a 400-seed synthetic
  sweep + the large edge seeds asserting the full district contract each (2-4 venues, >=1 pub, never
  the openLate shop, no band twice/night town-wide, per-venue 4-6 nights, every poster off the
  carriageway). Asserts the seeded count spans {2,3,4} across 400 seeds (146/121/133 — range is
  exercised, not pinned). osm graceful placement: katoomba (19 shops, no warehouse district) still
  lands 2-4 valid venues incl. a pub for every hero seed. Poster check kept at roadWidth/2 (a literal
  poleOffset compare would false-fail corner posters); added -1e-6 epsilon on the strict boundary.
  ALL GREEN 14264/14264 in ~0.7s.
- CITY_SPEC §v3: final wording pass, marked FROZEN at v3.0 (round 14) with a v2-style marker + an
  explicit carve-out for F's pending alias-line deletion. Fixed the roadWidth prose (full carriageway
  width, not 'half-corridor'). Genres final (pubrock/grunge/covers).

Coordination: the R12 single-venue alias line stays in §v3 for F to delete atomically with the
gig_state.js getters (debt #1) — deleting now would be doc-ahead-of-code (alias still live) and race F.
Flagged to F that the runtime block omits nightOf(id); to B that venue.js:228 is a 2nd bare-string
site. Verified: mutation-tested the sweep (non-vacuous — catches 5-venue/7-night/on-road violations)
+ adversarial pass (goldens frozen, freeze matches code, deterministic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:36:31 +10:00
m3ultra
55f6698fb2 Lane E R14 (v3.0 release): provenance freeze + doc-consistency fixes
Provenance close-out for the v3.0 tag. Verified via a 6-agent inventory+audit workflow
(both adversarial audits PASS, 0 blockers) cross-checked against glb_stat/ffprobe/manifest.

- LANE_E_NOTES: v3.0-FROZEN provenance table + full audio(28)/GLB(38)/skins(96)/packs(3)
  inventories; by-design notes + v3.1 deferrals (street_bin decimation).
- AUDIT.md: add R13 gig-layer finalization section (14k decimation, _hi published, gigKey
  rename + 2 beds, depot publish done) — ledger was stale at R12.
- manifest glb_law string: state the documented <=8k-hero / <=14k-instrument tiers so the
  frozen contract is self-consistent (regenerated; 1-line diff, Gate 3 still 0/0).
- gen_audio: roomtone-video gain 0.3 -> 0.35 to match the shipped manifest.

Gate 3 GREEN; publish.py --verify clean; 0 quarantine assets in manifest; goldens unmoved
(synthetic 0x3fa36874, gig 0x1f636349). No new synthesis (genres final); no poster templates
(no ask from B).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:18:40 +10:00
23 changed files with 776 additions and 131 deletions

View File

@ -3,7 +3,54 @@
*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-15 (round 13) · owner: PROCITY-C · reviewer: Fable
Last updated: 2026-07-16 (round 14) · owner: PROCITY-C · reviewer: Fable
---
## 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 12 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.
---

View File

@ -1,9 +1,39 @@
# LANE A — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check all-green (3273/3273); rounds 213 closed** (see below).
Node QA gates (selfcheck · scaffold · consistency) GREEN. v3.0-beta DISTRICT interface published (24
venues, week schedule, town-wide posters); gig golden `0x1f636349`. Strict 6/6 is F's end-of-round gate.*
*Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)*
*Status: **all deliverables landed; self-check all-green (14264/14264); rounds 214 closed** (see below).
v3.0 RELEASE round: 400-seed district invariant sweep landed + CITY_SPEC §v3 frozen at `v3.0`. Goldens
unchanged (synthetic `0x3fa36874`, gig `0x1f636349`) — no golden moved, no rng draw added this round.*
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 freeze
Release round, Lane A scope = harden + freeze (no new systems). **No golden moved** (synthetic
`0x3fa36874`, gig `0x1f636349` both re-verified frozen); the only code touched is `selfcheck.js`
(test-only), so no seeded stream gained a draw — the release-round law holds.
Delivered:
1. **400-seed district invariant sweep** (`selfcheck.js`, ALL GREEN **14264/14264** in ~0.7s). Factored
the district checks out of `gigSuite` into `districtInvariants(plan,label)` (behaviour-preserving — the
4 hero-seed gigSuite runs are byte-identical), then swept **seeds 1400 + the large edge seeds**
asserting the whole contract each: 24 venues, ≥1 pub, never the openLate shop, no band twice per night
town-wide, per-venue 46 nights, every poster off the carriageway. Added a distribution assert that the
seeded count **spans {2,3,4}** across 400 seeds (146/121/133) — proves the range is exercised, not pinned.
`structuralSuite` still runs on the 6 hero SEEDS + osm parity (unchanged); the sweep runs the light
district checks only, so 400 gig-on generations stay ~0.5s.
2. **osm graceful placement** in the sweep: `katoomba` (19 shops, **no warehouse district**) still lands
24 valid venues incl. a pub for every hero seed — proving `pickVenues`' off-spine fallback. Honest
scope noted: both osm fixtures keep a main spine, so this proves the no-warehouse branch (not no-spine).
3. **Poster-check hardening**: kept the `roadWidth/2` nearest-edge formulation (a literal `poleOffset`
compare would false-fail corner posters); added a `1e-6` epsilon on the strict `<` to immunise the
exact kerb-line boundary against r2 rounding (only reachable on a no-main-spine fallback; not hit today).
4. **CITY_SPEC §v3 frozen at `v3.0`** — §v2-style FROZEN marker, final wording pass over venues table,
genre map, gigKey contract, corridor law, gig golden. Genres final (pubrock/grunge/covers, Fable's ruling).
Coordination: the R12 single-venue **alias line stays in §v3 for F to delete atomically** with the
`gig_state.js` getters (debt #1) — deleting it now would be doc-ahead-of-code (alias still live) and would
race F on the same lines; the freeze marker carves it out explicitly. Flagged to F that the runtime block
omits `nightOf(id)`, and to B that `venue.js:228` is a second bare-string edit site. **Not touched:**
`web/assets/manifest.json` shows a concurrent Lane E `glb_law` edit in the shared tree — left for E.
## Round 13 (2026-07-15) — v3.0-beta THE DISTRICT: 24 venues + the week + town-wide posters + verge fix

View File

@ -204,7 +204,15 @@ Interiors are **not** street geometry. Walking into a doorway triggers
exactly where you stood. Street windows show a cheap fake (tinted glass + emissive warm glow at
night in v1; interior-mapping shader is a stretch goal).
## CityPlan v3 — gig layer (`?gigs=1`) — v3.0-beta (THE DISTRICT)
## CityPlan v3 — gig layer (`?gigs=1`) — THE DISTRICT
> **FROZEN at `v3.0` (round 14).** The authoritative Layer-1 **gig-layer** contract — venues, the week's
> schedule, posters, the `gigKey` map, the corridor law, and the runtime surface. `web/js/citygen/selfcheck.js`
> enforces every law here, including the round-14 **400-seed district invariant sweep** + an osm
> graceful-placement case. Changes are a **CITY_SPEC amendment in the same commit + a gig-golden re-pin**,
> never silent drift. **One edit is still pending and lands with Lane F's alias-retirement commit** (R13
> debt #1): the R12 single-venue compat alias in the runtime block below retires; everything else here is
> final for the `v3.0` tag.
> **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector only when
> `?gigs=1`. With the flag OFF, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan —
@ -281,7 +289,8 @@ places E's poster skin at `(x, z)` rotated `ry`; F wires "poster → that venue'
the **full corridor** (carriageway + verge/footpath, centreline ± width/2), never the carriageway alone —
so a naive `node.x + 2` seats furniture mid-bitumen. `web/js/core/registry.js` now exports the split as
pure functions over an edge (no plan fields change ⇒ base goldens stay frozen), re-exported from the
barrel: **`roadWidth(edge)`** (carriageway half-corridor: main 10 m, side 68, lane = all, arcade 0),
barrel: **`roadWidth(edge)`** (full carriageway width — main 10 m, side 68, lane = all, arcade 0; the
kerb sits at `roadWidth/2` from the centreline),
**`vergeBand(edge) → [inner,outer]`** (where footpath furniture lives), **`poleOffset(edge)`** (where a
pole/poster seats — just behind the kerb). Posters, poles, verandah posts belong in the verge band; the
selfcheck asserts no poster stands within `roadWidth/2` of any edge centreline.

View File

@ -2,6 +2,47 @@
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 frozen
Release round — no new systems, no golden moves. **Synthetic `0x3fa36874` and gig `0x1f636349` are
unchanged**; the only code I touched is `selfcheck.js` (test-only), so no seeded stream gained a draw.
### The 400-seed district sweep (`selfcheck.js`, ALL GREEN 14264/14264 in ~0.7s)
- Factored the district-contract checks out of `gigSuite` into `districtInvariants(plan, label)` so the
sweep asserts them at SCALE without re-running the heavy `structuralSuite` (that still runs on the 6
hero `SEEDS` + osm parity). Behaviour-preserving — the 4 hero-seed `gigSuite` runs are byte-identical.
- Sweeps **seeds 1400** synthetic + the large edge seeds (2e9, 8675309) asserting the full district
contract each: 24 venues, ≥1 pub, never the openLate shop, no band twice in one night town-wide,
per-venue 46 nights, every poster off the carriageway. Also asserts the seeded count **spans {2,3,4}**
across the range (measured 146/121/133) — proves the 24 range is genuinely exercised, not pinned.
- **osm graceful placement**: `katoomba` (smallest fixture, 19 shops, **no warehouse district**) still
lands 24 valid venues incl. a pub for every hero seed — proving `pickVenues`' off-spine fallback.
Honest scope: both osm fixtures keep a main spine, so this proves the *no-warehouse* branch, not
*no-spine* (a genuine no-spine town would need a single-row fixture that doesn't exist yet — v3.1).
- Poster check kept as "≥ `roadWidth/2` from the nearest edge centreline" (not a literal `poleOffset`
compare, which would false-fail legit corner posters seated behind their placement edge's `poleOffset`
but inside a crossing edge's clearance band). Added a `1e-6` epsilon on the strict `<` to immunise the
exact kerb-line boundary against r2 rounding (only reachable on a no-main-spine fallback; not hit today).
### CITY_SPEC §v3 frozen at v3.0
Final wording pass; marked **FROZEN at `v3.0` (round 14)** with a §v2-style marker. Venues table, genre
map, gigKey contract, corridor law, and gig golden `0x1f636349` are all final (genres are pubrock/grunge/
covers — Fable's R14 ruling, John did not re-flavour).
### → Lane F: the alias-retirement handshake (R13 debt #1) — I deliberately did NOT touch it
The R12 single-venue compat alias is **still live** (`gig_state.js:141-151`, read by `audio.js`/`venue.js`),
so I left the §v3 alias sentences (`CITY_SPEC.md` ~lines 271272) **intact** — deleting them at freeze
time would be doc-ahead-of-code and would race you on the same lines. The freeze marker explicitly carves
this out as your pending edit. In your alias-retirement commit (after B drops its readers), delete both
`gig_state.js:141-151` and those two spec sentences **together** — one atomic step, one deleter, no race.
- While you're in that runtime block: it enumerates `stateOf/onOf/openOf/gigOf/coverOf/bandNameOf/paidOf`
+ `markPaidOf` + `.venueShopIds`, but **omits `nightOf(id)`** (`gig_state.js:131`). Add it when you edit.
### → Lane B: second edit site for the bare-string arm
When you remove the `venue.update()` bare-string alias arm, note `venue.js:228` also self-calls
`update('quiet')` at construction through that same arm — fix both or it falls through (harmless today:
still yields `'quiet'`, but it's a live second site).
## Round 13 (2026-07-15) — v3.0-beta THE DISTRICT (published FIRST; C/D/B/F hang off this)
Supersedes the R12 alpha interface below. Same flag (`?gigs=1`), same prime-flag law: flag OFF ⇒ plan

View File

@ -1,5 +1,76 @@
# LANE B — NOTES (measured)
## Round 14 (Fable's ROUND14 → Lane B) — v3.0 RELEASE: alias retirement + the tour audit
Release round — no new systems. Three B files touched (`audio.js`, `venue.js`, `dbg.js`); no world
geometry, no rng, goldens frozen (`0x3fa36874` synthetic, `0x1f636349` gig — neither moved). Verified
fresh Chromium, seed 20261990, `?gigs=1`.
### 1. Alpha-alias readers dropped (debt #1 — B first; **F unblocked**)
The R12 `window.PROCITY.gigs` scalar alias (`.state/.on/.venueShopId/.cover/.paid/…`) is retired this
round. B's readers removed:
- **`audio.js`** `venueGigState()` — dropped the alias hop (`g.venueShopId === shopId → g.state`).
Now byVenue-only + the clock fallback: `g.byVenue[id]` → else `plan.gigs` night-0 vs the segment.
- **`venue.js`** `stateGetter()` — dropped the bare-string arm; reads `gigStates.byVenue || gigStates`
(F passes `gigState.byVenue`, confirmed at `index.html:379`). Seed call is now `update(null)`.
Verified live: F's `gigState.byVenue` reads `{116:'on', 404:'on', 490:'quiet'}` at NIGHT (pub + RSL
playing, band_room dark); passing it to `venue.update()` lights the marquee with **no string arm**, and
`audio.js` resolves each venue's `gig-<genreKey>` with **no alias hop**. Audio engine loads `live`.
**→ Lane F (you can now delete the alias):** an alias-surface sweep (all of `web/js/**`, `index.html`,
`tools/**`) confirms **CROSS-LANE CLEAN** — no A/C/D/E file reads the scalar alias (interior_mode.js
uses `onOf()/gigOf()`; sim.js uses shell-driven `setGig`). The only remaining alias readers are **your
own 20 lines** in `tools/flags_check.py` + `tools/qa/gig_shot.py` (`.venueShopId`×12 → `venueShopIds[0]`
or an explicit id; `.state`×5 → `stateOf(id)`; `.cover`×2 → `coverOf(id)`; `.bandName`×3 →
`bandNameOf(id)`; `gig_shot.py:81` already prefers `stateOf(id)`). Delete the alias getters at
`gig_state.js:141151` + migrate those tool lines in the same commit; `!!P.gigs` existence checks keep
working (the per-venue machine still exists). CITY_SPEC §v3 runtime block loses the alias line (with A).
### 2. Bookmark / tour audit under `?gigs=1` — **all clean, no pose fixes**
All 19 bookmarks (16 v2 + `venue_night` + `district_posters` + new `queue_night`) resolve, frame
**un-letterboxed**, **0 page errors**, ≤300 draws. The district's in-place venue conversions hijacked
**no** bookmark: `night_neon`→"Video Regal" (openLate is never converted), `dig_real`→"Retro Groove"
(a real record shop survives — multiple per type). Framing eyeballed clean on the tour spread:
`street_noon`, `venue_night`, `queue_night`, `tram_stop`, `district_posters`, `night_neon`. **No dbg.js
pose changes were needed** beyond adding `queue_night`.
- **Measurement gotcha (re-confirmed):** `DBG.shot` teleports, so shooting many bookmarks in sequence
leaves orphan chunks that inflate tris on later shots (saw 238k on `patronage_door`; **42k** on a
fresh load / shoot-twice-to-settle). Shoot from a clean load, or twice, for the true per-frame count.
- **Observation → Lane D / Fable (not a Lane B fix):** `tram_stop` is a stable **~293k tris** — real,
from close-range full-detail GLB ped rigs on the footpath (framing is fine). It's not a venue block
(F's tri gate measures at the venue block, ~64k), so it likely doesn't trip the gate, but it exceeds
the 200k reference for a tour shot; worth a look if a close ped shot lands in the release set.
### 3. `queue_night` — John's release hero (`dbg.js`)
New bookmark `queue_night` (seg 5 / NIGHT / 'on'): a close 3/4 on the pub door, biased to the venue's
`+right` (the queue side). New `poseForVenueQueue()` recomputes venue.js's frontage geometry from the
pub lot: `DIST 7 m` out on the street, `SIDE = mw/2 + 2.2 m` to the queue side, eye 2.2 m, looking at
the door lifted to y2.7 (catches the marquee). Verified: frames marquee + window glow + streetlamp
pools + the queue. **The queue is spawned by the shell's live rAF loop** (`index.html:383` builds D's
`VenueQueue` on `openOf(id)`), so F's `gig_shot.py` must drive it **live** (the automation rAF is
throttled — I confirmed the pose + D's line by manually spawning `VenueQueue` at the pub queueZone: 2
punters seat at the door, facing it).
- **→ Lane A (blocks the hero's poster):** `queue_night` frames door + marquee + queue, but **no poster
lands in the close frame** — the pub's own-frontage poster is still on A's local **Z** (the BACK
wall, `gigs.js buildPosters` item 1 `-sin,-cos`; measured 15.9 m from the +Z street facade), and the
nearest spine poster is 53 m away. Per Fable's "fix poses, don't touch world code," B did **not**
work around it. **Flip item 1 to `+sin,+cos`** and the frontage poster seats by the door — in the
existing `queue_night` frame with no pose change. (Re-flagged from R13; now it gates John's hero shot.)
### 4. Poster-repetition call (debt #4) — **FINE, no ask to E**
Tonight's 14 posters carry **2 distinct designs** (Screaming Utes / screenprint ×7, The Dead Ringers /
xerox ×7 — different skins, different bands). Variety = number of venues playing tonight (one seeded
skin per gigId), so a spine run reads as two real band bills, not a repeat. Only a town where a single
gig plays everywhere would look monotonous (rare, and reads as "a residency"). No new templates needed.
### 5. Town-wide streetlamp pools — v3.1 candidate (Fable ruling, no code)
Fable ruled the night pools **stay venue-scoped for v3.0** (town-wide needs always-on `furniture.js`
changes that fight flags-off parity in release week; venue blocks read fine — `venue_night` proves it).
Parked here as a **v3.1 candidate** alongside wind sway and bench-sit loiter. No R14 code.
---
## Round 13 (Fable's ROUND13 → Lane B) — the district reads by night (`?gigs=1`, v3.0-beta)
Four B pieces, all gig-gated (flags-off byte-identical, verified). Files: `venue.js` (rewrite),

View File

@ -1,8 +1,16 @@
# LANE C — audio contract (round 11) → for Lane F + Lane B
# LANE C — interior audio contract (v3.0-FROZEN) → for Lane F + Lane B
*PROCITY-C, 2026-07-15. Answers the R11 §Lane C question: "where should the interior bed + room-tone
attach?" **Confirmed**`buildInterior` now returns `room.audio = { musicKey, toneKey }`, seeded per
shop. Lane F's interior_mode just plays it; Lane B's audio.js resolves the keys → files.*
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane F + Lane B. Describes the shipped state; changes
> require a version bump.
*`buildInterior` returns `room.audio = { musicKey, toneKey }`, seeded per shop. Lane F's interior_mode just
plays it; Lane B's audio.js resolves the keys → files.*
> **Scope:** this doc covers the **base interior audio** (the seeded music bed + room-tone every shop
> returns). The **gig audio**`room.audio.gigKey`, the live-band bed, canonical form `gig-<genreKey>`
> (`gig-pubrock` / `gig-grunge` / `gig-covers`), present only when `opts.gig` is on — is specified in
> [LANE_C_PUB.md](LANE_C_PUB.md) §1. `room.audio` carries `gigKey?` as a third, optional key alongside the
> two below.
## The contract
```js
@ -42,9 +50,9 @@ on exit / dispose(room):
Nothing before the first gesture; `?mute=1`/`?noassets=1` short-circuit before any fetch. `room.audio`
is plain data on the return — no new lifecycle, no dispose hook needed from Lane C's side.
## Optional (deferred to v2.2, per the round scope)
## Deferred idea (v3.1+, not in the frozen contract)
A `places`-tagged visual source ("the music comes from *there*") — e.g. a radio/hifi mesh with
`userData.audioEmitter = musicKey` so Lane B can position/spatialise the bed. Only the record shop has a
fitting for it today (`listeningCorner`); milk-bar/video would need a new radio prop. Not in this round —
proposing the `userData.audioEmitter` hook + the two props as a tidy v2.2 item. v2.1 plays the bed
non-spatially (room-filling), which is the right default for an interior anyway.
fitting for it today (`listeningCorner`); milk-bar/video would need a new radio prop. A tidy v3.1 item: the
`userData.audioEmitter` hook + the two props. v3.0 plays the bed non-spatially (room-filling), which is the
right default for an interior anyway.

View File

@ -1,66 +1,48 @@
# LANE C — venue interfaces (round 13, v3.0-beta: the district) → Lane D + Lane F
# LANE C — venue interface contract (v3.0-FROZEN: the district) → Lane D + Lane F
*PROCITY-C, 2026-07-15. R13 §Lane C. The district ships **three** venue archetypes behind `?gigs=1`
`pub` (R12), and new this round **`band_room`** and **`rsl`**. A converts a chosen shop to a venue kind in
place (`shop.type = kind`); C keys the interior recipe off `shop.type`. All three return the SAME shape
(below), so Lane D drops rigs in and Lane F reads state with no per-kind branching. Non-venue rooms are
byte-identical (venue code only runs for `recipe.venue`, on its own seed sub-streams).*
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane D + Lane F. Describes the *shipped* state;
> changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (release audit).
*Verified (fresh context, seeds 20261990/1990/7/42/101 + 90-build venue soak): 0 throws · 0 carves ·
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Draws below.*
*The district ships **three** venue archetypes behind `?gigs=1``pub`, `band_room`, `rsl`. Lane A
converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off
`shop.type`. All three return the SAME shape (below), so Lane D drops rigs in and Lane F reads state with
no per-kind branching. Non-venue rooms are byte-identical (venue code runs only for `recipe.venue`, on its
own seed sub-streams). Verified fresh (seeds 20261990/1990/7/42/101 + 90-build soak): 0 throws · 0 carves ·
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Budgets in §3.*
---
## 0. The pinned pose `ry` convention (ROUND13 debt #4) — READ THIS FIRST
## 0. The pose `ry` convention (pinned) — READ THIS FIRST
**Canonical convention (all of C's poses use it):** a pose's `ry` is the yaw of a thing whose **front /
**Canonical convention (every C pose uses it):** a pose's `ry` is the yaw of a thing whose **front /
outward normal is local Z at ry=0**, i.e. world-facing `(sin ry, cos ry)`. `counter.stand`,
`browsePoints`, `watchPoints` and `bandPoses` ALL follow it, and the box placeholder (`placeholder.js`) is
built to it (toes at Z). This is the long-standing convention `keepers.js` greet math already assumes.
built to it (toes at Z). `keepers.js` greet math assumes it. **Drop a rig in at a pose's `ry` and it faces
the right way — no per-consumer flip.**
**The GLB fleet mesh is the outlier — it faces local +Z at ry=0.** Verified two ways in a fresh context:
- toe-bone probe (`toe foot`, the anatomically-anterior direction) on 5 peds → `(0.05..0.14, +0.99)` = **+Z**.
- planting a fleet rig at C's `counter.stand.ry` via `spawnRig` (no flip) and dotting its forward against
the intended customer-facing direction → **0.99 (180° backward)**; at `ry+π` → +0.99.
- for contrast the box placeholder's foot mesh sits at z 0.076 → **Z** (matches the convention).
**Finding: with the GLB fleet loaded, keepers AND browsers have been facing 180° backward since the fleet
was wired in.** `keepers.js` spawns rigs at `stand.ry` / `browsePoint.ry` with **no flip**, and the mesh
front is +Z, so the keeper faces the bar wall and the browser faces the aisle instead of the goods. The R12
band reads correctly only because `band.js` added `RY_FLIP = π`. The two actor kinds are **opposite**
(placeholder Z, GLB rig +Z), so the current per-consumer state is inconsistent both ways:
| consumer | applies | placeholder (Z front) | GLB rig (+Z front) |
|---|---|---|---|
| `keepers.js` (keepers, browsers) | no flip | ✅ correct | ❌ **backward** |
| `band.js` (band, crowd) | `+RY_FLIP` (π) | ❌ backward | ✅ correct |
**Recommended canonical fix (Lane D — visual only, NO golden moves, no C poses change):** normalise the
fleet at the source. In `rigs.js buildFigure`, rotate the cloned `inner` by π about Y so every GLB rig's
front becomes **Z**, matching the placeholder and every C pose. Then:
- `keepers.js` stays flip-free and is now correct for GLB too (fixes keepers + browsers);
- `band.js` **drops** `RY_FLIP` (set to 0) — the poses already face right under the Z convention;
- placeholder and GLB modes become consistent (no more mode-dependent facing).
One rotation at the source, delete every consumer flip. C's data does not move (pure poses, all Z already),
so no golden is touched — it's the visual fix debt #4 anticipated. Repro handed to D in `C-progress.md`.
*(If D prefers to keep the flip per-consumer instead, the equivalent is: ADD `RY_FLIP` to `keepers.js`
spawns — but that leaves placeholder-mode keepers backward, so the source-normalise is the clean one.)*
**Why no flip (the R13 fix, for the record):** the GLB fleet meshes ship facing local **+Z** at ry=0 — the
opposite of the Z convention (verified: the anterior toe-bone vector `toe foot` reads `(≈0, +0.99)`).
Before R13 this was compensated per-consumer, inconsistently: `band.js` flipped by π (band/crowd correct,
placeholders backward) while `keepers.js` did not (placeholders correct, **keepers + browsers 180° backward
whenever the GLB fleet was loaded** — they'd been shipping back-to-front). **D fixed it at the source in
`rigs.js buildFigure`: the cloned `inner` is rotated π about Y so every GLB rig faces Z**, matching the
placeholder and every C pose. All consumer flips were then deleted. Net: keepers, browsers, band and crowd
all face correctly, GLB and placeholder modes are consistent, and C's pure-data poses never moved (no golden
touched). If you add a new rig consumer, plant it at the pose `ry` with no flip.
---
## 1. What `buildInterior` returns for a venue (`shop.type` ∈ `pub` | `band_room` | `rsl`)
Everything below is **pure data on the return** (room-local coords, the ry convention pinned in §0). Lane D
drops rigs straight in; Lane F reads state — no new lifecycle vs. the R9 keeper/browser seam.
Everything below is **pure data on the return** (room-local coords, the ry convention in §0). Lane D drops
rigs straight in; Lane F reads state — no new lifecycle vs. the R9 keeper/browser seam.
### `room.stage` — the band stage, now a **4-PIECE** (ROUND13 debt #3, null for non-venues)
### `room.stage` — the band stage, a **4-piece** (null for non-venues)
```js
room.stage = {
x, z, // deck centre (against the back wall)
w, d, // deck size (m) — varies by kind (table below)
deckY, // deck top surface height (m) — front line stands here
deckY, // deck top surface height (m) — the front line stands here
riserY, // drum riser top height (m) = deckY + 0.16 — the drummer sits here
frontZ, // z of the deck lip (the crowd stands in front of this, +Z)
bandPoses: [ { x, z, ry, role, y, seated? } ×4 ],
@ -68,14 +50,13 @@ room.stage = {
```
- `bandPoses[0..2]` = the **front line**: `role``guitar` / `vocal` / `bass`, across the deck lip, `y = deckY`.
- `bandPoses[3]` = the **drummer**: `role: 'drums'`, `seated: true`, up-stage centre on the drum riser,
`y = riserY` (= `deckY + 0.16`, the `fittings.js` stage riser). The riser mesh already exists in the stage
fitting — the pose just seats the kit + drummer on it.
- **Lane D:** lift each member to **its own `pose.y`** (was `stage.deckY`; front line is unchanged, the
drummer now sits 0.16 m higher on the riser). `role` hints the instrument — E's GLBs via `opts.instrumentFor(role)`
(electric_guitar/bass_guitar/mic_stand/drum_kit), primitives otherwise (asset law). `seated:true` → seated bob.
- `ry = π` faces the audience (+Z) **under the pinned Z convention** → apply your fleet flip per §0.
- **John may veto to a front-line trio** (charter): say so before you start and C ships `bandPoses` length 3
(drop `[3]`), and F amends its `band==` smoke back to 3.
`y = riserY` (= `deckY + 0.16`, the `fittings.js` stage riser box).
- **Lane D:** lift each member to its own `pose.y` (front line = `deckY`, drummer = `riserY`). `role` hints
the instrument — E's GLBs via `opts.instrumentFor(role)` (electric_guitar/bass_guitar/mic_stand/drum_kit),
primitives otherwise (asset law). `seated:true` → seated bob. `ry = π` already faces the audience (+Z) — no
flip (§0).
- The band is a **4-piece** for v3.0 (front-line trio + drummer). Reverting to a trio would be a version bump
(drop `bandPoses[3]`), not a runtime toggle.
### `room.watchPoints` — the audience, **cap by kind** (empty `[]` for non-venues)
```js
@ -88,10 +69,10 @@ room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // seeded floor poses, a
stand + browse points + stage/PA. Deterministic per `shop.seed`.
### `room.counter` / keeper — unchanged
The bar is a normal counter; the barkeep is just a keeper at `room.counter.stand` (R9 seam). *(Subject to the
§0 facing fix — the keeper is one of the actors currently backward with the GLB fleet.)*
The bar is a normal counter; the barkeep is just a keeper at `room.counter.stand` (R9 seam). Faces the
customer correctly under §0.
### `room.audio.gigKey` — the live bed (ROUND13 debt #1)
### `room.audio.gigKey` — the live bed
`room.audio = { musicKey, toneKey, gigKey? }`. The manifest key **is** the gigKey, canonical form
**`gig-<genreKey>`** — no mapping table. F passes gig state via `opts.gig`:
```js
@ -99,10 +80,12 @@ buildInterior(rslShop, THREE, { gig: { on: true } }) // → room.audio.gigKey
```
- Genre resolves `opts.gig.genreKey` → the venue's own `shop.genreKey` (A sets it) → a kind default
(`pub`→pubrock, `band_room`→grunge, `rsl`→covers). Present **only when a gig is on**.
- **Prefer `gigKey` over `musicKey`** while the gig plays. **Lane E** names the beds `gig-pubrock`,
`gig-grunge`, `gig-covers`; a missing key → silence (audio law), band still visible.
- **Prefer `gigKey` over `musicKey`** while the gig plays. E's beds are `gig-pubrock` / `gig-grunge` /
`gig-covers`; a missing key → silence (audio law), band still visible.
- C hand-builds `'gig-'+genreKey` (byte-identical to citygen's `gigKeyFor`, selfcheck-asserted) rather than
importing it — deliberately, to keep the interiors lib standalone (it never imports citygen).
### The venue table (C-owned tuning, FYI)
### The venue table (C-owned tuning)
| kind | archetype bias | ceiling | deck `w × d`, `deckY` | crowd cap | genre → gigKey | dressing |
|---|---|---|---|---|---|---|
| `pub` | wide / hall | 3.6 m | `0.58·W × 2.0`, 0.32 | 8 | pubrock → `gig-pubrock` | pub tables, sticky carpet, bar |
@ -114,19 +97,39 @@ No `opts.gig` → a normal interior: `stage` + `watchPoints` are still returned
`gigKey`** (room-tone + maybe the seeded radio `musicKey`), and D spawns no crowd. A seeded dark night reads
true — the venue is just a quiet pub/club.
## 3. Budgets (measured, seed 20261990, fresh context)
- **Empty shells** (`drawSweep` glb-off): band_room **124** · pub **126** · rsl **135** — town-wide worst
161 (opshop/hall). ≤ 350. ✓
- **Gig-night incl. FULL crowd + 4-piece** (worst-case framing from the door): RSL **237 draws** asset-free
(16 rigs: 12 crowd + 4 band) / **77** GLB, **40.9k tris**. pub 176/56. band_room 173/53. ≤ 350 draws /
200k tris. ✓ *(Measured with all 12 RSL watchPoints filled — band.js still caps crowd at 8 in R12; raising
it to the watchPoint cap is D's R13 item, so this is the forward-looking worst case.)*
- **90-build venue soak** (pub/band_room/rsl mixed): 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms;
leak geo 0 / tex 0.
## 3. `guitar_amp` placement ruling (v3.0)
## 4. The archetypes (C-owned, FYI)
D plants E's `guitar_amp` GLB (`band.js _addBackline`) at `(stage.x + stage.w·0.32, deckY, stage.z + 0.15)`.
**Ruling: reposition up-stage — this pose is a v3.1 fix for D, not blessed as-is.** That `z = stage.z + 0.15`
is **down-stage** (audience side): the 0.5 m amp lands 0.29 m from the bass rig, so it **interpenetrates the
bassist** and reads as clutter, not a backline. Move it **up-stage** behind the front line and clear of both
the bass rig *and* C's own primitive ampStacks. C's `placeStage` plants **12 primitive `ampStack` fittings**
at `(±stage.w·0.28, deckY, stage.z stage.d·0.18)` (up-stage, always rendered, the asset-free backline
fallback — no GLB slot, so they stay primitive). A clean spot for D's GLB: **`(stage.w·0.36, deckY, stage.z
stage.d·0.22)`** (up-stage-right, clears the bass and offsets from the primitives). With the GLB up-stage it
reads as a flanking backline with C's primitive. *Redundancy note:* the primitive + GLB coexist (23 amps);
fully unifying them — C exposes a `stage.backline[]` slot list that D's GLB fills, replacing the primitive —
is a **v3.1** item (no new contract fields in the frozen v3.0). Since the interpenetration is cosmetic and
`?gigs=1` is opt-in, it does not block the v3.0 tag; D corrects the pose when convenient.
## 4. Budgets (measured R14, seed 20261990, fresh context)
- **Empty shells** (`drawSweep` glb-off): band_room **124** · pub **126** · rsl **135** — town-wide worst
161 (opshop/hall). ≤ 350. ✓ (identical to R13 — C geometry unchanged).
- **Gig-night interior draws**, D's `GigCrew` with the real instrument GLBs + amp (localdepot): pub **60** ·
band_room **52** · rsl **71**. Asset-free (placeholder crowd, no GLBs) worst RSL **237** (16 rigs). ≤ 350. ✓
- **Tris**: fleet peds ~2.7k each; E's instrument GLBs **14k each** (decimated to the ≤15k target), 5 on a
stage = 70k — the instruments dominate. RSL gig-night ≈ **182k tris** once the async GLBs settle. The
interior gate is **draws** (passes); tris are the *street* budget (200k), so this is not an interior gate.
*Note for E/F:* F's gig-night smoke reports ~34k because it measures **before** the async instrument GLBs
load; the settled scene is ~180k. A camera-distance LOD on instruments is a v3.1 candidate.
- **Determinism / release-law**: C's interior code is unchanged since R13; D's continuity + RY-fix ride
disjoint rng namespaces (`ctx.stream(salt)` vs `rng(citySeed,kind,id)`) and cannot perturb C's placement
or any golden. 90-build venue soak: 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo0/tex0.
## 5. The archetypes (C-owned)
`band_room` = a tin shed: small (cosy/pokey), low ceiling, barely-a-riser deck, near-empty standing floor,
bar in the corner — the PA is louder than the room. `rsl` = carpet-and-flock club: the biggest floor in
town (hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest
crowd cap. Both reuse the `pub` venue machinery (`theme.js venueSpec` + `layout.js` placeStage/placeWatchPoints);
only the dressing + `venueSpec` differ. Stage/PA/amp keep their GLB slots for Lane E (primitive fallback holds).
bar in the corner — the PA louder than the room. `rsl` = carpet-and-flock club: the biggest floor in town
(hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest crowd
cap. Both reuse the `pub` venue machinery (`theme.js venueSpec` + `layout.js` placeStage/placeWatchPoints);
only the dressing + `venueSpec` differ. Stage/PA keep GLB slots for Lane E (primitive fallback holds); the
primitive ampStacks are the backline fallback (see §3).

View File

@ -6,6 +6,79 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
---
## ROUND 14 — v3.0 release: identity continuity (the ped you followed is in the crowd)
*PROCITY-D, 2026-07-16. The last charter promise. Behind `?gigs=1`; files `sim.js`, `band.js`, `queue.js`.
**No golden moves** — both goldens are plan-gen hashes (independent of runtime crowd identity), and an
empty roster is byte-identical to R13. Verified in-shell against A's 3-venue plan.*
### The tonight roster (sim.js) — who came to the gig
A per-venue **Map<key,{pedIndex,height}>** (`_venueRoster`) of the identities who entered a gig venue
**tonight**, from two sources:
- **Surge occupants** — a streamed ped that patronage steers into a gig venue self-records in `_enter`
(gated `this._gigVenues.has(shopId)`), keyed by the ped's stable `c.id` so re-entry doesn't dup. Unlike
R9 occupancy (which clears on emerge), the roster **persists past the ped's dwell** — "who came tonight".
- **Queue admits** — F relays each `VenueQueue.admitOne()` via `citizens.recordVenueEntry(venueShopId, {pedIndex,height,key})`.
It's **bounded + per-night**: cleared automatically on the `setGig(id, false)` on→off transition (the gig
ends → the crowd disperses), so it survives interior exit/re-enter (gig stays on) but resets each night.
Pure bookkeeping — **no rng draw, no GPU** (draw/tri delta 0).
### The crowd consumes it (band.js) — the front slots become the roster
`GigCrew.spawn(roomGroup, { …, roster })`: crowd slot `i` fills from `roster[i]` (its `{pedIndex,height}`),
seeded strangers fill the remainder. This is the release-round's load-bearing safety: each crowd slot is
an **independently-keyed rng stream** (`gig`/`crowd:<gid>:<slot>`), and `_make`'s existing override branches
already skip a slot's own height+pickRig draws when identity is supplied — so a roster override touches
**only that slot**, never another slot / the sway-phase stream / the band / either golden. An **empty
roster is byte-identical to R13** (verified). Roster heights are the sim's 1.551.95 m, inside the
no-giants [1.4,2.0] gate. `crew.spawn` returns `{ band, crowd, fromRoster }` and each member carries
`member.pedIndex` (for F's continuity smoke — assert on the seam, not pixels).
### → Lane F: the seam (three touches; the clear is automatic)
```js
// 1. interior — pass the venue's roster into the crowd (interior_mode.js:239):
crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints,
gig: tonight, roster: citizens.tonightRoster(shop.id) });
// 2. street — relay each queue admit into the roster (index.html per-venue loop ~380-393):
q.spawn(scene, { queueZone, gigId, onAdmit: (id) => citizens.recordVenueEntry(venueShopId, id) });
// 3. nothing — the roster auto-clears on your existing setGig(id, false) at the gig's end.
```
Accessor for your smoke: `window.PROCITY.citizens.tonightRoster(venueShopId) → [{pedIndex, height}]`
(insertion-ordered, deduped, bounded). Assert **crowd ⊇ roster ∩ cap** via `crew.crewInfo.fromRoster` +
`member.pedIndex ∈ roster`. `admitOne()` now returns `{pedIndex,height,key}` (was `1`) — truthy, so your
existing `if (q.admitOne())` still holds. Also: `crew.spawn` is now null-`gig`-safe (quiet-night guard).
### Verified in-shell (seed 20261990, The Exchange Hotel #116)
| check | result |
|---|---|
| roster API — record / dedup-by-key / invalid-pedIndex-ignore / per-venue / clear-on-gig-off | all ✓ |
| **surge continuity** — real peds ducked in → roster → crowd | **8/8** (same pedIndex + height) |
| **queue continuity** — admits relayed via `onAdmit` → roster | **4/4** |
| consume — front slots = roster, tail = seeded strangers unchanged | ✓ |
| **determinism** — empty-roster crowd == R13 baseline (pedIndex + stature per slot) | byte-identical ✓ |
| **soak** — 7 nights × 3 venues, enter/exit + re-spawn | leak **geo Δ0 / tex Δ0**, roster **bounded** (≤ cap, cleared per night), **no identity churn** |
| budget | interior gig **64 draws ≤350**, **0 new rigs** (continuity is bookkeeping) |
| `?noassets=1&gigs=1` | crowd = placeholders using roster heights, **no crash, 0 fetch** |
### Debt #2 (the drummer sit clip) — INVESTIGATED, deferred to v3.1 (real blockers, not a punt)
The clip **does exist**`~/Documents/mixamo-fetch/out/` is NOT on m3ultra (Fable's path is stale; the
bank lives on the tailnet source box `johnking@100.91.239.7`). `Sitting_Idle.fbx` there converts cleanly
via Blender to a 394 KB **mesh-free `sit.glb`** that binds (65/65 bones; the fleet's `_canon` keeps the
colon, `mixamorig:Hips`, so raw Mixamo naming is already canonical). **But wiring it is a v3.1 change, not
a release-week one, for two real reasons:**
1. **`_rotOnly` breaks a seated pose** — it drops `Hips.quaternion` (pelvic tilt) + all positions (hip
descent), giving a "levitating L-sit". Correct wiring needs a parallel `_rotWithHips` filter + a
post-pose foot re-plant in `spawnRig` — a change to the **shared** rig stack (keepers/browsers/band/crowd).
2. **A genuinely seated figure is ~1.2 m crown-above-feet**, which **fails F's no-giants stature floor
[1.4,2.0]** (`flags_check.py:695`) — it needs an F smoke exemption for the seated band member.
Both land a qa-red change pending an F handshake, in the freeze round, for a polish item — the drummer
already reads seated from the crowd POV (money shot) via the `SEAT_DROP` hack. **Rides Fable's existing
v3.1 item "bench-sit loiter from the same `sit` clip".** Recipe (for v3.1): scp Sitting_Idle.fbx → Blender
`--background --python` import/export GLB (no bone rename needed) → `web/models/peds/sit.glb``_rotWithHips`
loader + `spawnRig` re-plant + F stature exemption. See LANE_D_NOTES wishlist #1.
---
## ROUND 13 — v3.0-beta the district: the RY fix, instruments/drummer, the queue, multi-venue surge
*PROCITY-D, 2026-07-15. Behind `?gigs=1`; verified in-shell against A's landed 3-venue plan (pub #116

View File

@ -1,5 +1,87 @@
# LANE E — cross-lane notes
## Round 14 — v3.0 PROVENANCE FREEZE ✅ (release round)
> **FROZEN for the v3.0 tag** (2026-07-16). Lane E R14 is a provenance close-out — **no new synthesis**
> (genres final: pubrock / grunge / covers; John "fine for now"). Everything below is the durable
> Lane-E release contract; the deep per-asset narrative lives in `pipeline/AUDIT.md` (current through
> its R13 section). Counts tool-verified (glb_stat / ffprobe / manifest parse) and adversarially
> re-audited by two independent passes — both PASS, **0 release blockers**.
**Release status**
- **Gate 3 GREEN**`validate_manifest.py --depot` → 0 errors, 0 warnings. Not blocking the tag.
- **`publish.py --verify`** — depot matches `_published.json` (38 entries). The lone drift note
(`vintage-cash-register.glb` absent from the *tailnet mirror*) is cosmetic: it's a depot-reuse asset
and IS live on the public `digalot.fyi` depot (Gate 3 confirms), so the manifest's real target is fine.
- **Licensing 🟢 clean** — 0 quarantined/branded assets in the manifest (grep of every AUDIT QUARANTINE
token — vitra/eames/calligaris/ikea/technics/1210/tuborg/uploads_files/akai/mpc/turntable — → 0 hits).
- **Poster templates (R13 debt #4): no ask from B** → no new templates (E builds only on B's ask).
- R14 doc-consistency fixes applied: AUDIT.md R13 section added; `roomtone-video` gain 0.3→0.35 aligned
to the manifest; manifest `glb_law` string amended to state the ≤8k-hero / ≤14k-instrument exceptions.
### Provenance & licensing — by source zone (all 🟢 web-ok)
| zone | assets | source | method | licence |
|---|---|---|---|---|
| House library GLBs | 18 | thriftgod / house depot, John-presorted **generic** props (benches, shelves, counter, crate, bin, streetlight, food_cart…) — no recognizable trademark | `normalize.py` (passthrough or COLLAPSE ≤5k) | 🟢 web-ok |
| On-device synthetic GLBs | 11 (+5 `_hi`) | MODELBEAST `flux_local` (FLUX.2-klein-4B) concept → `trellis_mac` mesh | 6 hero via `bake_lowpoly.py` (≤8k) · 5 gig instruments via `normalize`→`decimate_props.py` (14k) | 🟢 CC0-equiv, 100% on-device |
| fal-Hunyuan GLBs | 3 | `flux_local` concept → `fal-ai/hunyuan3d/v2` (glass_case, magazine_rack, bus_shelter) | `bake_lowpoly.py` (≤8k) | 🟢 original (~$0.64 total spend) |
| Depot-reuse GLB | 1 | `cash_register` = John's own `thriftgod_hero` (already live) | reuse (not regenerated) | 🟢 cleared |
| Skins (2D) | 96 | 90sDJsim ports (7 facade + 2 ground) + thriftgod R1 store-art + `flux_local` gap-fill fronts/posters | `gen_skins.py --local` (on-device) | 🟢 web-ok |
| Audio pack | 28 | 100% procedural numpy synthesis (`gen_audio.py`) | oscillator/noise render → ffmpeg OGG+M4A | 🟢 CC0-equiv, parody-safe |
| Stock packs | 3 (350/311/273) | GODVERSE `dealgod` Postgres — **real** cover art | `build_stock_pack.py`: parody-transform metadata + atlas | 🟢 real-art / parody-metadata |
**GLB fleet = 33 manifest (23 fit + 10 furniture) + 5 `_hi` = 38 published.** Only external spend ever:
~$0.64 (fal-Hunyuan, R5). Everything else is $0 / on-device (electricity only).
### Audio inventory — 28 assets (`web/assets/audio/`, committed; contract = `manifest.audio`)
All procedural, seeded, $0, parody-safe; OGG/Opus + M4A/AAC; beds+music loudnorm 16 LUFS, SFX peak-norm.
| group | n | assets | gig? |
|---|---|---|---|
| ambience | 7 | street-day, street-night, rain, roomtone-{retail,milkbar,video}, **crowd-walla** | crowd-walla `gig:true` |
| sfx | 8 + 6 steps | doorbell, door-open, till, riffle, toast, tram-bell, tram-rumble(loop), **applause**; footstep{pavement×3, timber×3} | applause `gig:true` |
| music | 7 | record-shop, milkbar, video-synth, arcade, **gig-pubrock**, **gig-grunge**, **gig-covers** | 3 gig beds `gig:true` |
- **Gig beds (R13, the manifest key IS the gigKey `gig-<genreKey>`):** `gig-pubrock` (pub, 0.6, 13.4s),
`gig-grunge` (band_room, 0.6, 17.2s), `gig-covers` (rsl, 0.5, 19.2s). Seamless loops, non-silent
(verified 15.7…17.6 dB mean). Old `pubrock-live` fully retired (files gone, no live refs).
### GLB inventory — 38 (tri counts glb_stat-verified; footprints/heights match manifest; no Draco, tex ≤1024)
**Fittings (23):** record_crate 5k · wire_shelf 330 · clothes_rack 3.2k · bookshelf 5k · cube_shelf_wide 5k ·
counter 276 · coffee_table 495 · work_table 1.6k · box_crate 78 · crate_stack 3k · **listening_booth 8k ·
drinks_fridge 8k · arcade_cabinet 8k · milkshake_mixer 8k · counter_till 8k** (bake_lowpoly hero) ·
**glass_case 8k · magazine_rack 8k** (fal) · **electric_guitar / bass_guitar / drum_kit / guitar_amp /
mic_stand = 14k each** (R13 decimate) · cash_register 6k (reuse).
**Furniture (10):** bench 1.9k · bench_modern 640 · bench_wood 4k · longbench 1.2k · park_bench 5k ·
streetlight 5k · food_cart 5k · **novelty_record 8k** (bake_lowpoly) · **bus_shelter 8k** (fal) · bin 3k.
**Preserved `_hi` originals (5, on depot, NOT manifest-referenced — LOD/re-bake source):**
electric_guitar 43.5k · bass_guitar 36k · mic_stand 34.1k · drum_kit 26.2k · guitar_amp 96.6k.
### Skins — 96 (`web/assets/gen/*.jpg`, local; all present, 0 missing/orphan)
facade 48 (43 blank + 5 baked-sign: djsim-video/milkbar/pub, market, grimy) · sky 10 · ground 10 ·
wall 8 · interior floor 6 · interior surface 7 · awning 3 · **poster 4** (grunge/retro/screenprint/xerox,
blank name-band). Per-shop-type facade coverage ≥2 for all types (validate_manifest asserts; min = 3).
### Stock packs — 3 (indexes committed in `pipeline/packs/`; atlases on depot; read directly by Lane C)
record 350 (6 atlases, 256²) · book 311 (4 atlases, 176×256) · toy 273 (5 atlases, 256²). Real cover art
(GODVERSE), **parody-transformed metadata** (sha256-seeded word-banks; no real trademarks). All pass `validate_pack`.
### By-design notes (documented so a future reader doesn't re-flag them)
- **5 `_hi` in `_published.json` but not the manifest** — intentional archival hi-poly originals (R13).
- **Stock-pack indexes not in the manifest** — by design; Lane C reads `pipeline/packs/*` directly, atlases ship on the depot.
- **`glb_law` tiers** — ≤5k library / ≤8k baked hero (TRELLIS shell-soup, texture-intact) / ≤14k gig instruments; now stated in the manifest convention string + AUDIT.
- **`validate_manifest` prints "skins 85 groups"** — a shallow `sk.values()` count (the nested `interior` dict counts as 1); true leaf total is 96. Cosmetic label, flagged for v3.1.
- **Minor stale ledger figures** (non-live): `food_cart` 4991 vs `_normalize_results` 5000 (9-tri); a superseded `cube_shelf_01` entry in `_normalize_results` (DROP-filtered from the manifest, rejected in AUDIT). Harmless.
### Deferred to v3.1 (documented, not started)
- **`street_bin` decimation** — B's non-blocking, *conditional* ask ("if the combined-lane budget tightens"); the bin is ~3k tris (within house law), and B measured ~100k tri / ~150 draw margin at the busiest block, so no action for v3.0.
- **Texture-bake-to-lowpoly / KTX2-Basis atlas pass** — if a future budget tightens; the `_hi` instrument originals + the hero props are the LOD source (R9 VRAM note + AUDIT).
## Round 13 — two genres, the gigKey rename (debt #1), lighter instruments
**v3.0-beta THE DISTRICT.** All local work landed + verified; the one gated step (depot publish)

View File

@ -0,0 +1,138 @@
# PROCITY — Round 15 lane instructions (from Fable, integrator)
Date: 2026-07-16 · **Round-14 verdict: AE landed and green; LANE F NEVER RAN.** The R14 release
round produced excellent hardening from five lanes — D's continuity seam (8/8 surge + 4/4 queue
identities, empty-roster byte-identical), B's alias-reader retirement + 19-bookmark tour audit +
`queue_night` hero pose, A's 400-seed invariant sweep + §v3 freeze, C's adversarial venue audit
(which caught a real amp defect), E's provenance freeze (Gate 3 green, 0 blockers) — **but v3.0 is
NOT tagged.** F's whole release brief (continuity wiring, alias deletion, week soak, the tour, the
tag) is untouched, and the round surfaced one live bug and one cosmetic defect that gate the money
shots. **Round 15 closes the release.** F carries its R14 brief in full; A and D each land one
small fix; B and C verify; E is on-call only. Nothing else — the freeze holds.
## The ledger (everything between here and the tag)
1. **Frontage posters are on the BACK WALL (A — live bug, gates John's hero shot).**
`gigs.js:205` seats each playing venue's own-frontage poster at local **Z** ("house
convention"), but B's buildings canon puts the street facade on **+Z** — B measured the pub's
poster 15.9 m behind the street face. Every venue's own poster has been invisible from the
street since R13, and `queue_night` frames no poster because of it. Fix: flip item 1 of
`buildPosters` to `+sin,+cos`. This moves `plan.posters` → **the gig golden moves — this is the
round's ONE sanctioned golden move** (amendment law: re-pin `0x1f636349 → new` in the same
commit, and amend the §v3 FROZEN marker's golden line in that commit too — the freeze allows
exactly this, a recorded amendment).
2. **Alias deletion (F — B unblocked you, with a map).** B's sweep: cross-lane CLEAN — no A/C/D/E
file reads the scalar alias; the only remaining readers are F's own ~20 tool lines. Delete
`gig_state.js:141151`, migrate `tools/flags_check.py` + `tools/qa/gig_shot.py`
(`.venueShopId → venueShopIds[0]`/explicit id · `.state → stateOf(id)` · `.cover → coverOf(id)`
· `.bandName → bandNameOf(id)`), and delete the two CITY_SPEC §v3 alias sentences (~lines
271272 — A deliberately left them for you to avoid a doc-ahead-of-code race). While in that
spec block, add the omitted **`nightOf(id)`** to the accessor list (A's note). One atomic commit.
3. **Continuity wiring + smoke (F — D handed you three touches).**
`crew.spawn(…, { roster: citizens.tonightRoster(shop.id) })` at `interior_mode.js:239`;
`onAdmit: id => citizens.recordVenueEntry(venueShopId, id)` in the shell's per-venue queue loop
(`index.html` ~380393); the clear is automatic on your existing `setGig(id,false)`. Smoke:
assert **crowd ⊇ roster ∩ cap** on the seam (`crew.crewInfo.fromRoster`, `member.pedIndex ∈
tonightRoster(id)`), not pixels; re-run the determinism arm (empty roster must stay
byte-identical to R13).
4. **`guitar_amp` reposition (D — C's ruling, not blessed as-is).** Your backline plant is 0.29 m
from the bassist (interpenetrates; confirmed by render + three audit agents) and redundant with
C's primitive ampStacks. Move to C's corrected pose (LANE_C_PUB §3): up-stage-right
`(stage.x + stage.w·0.36, deckY, stage.z stage.d·0.22)`. One-liner in `band.js
_addBackline`; C verifies. (Full `stage.backline[]` unification stays v3.1.)
5. **Settled-tris honesty (F).** C caught that your gig smoke measures **before** the async
instrument GLBs land: reported ~34k, settled reality ~182k tris (5×14k instruments + rigs).
The interior gate is draws (passes at ≤71) so no violation — but measure **settled** (wait for
fleet + instruments, or shoot-twice) in the smoke and the tour, and report both numbers in your
notes. Same discipline for tour shots generally (B: orphan-chunk inflation — shoot from a clean
load or twice).
6. **Drummer sit clip — deferral RATIFIED (v3.1).** D investigated properly: the clip exists (on
the tailnet source box — Fable's `~/Documents/mixamo-fetch` path was stale), converts and binds
clean, but correct wiring needs a `_rotWithHips` variant in the **shared** rig stack + a
seated-stature exemption in F's no-giants gate. Two cross-cutting changes for a polish item in
freeze week = v3.1. The `SEAT_DROP` hack ships in v3.0; it reads seated from the crowd POV.
## Laws (unchanged, one exception)
- Prime flag law: `?gigs=1` **opt-in at the tag** (John concurred, R14). Flags-off byte-identical;
synthetic golden `0x3fa36874` frozen. **Gig golden moves ONCE** (ledger #1, A's commit only).
- Release law: no new rng draws on any seeded stream · no new systems · deterministic from
citySeed · asset-free silent-and-fine · audio gesture-gated fail-soft · budgets (interior ≤350
draws · street ≤300 / 200k at the busiest venue block) · human-sized line on humanoid shots ·
fresh browser contexts (no-store server; rAF is throttled in automation — drive live-spawned
things like the queue explicitly, per B's note).
- Shared-tree discipline: atomic commits by explicit pathspec.
## Lane A — the poster flip (first, everything waits on you)
1. Ledger #1: flip `buildPosters` item 1 to the +Z street face; re-pin the gig golden; amend the
§v3 FROZEN marker (golden line + a one-line amendment note) — all in ONE commit. Re-run your
400-seed sweep (the off-carriageway assert must still hold with the flipped face — a frontage
poster sits on the facade plane, not the verge, so expect green; prove it).
2. Tell B + F the new golden the moment it lands.
## Lane B — verify the hero (after A)
1. Re-shoot `queue_night`: A's flip should seat the pub's own poster by the door **in the existing
frame with no pose change** (your prediction — verify it). Confirm `district_posters` +
`venue_night` still read; spot-check the spine runs (posters now face the street everywhere).
2. Nothing else. Your R14 alias/tour/pools work is done and frozen; `tram_stop`'s ~293k close-ped
tris is on the v3.1 list.
## Lane C — verify the amp (after D)
1. Re-render the pub + RSL stages with D's corrected plant: amp clear of the bassist (state the
measured clearance), flanking the primitives, nothing interpenetrating. One line in your notes;
your freeze stands otherwise.
## Lane D — the amp one-liner
1. Ledger #4: move the backline plant to C's §3 pose. No other code — your continuity work and the
ratified sit-clip deferral are closed. Confirm draw/tri delta 0 and `?noassets` unaffected.
## Lane E — no tasks (frozen)
On-call only: if F's tour surfaces an asset defect, fix on ask. Nothing else — your provenance
freeze is the release contract.
## Lane F — the release (last; this is your R14 brief, carried in full)
1. **Continuity wiring + smoke** (ledger #3) — D's three touches, assert on the seam.
2. **Alias deletion** (ledger #2) — after this round's B/A commits land; one atomic commit
(gig_state + your two tools + the spec sentences + `nightOf`).
3. **Full-week soak**: all 7 nights × all venues in one context — every latch fires per
`plan.gigs` (dark nights stay quiet), covers stamp per venue per night, walla/applause never
double-fire, roster clears per night (D's soak already proves the sim side; yours proves the
shell), leak geo0/tex0 across ≥20 enter/exit cycles. Non-strict qa extra.
4. **The money-shot tour**: `tools/qa/tour_shots.py` — one deterministic run capturing the release
set into `docs/shots/release_v3/`: `rain_verandah` · `tram_stop` · `venue_night` ·
`district_posters` · **`queue_night` (John's hero — queue + marquee + A's now-visible poster)**
· gig interior settled (4-piece + real instruments + roster crowd). Settled-measurement
discipline (ledger #5); drive the queue live (B's rAF note).
5. **Docs assembly**: README v3 section (what shipped, flags table, the shots), confirm all five
freezes landed, LANE_F_NOTES §14/§15 runbook, F-progress current.
6. **Acceptance + tag + push**: `qa.sh --strict` 6/6 + continuity smoke + week soak green → tag
**`v3.0`** → `git push origin main` and `git push origin v3.0` as separate commands. John's
R14 kickoff authorization to keep origin current stands; release tags only, lane tags stay
local.
## Order
**A first** (the poster flip + re-pin — B's verify and F's hero shot hang off it). **D** anytime
(one line); **C** verifies D; **B** verifies A. **F last**, once A+D are in. Handshakes: A→B/F the
new golden; D→C "amp moved"; B→F "hero verified".
## Standing — the v3.1 backlog (consolidated; do not start)
Flag-default flip (`?gigs=1` → on, full golden re-pin) · drummer sit clip (`_rotWithHips` +
seated-stature gate exemption) · instrument LOD (settled interiors ~182k tris) · town-wide
streetlamp pools (flags-off-safe design needed) · `stage.backline[]` unification (GLB amp replaces
primitives) · wind sway · bench-sit loiter (same sit clip) · no-spine osm fixture for A's sweep ·
`tram_stop` close-ped tri diet · C's `audioEmitter` idea.
## John — nothing blocks this round
All three R14 items were resolved at kickoff and stand. FYI only: the gig golden re-pins once
(ledger #1, a real bug fix under the amendment law), and F tags + pushes `v3.0` at round end under
your standing OK. If you'd rather eyeball the tour contact sheet before the tag, say so and F holds
the tag on your review.

View File

@ -448,5 +448,36 @@ by D's band rigs; primitive fallback if a GLB is absent (house law).
- Same TRELLIS tri-weight tradeoff as round-3 props (3096k tris vs the ≤5k library law) — accepted
for focal/optional/LOD-able stage props seen at mid-distance; texture-bake-to-lowpoly is the
follow-up. Amp (96k) is the outlier — a candidate for that pass first. Eyeballed (contact sheet).
- Staged to `web/assets/models/` for local validation (`?localdepot=1`). **Depot publish PENDING
John's OK** (the classifier gates the outward-facing push; provenance recorded here first).
- Staged to `web/assets/models/` for local validation (`?localdepot=1`). Depot publish was
**PENDING John's OK** at R12 → **DONE in R13** (see the R13 section below; the 3096k figures in
the table above are the R12 originals, decimated to 14k each in R13).
## Round 13 — gig-layer finalization (v3.0-beta, `?gigs=1`)
Supersedes the R12 instrument table + pending-publish note above. Three closes, all 🟢 provenance
unchanged (100% on-device synthetic / procedural — decimation and renaming don't touch the licence zone):
**1. Instrument decimation (`pipeline/decimate_props.py`, Blender collapse-decimate — NOT voxel-remesh,
so thin features survive).** The R12 instruments were the TRELLIS shell-soup tri-weight outlier; a
district means multiple concurrent stages, so all 5 were taken to a uniform **14 000 tris** (fleet
**236k → 70k, 70%**). Collapse (keeps original UVs/textures, no re-bake) held the mic boom pole + cable,
drum cymbals/stands and guitar necks — verified by re-rendered thumbs. The high-poly originals are kept
as `_hi` variants (published to the depot as archival source, not manifest-referenced).
| instrument | R12 tris → R13 | `_hi` original | footprint (m) | h (m) |
|---|---|---|---|---|
| electric_guitar | 43 451 → **14 000** | 43 451 (`_hi`) | 0.38 × 0.08 | 1.00 |
| bass_guitar | 36 031 → **14 000** | 36 031 (`_hi`) | 0.34 × 0.12 | 1.15 |
| mic_stand | 34 079 → **14 000** | 34 079 (`_hi`) | 0.57 × 0.57 | 1.70 |
| drum_kit | 26 238 → **14 000** | 26 238 (`_hi`) | 1.50 × 1.24 | 1.30 |
| guitar_amp | 96 583 → **14 000** | 96 583 (`_hi`) | 0.51 × 0.29 | 0.61 |
**2. gigKey rename + two new beds (debt #1).** The manifest music key IS the gigKey, always
`gig-<genreKey>`. `music-pubrock-live`**`gig-pubrock`**; added **`gig-grunge`** (band_room) and
**`gig-covers`** (rsl, organ-led) — all procedural numpy synth, $0, seamless, parody-safe (new synth
helpers `organ()` + `grungechord()`). Audio pack **26 → 28 assets**. Old `pubrock-live` files deleted.
**3. Depot publish — DONE (John OK'd direct publish, R13).** Pushed via the passwordless tailnet
ingress (this box is allowlisted): the 5 base names now serve **14k bytes** + the 5 `_hi` originals are
live. Verified on the public `digalot.fyi` depot (amp base **5.9 MB → 1.31 MB**, `_hi` 5.9 MB live).
`_published.json` **33 → 38**. `validate_manifest --depot` GREEN; Gate-3 provenance flag **cleared**.

View File

@ -269,7 +269,10 @@ def build():
"version": 1,
"depot": DEPOT,
"conventions": {
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), <=5k tris, WebP <=1024, no Draco",
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), WebP <=1024, no Draco; "
"tris <=5k for library props, with documented exceptions for focal/LOD-able assets: "
"baked hero props <=8k (TRELLIS shell-soup, texture-intact) and gig instruments <=14k "
"(v3.0; hi-poly _hi originals on the depot) — see AUDIT.md + LANE_E_NOTES",
"facing": "symmetric props are orientation-agnostic; normalize.py applies no automatic "
"facing-correction (per-asset yaw is available in batch.json) — Lane B/C rotate "
"directional props (counter, food_cart) to taste when placing",

View File

@ -692,7 +692,7 @@ PACK = {
"ambience-rain": ("ambience", amb_rain, dict(loop=True, gain=0.45, note="layer over street beds")),
"roomtone-retail": ("ambience", amb_roomtone_retail, dict(loop=True, gain=0.35)),
"roomtone-milkbar": ("ambience", amb_roomtone_milkbar, dict(loop=True, gain=0.35)),
"roomtone-video": ("ambience", amb_roomtone_video, dict(loop=True, gain=0.3)),
"roomtone-video": ("ambience", amb_roomtone_video, dict(loop=True, gain=0.35)),
# sfx
"sfx-doorbell": ("sfx", sfx_doorbell, dict(loop=False, gain=0.7)),
"sfx-door-open": ("sfx", sfx_door_open, dict(loop=False, gain=0.6)),

View File

@ -2,7 +2,7 @@
"version": 1,
"depot": "https://digalot.fyi/3god",
"conventions": {
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), <=5k tris, WebP <=1024, no Draco",
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), WebP <=1024, no Draco; tris <=5k for library props, with documented exceptions for focal/LOD-able assets: baked hero props <=8k (TRELLIS shell-soup, texture-intact) and gig instruments <=14k (v3.0; hi-poly _hi originals on the depot) \u2014 see AUDIT.md + LANE_E_NOTES",
"facing": "symmetric props are orientation-agnostic; normalize.py applies no automatic facing-correction (per-asset yaw is available in batch.json) \u2014 Lane B/C rotate directional props (counter, food_cart) to taste when placing",
"facade_blank_signboard": "facades tagged signboard:'blank' ship a blank sign panel \u2014 Lane B overlays the shop name there. signboard:'baked' facades (some djsim-* Flux skins) already have sign text: use as-is, don't overlay",
"facade_face": "facade entries without a 'face' key are front elevations (shop or house fronts); face:'side' entries are windowless corner-lot side walls with empty types \u2014 never select one as a shop front",

View File

@ -128,23 +128,25 @@ export class GigCrew {
_make(target, key, { x, y, z, ry, height, pedIndex = null }) {
const r = rng(this.citySeed, 'gig', key);
const h = height != null ? height : 1.6 + r() * 0.3;
let actor, kind;
let actor, kind, usedIndex = null;
if (this.fleet && this.fleet.ready) {
const pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
const rig = pk && this.fleet.all[pk.index];
const sp = rig && spawnRig(rig, { ry, height: h, clip: this.fleet.idleClip, phase: r() });
if (sp) { actor = sp; kind = 'rig'; }
if (sp) { actor = sp; kind = 'rig'; usedIndex = pk.index; } // usedIndex: the fleet ped shown — F's continuity smoke reads it
}
if (!actor) { actor = makePlaceholder(rng(this.citySeed, 'gig-body', key), { height: h }); actor.fig.rotation.y = ry; kind = 'placeholder'; }
actor.fig.position.set(x, y, z);
target.add(actor.fig);
return { actor, kind };
return { actor, kind, pedIndex: usedIndex };
}
// spawn(roomGroup, { stage, watchPoints, gig }) — the band on the deck + crowd at the watch points.
spawn(roomGroup, { stage = null, watchPoints = [], gig = { gigId: 0 }, instrumentFor: instFor = instrumentFor } = {}) {
// spawn(roomGroup, { stage, watchPoints, gig, roster }) — the band on the deck + crowd at the watch points.
// roster (R14): [{pedIndex,height}] of the punters who entered this venue tonight (D's citizens.tonightRoster);
// the crowd's front slots become them (continuity), seeded strangers fill the rest.
spawn(roomGroup, { stage = null, watchPoints = [], gig = { gigId: 0 }, roster = [], instrumentFor: instFor = instrumentFor } = {}) {
this.disposeAll();
const gid = gig.gigId | 0;
const gid = (gig && gig.gigId) | 0; // null-safe (a quiet-night call would pass no gig)
this._gen++;
const gen = this._gen;
@ -157,10 +159,10 @@ export class GigCrew {
const seated = p.role === 'drums' || !!p.seated;
const poseY = p.y != null ? p.y : (stage.deckY || 0); // C tags per-pose y (front line deckY, drums riser)
const y = seated ? poseY - SEAT_DROP : poseY; // sink the drummer so the kit hides the legs
const { actor, kind } = this._make(roomGroup, `band:${gid}:${p.role}:${i}`,
const { actor, kind, pedIndex } = this._make(roomGroup, `band:${gid}:${p.role}:${i}`,
{ x: p.x, y, z: p.z, ry: p.ry, pedIndex: distinct.length ? distinct[i % distinct.length] : null,
height: 1.68 + rng(this.citySeed, 'gigh', `${gid}:${i}`)() * 0.2 });
const member = { actor, kind, part: 'band', role: p.role, dance: false, seated,
const member = { actor, kind, pedIndex, part: 'band', role: p.role, dance: false, seated,
base: { x: p.x, y, z: p.z, ry: p.ry }, phase: rng(this.citySeed, 'gigp', `b${gid}:${i}`)() * Math.PI * 2, extra: [] };
const inst = instFor(p.role);
if (inst) { actor.fig.add(inst); member.extra.push(inst); }
@ -170,14 +172,24 @@ export class GigCrew {
this._addBackline(roomGroup, stage, gen); // amp dressing beside the riser (async, dressing)
}
// ── crowd: stand-and-watch or dance at C's watch points ──
// ── crowd: R14 identity continuity — the front slots become tonight's roster (the punters who came in
// via the queue / surge), seeded strangers fill the remainder. The roster override passes {pedIndex,height}
// for that slot only; _make's existing branches then skip that slot's own height+pickRig draws. Because
// every crowd slot is an INDEPENDENTLY keyed stream (`gig`/`crowd:<gid>:<slot>`), this can't shift any
// other slot, the sway-phase stream (`gigp`), or the band — and an EMPTY roster is byte-identical to R13
// (no override → the exact R13 draws). Roster heights are the sim's 1.551.95 m, inside the [1.4,2.0] gate.
const pts = watchPoints.slice(0, Math.min(CROWD_CAP, watchPoints.length));
pts.forEach((w) => {
const { actor, kind } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`, { x: w.x, y: 0, z: w.z, ry: w.ry });
this.members.push({ actor, kind, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false,
let fromRoster = 0;
pts.forEach((w, i) => {
const who = roster[i] || null;
const { actor, kind, pedIndex } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`,
who ? { x: w.x, y: 0, z: w.z, ry: w.ry, pedIndex: who.pedIndex, height: who.height }
: { x: w.x, y: 0, z: w.z, ry: w.ry });
if (who) fromRoster++;
this.members.push({ actor, kind, pedIndex, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false, fromRoster: !!who,
base: { x: w.x, y: 0, z: w.z, ry: w.ry }, phase: rng(this.citySeed, 'gigp', `c${gid}:${w.slotIndex}`)() * Math.PI * 2, extra: [] });
});
return { band: this.members.filter(m => m.part === 'band').length, crowd: pts.length };
return { band: this.members.filter(m => m.part === 'band').length, crowd: pts.length, fromRoster };
}
// Swap a band member's primitive instrument for E's manifest GLB, placeholder-persists style. Fail-soft

View File

@ -42,11 +42,14 @@ export class VenueQueue {
}
// Build the seeded line. `count` overrides the seeded 2..6 (F may cap to the room's spare capacity).
spawn(parent, { door = null, queueZone = null, gigId = 0, count = null } = {}) {
// onAdmit (R14): fired with {pedIndex,height,key} on EVERY admit (auto-drain or explicit), so F can relay
// the punter who walked in into the sim's tonight roster (citizens.recordVenueEntry) → they join the crowd.
spawn(parent, { door = null, queueZone = null, gigId = 0, count = null, onAdmit = null } = {}) {
this.disposeAll();
this._gen++;
this.gid = gigId | 0;
this.parent = parent;
this.onAdmit = onAdmit;
const anchor = this._anchor(door, queueZone);
if (!anchor) return { queue: 0 };
this.anchor = anchor;
@ -81,9 +84,10 @@ export class VenueQueue {
const key = `${this.gid}:${slot}`;
const r = rng(this.citySeed, 'queueh', key);
const h = 1.58 + r() * 0.34;
let actor, kind;
let actor, kind, pedIndex = null;
if (this.fleet && this.fleet.ready) {
const pk = pickRig(this.fleet, r());
pedIndex = pk ? pk.index : null; // R14: remember the ped so admitOne can hand it to the crowd
const rig = pk && this.fleet.all[pk.index];
const sp = rig && spawnRig(rig, { ry: a.ry, height: h, clip: this.fleet.idleClip, phase: r() });
if (sp) { actor = sp; kind = 'rig'; }
@ -91,8 +95,8 @@ export class VenueQueue {
if (!actor) { actor = makePlaceholder(rng(this.citySeed, 'queuebody', key), { height: h }); actor.fig.rotation.y = a.ry; kind = 'placeholder'; }
actor.fig.position.set(tx, 0, tz);
this.parent.add(actor.fig);
this.members.push({ actor, kind, slot, target: { x: tx, z: tz }, base: { ry: a.ry },
phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
this.members.push({ actor, kind, slot, pedIndex, height: h, key, // key = `${gid}:${slot}` — stable per punter (slot shuffles, key doesn't)
target: { x: tx, z: tz }, base: { ry: a.ry }, phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
}
_gap() {
@ -102,15 +106,17 @@ export class VenueQueue {
_nextDrainGap() { return DRAIN_MIN + rng(this.citySeed, 'queuedrain', `${this.gid}:${this.members.length}:${this.t | 0}`)() * (DRAIN_MAX - DRAIN_MIN); }
// Admit the head of the line (nearest the door): it walks in, everyone else shuffles up one slot.
// Returns the admitted count so far (F can stop when the room is at watchPoint cap).
// R14: returns the admitted punter's identity {pedIndex, height, key} (or null when empty) and fires
// onAdmit(identity) — F relays it to citizens.recordVenueEntry so the punter joins the interior crowd.
admitOne() {
if (!this.members.length) return 0;
if (!this.members.length) return null;
// the head = the smallest slot (closest to the anchor)
let headIdx = 0;
for (let i = 1; i < this.members.length; i++) if (this.members[i].slot < this.members[headIdx].slot) headIdx = i;
const head = this.members.splice(headIdx, 1)[0];
if (head.actor.fig.parent) head.actor.fig.parent.remove(head.actor.fig);
head.actor.dispose && head.actor.dispose();
const identity = { pedIndex: head.pedIndex, height: head.height, key: head.key };
// shuffle everyone forward one slot → new target one step closer to the door
const a = this.anchor, gap = this._gap();
for (const m of this.members) {
@ -119,7 +125,8 @@ export class VenueQueue {
m.target.z = a.z + a.trail.z * gap * m.slot;
}
this._drainAt = this.t + this._nextDrainGap();
return 1;
if (this.onAdmit) this.onAdmit(identity); // relay the walked-in punter → the crowd roster (auto or explicit)
return identity;
}
// idle shuffle + slide up into vacated slots. The seeded auto-drain is a safety so the line empties

View File

@ -126,6 +126,10 @@ export class CitizenSim {
this._gigVenues = new Set(); // R13: venueShopIds with a gig on (F sets per-venue via setGig) → multi-venue surge
this.weather = { state: 'clear', intensity: 0 }; // Lane B's PROCITY.weather contract (shell feeds it)
this._occupancy = new Map(); // R9 shopId → [{ seed, enteredAt, pedIndex }] — the interior-presence truth
this._venueRoster = new Map(); // R14 identity continuity: venueShopId → Map<key,{pedIndex,height}> — who
// entered a gig venue TONIGHT (surge occupants + F-relayed queue admits).
// GigCrew consumes it so the crowd IS the people who came in. Cleared when
// the gig ends (setGig off). Pure bookkeeping — no rng, no GPU.
this._dropKeys = []; // scratch: chunks to drop this frame
this._activeList = []; // this frame's active citizens (both modes) — test page reads it
this.timeOfDay = 0.5; // noon
@ -164,14 +168,14 @@ export class CitizenSim {
if (this.streamMode) return;
this.roster.forEach(c => this._releaseActor(c)); this.roster.length = 0; // tear down the v1 roster
if (!this.chunkEdges) this._buildChunkIndex();
this.chunkRosters.clear(); this._dropKeys.length = 0; this._encountered.clear(); this._occupancy.clear();
this.chunkRosters.clear(); this._dropKeys.length = 0; this._encountered.clear(); this._occupancy.clear(); this._venueRoster.clear();
this.streamRadius = radius; this.streamPerChunk = perChunk;
this.streamMode = true; this._hookDriven = false;
}
disableStream() {
if (!this.streamMode) return;
for (const arr of this.chunkRosters.values()) arr.forEach(c => this._releaseActor(c));
this.chunkRosters.clear(); this._occupancy.clear(); this.streamMode = false;
this.chunkRosters.clear(); this._occupancy.clear(); this._venueRoster.clear(); this.streamMode = false;
}
// rasterise each edge's centreline to the 64m chunks it passes through → which edges seed each chunk
@ -340,7 +344,11 @@ export class CitizenSim {
// per venue off its per-venue state; the alpha single-venue call (one id) is a subset — still works.
setGig(venueShopId, on = true) {
if (venueShopId == null) return;
if (on) this._gigVenues.add(venueShopId); else this._gigVenues.delete(venueShopId);
if (on) this._gigVenues.add(venueShopId);
// gig ends (on→off transition) → disperse tonight's roster so the next night starts fresh + it stays
// bounded. Persists across interior exit/re-enter (the gig stays "on" through those) — only the real
// close clears it. .delete() returns true only on the transition, so re-calls while quiet are no-ops.
else if (this._gigVenues.delete(venueShopId)) this._venueRoster.delete(venueShopId);
}
setWeather(w) { if (w && typeof w.state === 'string') this.weather = { state: w.state, intensity: +w.intensity || 0 }; }
@ -393,8 +401,18 @@ export class CitizenSim {
let occ = this._occupancy.get(id); if (!occ) this._occupancy.set(id, occ = []);
occ.push({ seed: c.id, enteredAt: this.timeOfDay, pedIndex: c.pedIndex });
c._occShop = id;
// R14 continuity: a surge occupant who ducked into a GIG venue joins tonight's roster (the crowd
// becomes the people who came in). The roster persists past this ped's short dwell (unlike occupancy,
// which clears on emerge) — "who came tonight", keyed by the ped's stable id so re-entry doesn't dup.
if (this._gigVenues.has(id)) this._recordRoster(id, c.id, c.pedIndex, c.height);
}
}
// record one entered identity into a venue's tonight roster (deduped by key, insertion-ordered, no rng).
_recordRoster(venueShopId, key, pedIndex, height) {
let m = this._venueRoster.get(venueShopId);
if (!m) this._venueRoster.set(venueShopId, m = new Map());
if (!m.has(key)) m.set(key, { pedIndex, height }); // first entry wins
}
_emerge(c) {
if (c._occShop != null) { this._removeOccupant(c._occShop, c.id); c._occShop = null; }
const w = c._savedWalk;
@ -415,6 +433,20 @@ export class CitizenSim {
const occ = this._occupancy.get(shopId) || [];
return { count: occ.length, occupants: occ.map(o => ({ ...o })) };
}
// R14 identity continuity — the D→F seam.
// recordVenueEntry: F relays a queue admit (VenueQueue.admitOne() → {pedIndex, height, key}) so the punter
// who walked in the door joins the crowd. Surge occupants self-record in _enter; this is the queue side.
recordVenueEntry(venueShopId, entry) {
if (venueShopId == null || !entry || entry.pedIndex == null) return;
const key = entry.key != null ? `q:${entry.key}` : `q:${entry.pedIndex}:${(+entry.height || 0).toFixed(3)}`;
this._recordRoster(venueShopId, key, entry.pedIndex, entry.height);
}
// tonightRoster: the night's entered identities, for GigCrew.spawn (F passes it in) + F's continuity
// smoke (assert crowd ⊇ roster ∩ cap). Insertion-ordered (entry order), deduped, bounded, pure read.
tonightRoster(venueShopId) {
const m = this._venueRoster.get(venueShopId);
return m ? [...m.values()].map(e => ({ pedIndex: e.pedIndex, height: e.height })) : [];
}
// stable identity signature of the active set — immutable spawn identity, NOT live position, so it
// holds while citizens walk (the determinism gate: same seed → same crowd, twice). Both modes.
identitySignature() {

View File

@ -145,7 +145,13 @@ function structuralSuite(plan, label) {
// The v3 GIG layer (?gigs=1). Runs the full structuralSuite (venue-aware) plus gig-specific
// invariants. Validates the DISTRICT interface every downstream lane (C/D/B/F) hangs off this round.
function gigSuite(plan, label) {
structuralSuite(plan, label); // full coverage; venues exempt from late-shop rule
structuralSuite(plan, label); // full structural coverage; venues exempt from late-shop rule
districtInvariants(plan, label); // + the v3 district contract (also run standalone by the ROUND14 sweep)
}
// The v3 DISTRICT contract, factored out of gigSuite (ROUND14) so the 400-seed sweep below can assert it
// at SCALE without re-running the heavy structuralSuite (already covered on the 6 SEEDS + osm parity).
function districtInvariants(plan, label) {
const venues = plan.shops.filter(s => s.venue);
const byId = new Map(venues.map(v => [v.id, v]));
@ -199,7 +205,9 @@ function gigSuite(plan, label) {
const d = Math.hypot(p.x - nx, p.z - nz);
if (d < best) { best = d; bestEdge = e; }
}
if (bestEdge && best < roadWidth(bestEdge) / 2) { onRoad = [p.id, bestEdge.id, +best.toFixed(2)]; break; }
// 1e-6 immunises the exact kerb-line boundary against r2 coord rounding (only reachable on a
// no-main-spine fallback where poleOffset == roadWidth/2; never hit by the current sweep).
if (bestEdge && best < roadWidth(bestEdge) / 2 - 1e-6) { onRoad = [p.id, bestEdge.id, +best.toFixed(2)]; break; }
}
ok(onRoad === null, `${label}: no poster stands on a carriageway` + (onRoad ? ` (poster ${onRoad[0]} is ${onRoad[2]}m from edge ${onRoad[1]}'s centreline)` : ''));
}
@ -294,6 +302,35 @@ const GIG_GOLDEN = 0x1f636349; // ROUND13 re-pin: multi-venue district changed t
ok(GIG_GOLDEN !== 0 && hash === (GIG_GOLDEN >>> 0), `gig layer golden 0x${hash.toString(16)} matches pinned 0x${(GIG_GOLDEN >>> 0).toString(16)}`);
}
// ── 3d. district invariant sweep (ROUND14): 400 synthetic seeds + osm graceful placement ───────────
// Release-round Lane A gate: the district contract must hold at SCALE, not just the 4 hero seeds. Runs
// districtInvariants ONLY (structuralSuite already covers the 6 SEEDS + osm parity) so 400 full gig-on
// generations stay well under a second. Pure validation — NO new rng draws, no golden moves.
section('district invariant sweep (ROUND14: 400 synthetic seeds + osm graceful placement)');
const SWEEP_N = 400;
const sweepVenueCounts = new Set();
for (let s = 1; s <= SWEEP_N; s++) {
const plan = generatePlanFor(s, 'synthetic', { gigs: true });
districtInvariants(plan, `sweep syn ${s}`);
sweepVenueCounts.add(plan.shops.filter(v => v.venue).length);
}
// also cover the large/edge uint32 seeds (2e9, 8675309 …) — where rng edge cases hide — that fall
// outside 1..400 and otherwise only get the flags-off byte-identity check, not the district contract.
for (const s of SEEDS) if (s > SWEEP_N) districtInvariants(generatePlanFor(s, 'synthetic', { gigs: true }), `sweep syn ${s}`);
// the seeded 24 count is genuinely exercised across the range, not pinned to one value
ok([2, 3, 4].every(n => sweepVenueCounts.has(n)),
`sweep: venue count spans {2,3,4} across ${SWEEP_N} seeds (saw {${[...sweepVenueCounts].sort((a, b) => a - b).join(',')}})`);
// osm graceful placement: katoomba is the smallest fixture (19 shops) with NO warehouse district, so
// pickVenues' band_room/rsl rules hit their off-spine fallback — yet still land 24 valid venues incl.
// a pub. (Both osm fixtures keep a main spine, so this proves the no-warehouse branch, not no-spine.)
for (const seed of GIG_SEEDS) {
const plan = generatePlanFor(seed, 'osm', { gigs: true, town: 'katoomba' });
ok(plan.blocks.every(b => b.kind !== 'warehouse'),
`sweep osm/katoomba ${seed}: fixture has no warehouse district (graceful-placement precondition)`);
districtInvariants(plan, `sweep osm/katoomba ${seed}`);
}
// ── 4. every facade skin referenced by the registry exists on disk ──────────────────
section('assets on disk');
for (const f of allFacadeSkins()) ok(existsSync(join(ASSETS, f)), `registry facade exists: ${f}`);

View File

@ -111,7 +111,10 @@ function audioFor(type, ctx, opts, shop) {
// Venue gig (R12/13): Lane F passes the live gig state via opts.gig; F should prefer gigKey over musicKey
// while the gig is on. A quiet-night venue has no opts.gig → normal interior (room-tone + maybe radio).
// The genre resolves gig.genreKey → the venue's own shop.genreKey (A sets it) → the kind default. The
// manifest key IS the gigKey, canonical form `gig-<genreKey>` — no mapping table (ROUND13 debt #1).
// manifest key IS the gigKey, canonical form `gig-<genreKey>` — no mapping table (ROUND13 debt #1). This
// string is byte-identical to citygen's `gigKeyFor(genreKey)` (selfcheck asserts it); it is hand-built,
// NOT imported, on purpose — the interiors lib stays standalone (never imports citygen). Keep the form,
// don't privately rename it (the R12 trap) and don't import citygen (breaks standalone).
const gig = opts && opts.gig;
if (gig && (gig.on || gig.genreKey)) {
const genreKey = gig.genreKey || (shop && shop.genreKey) || GENRE_BY_TYPE[type] || 'pubrock';
@ -172,8 +175,8 @@ export function buildInterior(shop, THREE, opts) {
places: lay.places,
counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose
browsePoints: lay.browsePoints, // [ {x,z,ry,atKind,slotIndex} ] 0..3 — Lane D browser rig poses (R9)
watchPoints: lay.watchPoints || [], // venue (R12): [ {x,z,ry,dance,slotIndex} ] 6..8 audience poses (else [])
stage: lay.stage || null, // venue (R12): { x,z,w,d,deckY,frontZ,bandPoses[3] } (else null)
watchPoints: lay.watchPoints || [], // venue: [ {x,z,ry,dance,slotIndex} ] audience poses, cap by kind (pub/band_room 8, rsl 12); else []
stage: lay.stage || null, // venue: { x,z,w,d,deckY,riserY,frontZ,bandPoses[4] } (4-piece: trio + drummer on the riser); else null
audio: audioFor(recipe.key, ctx, opts, norm.raw), // { musicKey, toneKey, gigKey? } — Lane E manifest.audio keys (R11/R12)
dims: { ...dims, archetype, type: recipe.key },
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },

View File

@ -432,7 +432,7 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
placed: placed.filter(p => !p.removed),
places, decor: decor.meshes, grid, pathOK, carved,
spawnCell, targetCell, placement, browsePoints,
watchPoints, // venue: 68 audience poses {x,z,ry,dance} facing the stage (else [])
watchPoints, // venue: audience poses {x,z,ry,dance} facing the stage, cap by kind (pub/band_room 8, rsl 12); else []
stage: stageInfo, // venue: { x,z,w,d,deckY,frontZ,bandPoses } (else null)
counter: {
mesh: counter.fitting.group,

View File

@ -205,15 +205,13 @@ export function createAudioEngine(PROCITY, { noassets = false, mute = false } =
const lot = pub && (PROCITY.plan?.lots || []).find((l) => l.id === pub.lot);
if (lot) venues.push({ shop: pub, lot, genreKey: pub.genreKey || 'pubrock' });
}
// Per-venue gig state for the muffled spill, robust across Lane F's alpha→district transition:
// 1. F R13 per-venue map window.PROCITY.gigs.byVenue[id].state
// 2. alpha alias window.PROCITY.gigs is the single machine → its state for its venueShopId
// 3. clock fallback plan.gigs (night 0) vs the current segment (works before/without F)
// Per-venue gig state for the muffled spill (R14: the R12 alpha alias hop is retired):
// 1. F's per-venue map window.PROCITY.gigs.byVenue[id] (the canonical surface since R13)
// 2. clock fallback plan.gigs (night 0) vs the current segment (works before F wires / headless)
// Returns null only when there is no gig layer at all (flags-off ⇒ byte-identical, no spill).
function venueGigState(shopId) {
const g = PROCITY.gigs;
if (g && g.byVenue) { const e = g.byVenue[shopId]; if (e) return typeof e === 'string' ? e : (e.state || 'quiet'); }
if (g && g.venueShopId === shopId && typeof g.state === 'string') return g.state;
const gigs = PROCITY.plan?.gigs; if (!gigs || !gigs.length) return null;
const ton = gigs.find((x) => x.night === 0 && x.venueShopId === shopId);
if (!ton) return 'quiet'; // dark tonight — reads true

View File

@ -137,6 +137,25 @@ export function installDBG(deps) {
|| (plan.shops || []).find((s) => s.type === 'pub');
return shop ? poseForShop(shop, dist) : null;
};
// John's release hero: a CLOSE 3/4 on the pub door during doors/on, biased to the venue's +right
// (the queue side) so punters' faces read and a verge poster enters frame. The queue + posters are
// spawned by the live shell loop when the venue opens (seg 5 = NIGHT / 'on'), so F drives this shot
// live; this just frames the door + marquee + queue zone. Geometry mirrors venue.js's frontage.
const poseForVenueQueue = () => {
const vs = venueShops();
const shop = vs.find((s) => (s.venueKind || s.type) === 'pub') || vs[0]
|| (plan.shops || []).find((s) => s.type === 'pub');
const lot = shop && lotOf(shop); if (!lot) return null;
const ry = lot.ry || 0, fx = Math.sin(ry), fz = Math.cos(ry), rx = Math.cos(ry), rz = -Math.sin(ry);
const w = lot.w || 8, d = lot.d || 10;
const cx = lot.x + fx * (d / 2), cz = lot.z + fz * (d / 2); // door centre on the street (venue.js frontage)
const mw = Math.min(w * 0.92, 6.5); // marquee width (queue head ~ +right of it)
const DIST = 7.0, SIDE = mw * 0.5 + 2.2, EYE = 2.2; // close standoff, biased to the queue side
return {
pos: [cx + fx * DIST + rx * SIDE, EYE, cz + fz * DIST + rz * SIDE],
look: [cx + rx * (mw * 0.30), 2.7, cz + rz * (mw * 0.30)], // door, lifted to catch the marquee
};
};
// Named bookmarks (LANE_F_NOTES §4). seg = day segment index (0 DAWN … 5 NIGHT).
// Predicates cover both Lane A's taxonomy (dept/stall/record/…) and the fixture's; each falls back
@ -164,6 +183,7 @@ export function installDBG(deps) {
// ── v3 gig district (round 13, ?gigs=1): the street money shots F scripts via tools/qa/gig_shot.py ──
venue_night: { seg: 5, resolve: () => poseForVenue('pub', 13) || streetViewPose(['main']) }, // ?gigs=1 — lit pub frontage + streetlamp pools + queue + posters at night
district_posters: { seg: 5, resolve: () => streetViewPose(['main']) }, // ?gigs=1 — the spine at night: poster poles + lit venue frontages receding
queue_night: { seg: 5, resolve: () => poseForVenueQueue() || poseForVenue('pub', 11) || streetViewPose(['main']) }, // ?gigs=1 — John's hero: close on the pub door at 'on', queue + marquee + poster, punters facing the door
};
const DBG = {

View File

@ -185,13 +185,13 @@ export function createVenuePresentation(plan, skins, scene, { assetBase = 'asset
scene.add(group);
// ── per-venue state → frontage glow. Accepts F's per-venue map { [venueShopId]: state }, a Map, an
// { byVenue } wrapper, OR a bare state string (alpha compat → the primary pub). Idempotent; the shell
// ── per-venue state → frontage glow. F passes its per-venue map `gigState.byVenue`
// ({ [venueShopId]: 'quiet'|'doors'|'on'|'done' }); a Map or a { byVenue } wrapper are also accepted.
// (R14: the R12 alpha bare-string arm is retired — F wires the map since R13.) Idempotent; the shell
// calls it every street frame, so pool/glow ramps and the night baseline lerp here. ────────────────
function stateGetter(gigStates) {
if (gigStates == null) return () => 'quiet';
if (typeof gigStates === 'string') return (id) => (id === primaryId ? gigStates : 'quiet');
const map = gigStates.byVenue || gigStates;
const map = gigStates.byVenue || gigStates; // F passes gigState.byVenue (per-venue map)
return (id) => {
const v = map instanceof Map ? map.get(id) : map[id];
if (v == null) return 'quiet';
@ -225,7 +225,7 @@ export function createVenuePresentation(plan, skins, scene, { assetBase = 'asset
if (poolMesh) poolMesh.visible = poolMat.opacity > 0.01;
}
}
update('quiet');
update(null); // seed: no gig states yet → every venue quiet (frontage dark, pools follow night)
function dispose() {
for (const d of disposables) d.dispose && d.dispose();