Compare commits
6 Commits
f8ab837985
...
e466491300
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e466491300 | ||
|
|
9a65f51a88 | ||
|
|
eee60e05a5 | ||
|
|
62e38c5df8 | ||
|
|
a3911a4590 | ||
|
|
55f6698fb2 |
@ -3,7 +3,54 @@
|
|||||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
*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.*
|
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 1–2 up-stage at `(±stage.w·0.28, deckY, stage.z −
|
||||||
|
stage.d·0.18)`). Corrected pose in [LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) §3: move up-stage-right ≈
|
||||||
|
`(stage.w·0.36, deckY, stage.z − stage.d·0.22)`, clear of the bass and offset from C's primitives → a clean
|
||||||
|
flanking backline. Full amp unification (C exposes `stage.backline[]`, GLB replaces primitive) = v3.1.
|
||||||
|
Cosmetic + `?gigs=1` opt-in → does not block the tag.
|
||||||
|
|
||||||
|
**Tri observation → E/F (not a C gate):** E's decimated instrument GLBs are **14k tris each** (5 on a stage =
|
||||||
|
70k; peds ~2.7k each), so a settled RSL gig-night interior is ~**182k tris**. The interior gate is *draws*
|
||||||
|
(passes at ≤71); the 200k tris is the *street* budget, so no violation. But F's gig-night smoke reports ~34k
|
||||||
|
because it measures **before** the async instrument GLBs load — the real settled scene is ~5× heavier. A
|
||||||
|
camera-distance instrument LOD is a v3.1 candidate.
|
||||||
|
|
||||||
|
**Docs freeze (v3.0-FROZEN markers added):**
|
||||||
|
- `LANE_C_PUB.md` — retitled + frozen marker; §0 ry convention reframed past-tense (the +Z fleet was fixed at
|
||||||
|
the source in D's `rigs.js buildFigure`, R13 — no per-consumer flips); 4-piece stated as shipped (veto
|
||||||
|
language removed); the amp ruling added (§3); budgets updated to R14; in-flight handoffs removed.
|
||||||
|
- `LANE_C_AUDIO.md` — frozen marker + a scope note (this covers base interior audio; gig audio `gigKey`
|
||||||
|
lives in LANE_C_PUB); the deferred `audioEmitter` idea re-targeted to v3.1.
|
||||||
|
|
||||||
|
**Code (comment-only, zero runtime/golden/rng impact):** fixed two stale R12 inline return-shape comments in
|
||||||
|
`interiors.js` (`watchPoints` 6..8 → 8/8/12 caps; `stage` `bandPoses[3]` → `[4]` + `riserY`) + one in
|
||||||
|
`layout.js`; added a note at the gigKey hand-build explaining it's deliberately standalone (byte-identical to
|
||||||
|
citygen's `gigKeyFor`, not imported — keeps the interiors lib citygen-free). `theme.js` untouched.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,39 @@
|
|||||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||||
|
|
||||||
*Status: **all deliverables landed; self-check all-green (3273/3273); rounds 2–13 closed** (see below).
|
*Status: **all deliverables landed; self-check all-green (14264/14264); rounds 2–14 closed** (see below).
|
||||||
Node QA gates (selfcheck · scaffold · consistency) GREEN. v3.0-beta DISTRICT interface published (2–4
|
v3.0 RELEASE round: 400-seed district invariant sweep landed + CITY_SPEC §v3 frozen at `v3.0`. Goldens
|
||||||
venues, week schedule, town-wide posters); gig golden `0x1f636349`. Strict 6/6 is F's end-of-round gate.*
|
unchanged (synthetic `0x3fa36874`, gig `0x1f636349`) — no golden moved, no rng draw added this round.*
|
||||||
*Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)*
|
*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 1–400 + the large edge seeds**
|
||||||
|
asserting the whole contract each: 2–4 venues, ≥1 pub, never the openLate shop, no band twice per night
|
||||||
|
town-wide, per-venue 4–6 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
|
||||||
|
2–4 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: 2–4 venues + the week + town-wide posters + verge fix
|
## Round 13 (2026-07-15) — v3.0-beta THE DISTRICT: 2–4 venues + the week + town-wide posters + verge fix
|
||||||
|
|
||||||
|
|||||||
@ -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
|
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).
|
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
|
> **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 —
|
> `?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 —
|
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
|
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
|
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 6–8, lane = all, arcade 0),
|
barrel: **`roadWidth(edge)`** (full carriageway width — main 10 m, side 6–8, 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
|
**`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
|
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.
|
selfcheck asserts no poster stands within `roadWidth/2` of any edge centreline.
|
||||||
|
|||||||
@ -2,6 +2,47 @@
|
|||||||
|
|
||||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
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 1–400** synthetic + the large edge seeds (2e9, 8675309) asserting the full district
|
||||||
|
contract each: 2–4 venues, ≥1 pub, never the openLate shop, no band twice in one night town-wide,
|
||||||
|
per-venue 4–6 nights, every poster off the carriageway. Also asserts the seeded count **spans {2,3,4}**
|
||||||
|
across the range (measured 146/121/133) — proves the 2–4 range is genuinely exercised, not pinned.
|
||||||
|
- **osm graceful placement**: `katoomba` (smallest fixture, 19 shops, **no warehouse district**) still
|
||||||
|
lands 2–4 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 271–272) **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)
|
## 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
|
Supersedes the R12 alpha interface below. Same flag (`?gigs=1`), same prime-flag law: flag OFF ⇒ plan
|
||||||
|
|||||||
@ -1,5 +1,76 @@
|
|||||||
# LANE B — NOTES (measured)
|
# 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:141–151` + 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)
|
## 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),
|
Four B pieces, all gig-gated (flags-off byte-identical, verified). Files: `venue.js` (rewrite),
|
||||||
|
|||||||
@ -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
|
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane F + Lane B. Describes the shipped state; changes
|
||||||
attach?" **Confirmed** — `buildInterior` now returns `room.audio = { musicKey, toneKey }`, seeded per
|
> require a version bump.
|
||||||
shop. Lane F's interior_mode just plays it; Lane B's audio.js resolves the keys → files.*
|
|
||||||
|
*`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
|
## The contract
|
||||||
```js
|
```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`
|
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.
|
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
|
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
|
`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 —
|
fitting for it today (`listeningCorner`); milk-bar/video would need a new radio prop. A tidy v3.1 item: the
|
||||||
proposing the `userData.audioEmitter` hook + the two props as a tidy v2.2 item. v2.1 plays the bed
|
`userData.audioEmitter` hook + the two props. v3.0 plays the bed non-spatially (room-filling), which is the
|
||||||
non-spatially (room-filling), which is the right default for an interior anyway.
|
right default for an interior anyway.
|
||||||
|
|||||||
@ -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` —
|
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane D + Lane F. Describes the *shipped* state;
|
||||||
`pub` (R12), and new this round **`band_room`** and **`rsl`**. A converts a chosen shop to a venue kind in
|
> changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (release audit).
|
||||||
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).*
|
|
||||||
|
|
||||||
*Verified (fresh context, seeds 20261990/1990/7/42/101 + 90-build venue soak): 0 throws · 0 carves ·
|
*The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A
|
||||||
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Draws below.*
|
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`,
|
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
|
`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:
|
**Why no flip (the R13 fix, for the record):** the GLB fleet meshes ship facing local **+Z** at ry=0 — the
|
||||||
- toe-bone probe (`toe − foot`, the anatomically-anterior direction) on 5 peds → `(0.05..0.14, +0.99)` = **+Z**.
|
opposite of the −Z convention (verified: the anterior toe-bone vector `toe − foot` reads `(≈0, +0.99)`).
|
||||||
- planting a fleet rig at C's `counter.stand.ry` via `spawnRig` (no flip) and dotting its forward against
|
Before R13 this was compensated per-consumer, inconsistently: `band.js` flipped by π (band/crowd correct,
|
||||||
the intended customer-facing direction → **−0.99 (180° backward)**; at `ry+π` → +0.99.
|
placeholders backward) while `keepers.js` did not (placeholders correct, **keepers + browsers 180° backward
|
||||||
- for contrast the box placeholder's foot mesh sits at z −0.076 → **−Z** (matches the convention).
|
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
|
||||||
**Finding: with the GLB fleet loaded, keepers AND browsers have been facing 180° backward since the fleet
|
placeholder and every C pose. All consumer flips were then deleted. Net: keepers, browsers, band and crowd
|
||||||
was wired in.** `keepers.js` spawns rigs at `stand.ry` / `browsePoint.ry` with **no flip**, and the mesh
|
all face correctly, GLB and placeholder modes are consistent, and C's pure-data poses never moved (no golden
|
||||||
front is +Z, so the keeper faces the bar wall and the browser faces the aisle instead of the goods. The R12
|
touched). If you add a new rig consumer, plant it at the pose `ry` with no flip.
|
||||||
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.)*
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. What `buildInterior` returns for a venue (`shop.type` ∈ `pub` | `band_room` | `rsl`)
|
## 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
|
Everything below is **pure data on the return** (room-local coords, the ry convention in §0). Lane D drops
|
||||||
drops rigs straight in; Lane F reads state — no new lifecycle vs. the R9 keeper/browser seam.
|
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
|
```js
|
||||||
room.stage = {
|
room.stage = {
|
||||||
x, z, // deck centre (against the back wall)
|
x, z, // deck centre (against the back wall)
|
||||||
w, d, // deck size (m) — varies by kind (table below)
|
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
|
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)
|
frontZ, // z of the deck lip (the crowd stands in front of this, +Z)
|
||||||
bandPoses: [ { x, z, ry, role, y, seated? } ×4 ],
|
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[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,
|
- `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
|
`y = riserY` (= `deckY + 0.16`, the `fittings.js` stage riser box).
|
||||||
fitting — the pose just seats the kit + drummer on it.
|
- **Lane D:** lift each member to its own `pose.y` (front line = `deckY`, drummer = `riserY`). `role` hints
|
||||||
- **Lane D:** lift each member to **its own `pose.y`** (was `stage.deckY`; front line is unchanged, the
|
the instrument — E's GLBs via `opts.instrumentFor(role)` (electric_guitar/bass_guitar/mic_stand/drum_kit),
|
||||||
drummer now sits 0.16 m higher on the riser). `role` hints the instrument — E's GLBs via `opts.instrumentFor(role)`
|
primitives otherwise (asset law). `seated:true` → seated bob. `ry = π` already faces the audience (+Z) — no
|
||||||
(electric_guitar/bass_guitar/mic_stand/drum_kit), primitives otherwise (asset law). `seated:true` → seated bob.
|
flip (§0).
|
||||||
- `ry = π` faces the audience (+Z) **under the pinned −Z convention** → apply your fleet flip per §0.
|
- The band is a **4-piece** for v3.0 (front-line trio + drummer). Reverting to a trio would be a version bump
|
||||||
- **John may veto to a front-line trio** (charter): say so before you start and C ships `bandPoses` length 3
|
(drop `bandPoses[3]`), not a runtime toggle.
|
||||||
(drop `[3]`), and F amends its `band==` smoke back to 3.
|
|
||||||
|
|
||||||
### `room.watchPoints` — the audience, **cap by kind** (empty `[]` for non-venues)
|
### `room.watchPoints` — the audience, **cap by kind** (empty `[]` for non-venues)
|
||||||
```js
|
```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`.
|
stand + browse points + stage/PA. Deterministic per `shop.seed`.
|
||||||
|
|
||||||
### `room.counter` / keeper — unchanged
|
### `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
|
The bar is a normal counter; the barkeep is just a keeper at `room.counter.stand` (R9 seam). Faces the
|
||||||
§0 facing fix — the keeper is one of the actors currently backward with the GLB fleet.)*
|
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
|
`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`:
|
**`gig-<genreKey>`** — no mapping table. F passes gig state via `opts.gig`:
|
||||||
```js
|
```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
|
- 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**.
|
(`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`,
|
- **Prefer `gigKey` over `musicKey`** while the gig plays. E's beds are `gig-pubrock` / `gig-grunge` /
|
||||||
`gig-grunge`, `gig-covers`; a missing key → silence (audio law), band still visible.
|
`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 |
|
| 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 |
|
| `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
|
`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.
|
true — the venue is just a quiet pub/club.
|
||||||
|
|
||||||
## 3. Budgets (measured, seed 20261990, fresh context)
|
## 3. `guitar_amp` placement ruling (v3.0)
|
||||||
- **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.
|
|
||||||
|
|
||||||
## 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 **1–2 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 (2–3 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,
|
`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
|
bar in the corner — the PA louder than the room. `rsl` = carpet-and-flock club: the biggest floor in town
|
||||||
town (hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest
|
(hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest crowd
|
||||||
crowd cap. Both reuse the `pub` venue machinery (`theme.js venueSpec` + `layout.js` placeStage/placeWatchPoints);
|
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).
|
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).
|
||||||
|
|||||||
@ -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.55–1.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
|
## 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
|
*PROCITY-D, 2026-07-15. Behind `?gigs=1`; verified in-shell against A's landed 3-venue plan (pub #116
|
||||||
|
|||||||
@ -1,5 +1,87 @@
|
|||||||
# LANE E — cross-lane notes
|
# 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
|
## 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)
|
**v3.0-beta THE DISTRICT.** All local work landed + verified; the one gated step (depot publish)
|
||||||
|
|||||||
138
docs/LANES/ROUND15_INSTRUCTIONS.md
Normal file
138
docs/LANES/ROUND15_INSTRUCTIONS.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# PROCITY — Round 15 lane instructions (from Fable, integrator)
|
||||||
|
|
||||||
|
Date: 2026-07-16 · **Round-14 verdict: A–E 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:141–151`, 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
|
||||||
|
271–272 — 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` ~380–393); 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.
|
||||||
@ -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 (30–96k tris vs the ≤5k library law) — accepted
|
- Same TRELLIS tri-weight tradeoff as round-3 props (30–96k tris vs the ≤5k library law) — accepted
|
||||||
for focal/optional/LOD-able stage props seen at mid-distance; texture-bake-to-lowpoly is the
|
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).
|
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
|
- Staged to `web/assets/models/` for local validation (`?localdepot=1`). Depot publish was
|
||||||
John's OK** (the classifier gates the outward-facing push; provenance recorded here first).
|
**PENDING John's OK** at R12 → **DONE in R13** (see the R13 section below; the 30–96k 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**.
|
||||||
|
|||||||
@ -269,7 +269,10 @@ def build():
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"depot": DEPOT,
|
"depot": DEPOT,
|
||||||
"conventions": {
|
"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": "symmetric props are orientation-agnostic; normalize.py applies no automatic "
|
||||||
"facing-correction (per-asset yaw is available in batch.json) — Lane B/C rotate "
|
"facing-correction (per-asset yaw is available in batch.json) — Lane B/C rotate "
|
||||||
"directional props (counter, food_cart) to taste when placing",
|
"directional props (counter, food_cart) to taste when placing",
|
||||||
|
|||||||
@ -692,7 +692,7 @@ PACK = {
|
|||||||
"ambience-rain": ("ambience", amb_rain, dict(loop=True, gain=0.45, note="layer over street beds")),
|
"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-retail": ("ambience", amb_roomtone_retail, dict(loop=True, gain=0.35)),
|
||||||
"roomtone-milkbar": ("ambience", amb_roomtone_milkbar, 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
|
||||||
"sfx-doorbell": ("sfx", sfx_doorbell, dict(loop=False, gain=0.7)),
|
"sfx-doorbell": ("sfx", sfx_doorbell, dict(loop=False, gain=0.7)),
|
||||||
"sfx-door-open": ("sfx", sfx_door_open, dict(loop=False, gain=0.6)),
|
"sfx-door-open": ("sfx", sfx_door_open, dict(loop=False, gain=0.6)),
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"depot": "https://digalot.fyi/3god",
|
"depot": "https://digalot.fyi/3god",
|
||||||
"conventions": {
|
"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",
|
"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_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",
|
"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",
|
||||||
|
|||||||
@ -128,23 +128,25 @@ export class GigCrew {
|
|||||||
_make(target, key, { x, y, z, ry, height, pedIndex = null }) {
|
_make(target, key, { x, y, z, ry, height, pedIndex = null }) {
|
||||||
const r = rng(this.citySeed, 'gig', key);
|
const r = rng(this.citySeed, 'gig', key);
|
||||||
const h = height != null ? height : 1.6 + r() * 0.3;
|
const h = height != null ? height : 1.6 + r() * 0.3;
|
||||||
let actor, kind;
|
let actor, kind, usedIndex = null;
|
||||||
if (this.fleet && this.fleet.ready) {
|
if (this.fleet && this.fleet.ready) {
|
||||||
const pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
|
const pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
|
||||||
const rig = pk && this.fleet.all[pk.index];
|
const rig = pk && this.fleet.all[pk.index];
|
||||||
const sp = rig && spawnRig(rig, { ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
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'; }
|
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);
|
actor.fig.position.set(x, y, z);
|
||||||
target.add(actor.fig);
|
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, watchPoints, gig, roster }) — the band on the deck + crowd at the watch points.
|
||||||
spawn(roomGroup, { stage = null, watchPoints = [], gig = { gigId: 0 }, instrumentFor: instFor = instrumentFor } = {}) {
|
// 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();
|
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++;
|
this._gen++;
|
||||||
const gen = this._gen;
|
const gen = this._gen;
|
||||||
|
|
||||||
@ -157,10 +159,10 @@ export class GigCrew {
|
|||||||
const seated = p.role === 'drums' || !!p.seated;
|
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 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 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,
|
{ 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 });
|
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: [] };
|
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);
|
const inst = instFor(p.role);
|
||||||
if (inst) { actor.fig.add(inst); member.extra.push(inst); }
|
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)
|
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.55–1.95 m, inside the [1.4,2.0] gate.
|
||||||
const pts = watchPoints.slice(0, Math.min(CROWD_CAP, watchPoints.length));
|
const pts = watchPoints.slice(0, Math.min(CROWD_CAP, watchPoints.length));
|
||||||
pts.forEach((w) => {
|
let fromRoster = 0;
|
||||||
const { actor, kind } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`, { x: w.x, y: 0, z: w.z, ry: w.ry });
|
pts.forEach((w, i) => {
|
||||||
this.members.push({ actor, kind, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false,
|
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: [] });
|
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
|
// Swap a band member's primitive instrument for E's manifest GLB, placeholder-persists style. Fail-soft
|
||||||
|
|||||||
@ -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).
|
// 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.disposeAll();
|
||||||
this._gen++;
|
this._gen++;
|
||||||
this.gid = gigId | 0;
|
this.gid = gigId | 0;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
this.onAdmit = onAdmit;
|
||||||
const anchor = this._anchor(door, queueZone);
|
const anchor = this._anchor(door, queueZone);
|
||||||
if (!anchor) return { queue: 0 };
|
if (!anchor) return { queue: 0 };
|
||||||
this.anchor = anchor;
|
this.anchor = anchor;
|
||||||
@ -81,9 +84,10 @@ export class VenueQueue {
|
|||||||
const key = `${this.gid}:${slot}`;
|
const key = `${this.gid}:${slot}`;
|
||||||
const r = rng(this.citySeed, 'queueh', key);
|
const r = rng(this.citySeed, 'queueh', key);
|
||||||
const h = 1.58 + r() * 0.34;
|
const h = 1.58 + r() * 0.34;
|
||||||
let actor, kind;
|
let actor, kind, pedIndex = null;
|
||||||
if (this.fleet && this.fleet.ready) {
|
if (this.fleet && this.fleet.ready) {
|
||||||
const pk = pickRig(this.fleet, r());
|
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 rig = pk && this.fleet.all[pk.index];
|
||||||
const sp = rig && spawnRig(rig, { ry: a.ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
const sp = rig && spawnRig(rig, { ry: a.ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
||||||
if (sp) { actor = sp; kind = 'rig'; }
|
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'; }
|
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);
|
actor.fig.position.set(tx, 0, tz);
|
||||||
this.parent.add(actor.fig);
|
this.parent.add(actor.fig);
|
||||||
this.members.push({ actor, kind, slot, target: { x: tx, z: tz }, base: { ry: a.ry },
|
this.members.push({ actor, kind, slot, pedIndex, height: h, key, // key = `${gid}:${slot}` — stable per punter (slot shuffles, key doesn't)
|
||||||
phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
|
target: { x: tx, z: tz }, base: { ry: a.ry }, phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
_gap() {
|
_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); }
|
_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.
|
// 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() {
|
admitOne() {
|
||||||
if (!this.members.length) return 0;
|
if (!this.members.length) return null;
|
||||||
// the head = the smallest slot (closest to the anchor)
|
// the head = the smallest slot (closest to the anchor)
|
||||||
let headIdx = 0;
|
let headIdx = 0;
|
||||||
for (let i = 1; i < this.members.length; i++) if (this.members[i].slot < this.members[headIdx].slot) headIdx = i;
|
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];
|
const head = this.members.splice(headIdx, 1)[0];
|
||||||
if (head.actor.fig.parent) head.actor.fig.parent.remove(head.actor.fig);
|
if (head.actor.fig.parent) head.actor.fig.parent.remove(head.actor.fig);
|
||||||
head.actor.dispose && head.actor.dispose();
|
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
|
// shuffle everyone forward one slot → new target one step closer to the door
|
||||||
const a = this.anchor, gap = this._gap();
|
const a = this.anchor, gap = this._gap();
|
||||||
for (const m of this.members) {
|
for (const m of this.members) {
|
||||||
@ -119,7 +125,8 @@ export class VenueQueue {
|
|||||||
m.target.z = a.z + a.trail.z * gap * m.slot;
|
m.target.z = a.z + a.trail.z * gap * m.slot;
|
||||||
}
|
}
|
||||||
this._drainAt = this.t + this._nextDrainGap();
|
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
|
// idle shuffle + slide up into vacated slots. The seeded auto-drain is a safety so the line empties
|
||||||
|
|||||||
@ -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._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.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._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._dropKeys = []; // scratch: chunks to drop this frame
|
||||||
this._activeList = []; // this frame's active citizens (both modes) — test page reads it
|
this._activeList = []; // this frame's active citizens (both modes) — test page reads it
|
||||||
this.timeOfDay = 0.5; // noon
|
this.timeOfDay = 0.5; // noon
|
||||||
@ -164,14 +168,14 @@ export class CitizenSim {
|
|||||||
if (this.streamMode) return;
|
if (this.streamMode) return;
|
||||||
this.roster.forEach(c => this._releaseActor(c)); this.roster.length = 0; // tear down the v1 roster
|
this.roster.forEach(c => this._releaseActor(c)); this.roster.length = 0; // tear down the v1 roster
|
||||||
if (!this.chunkEdges) this._buildChunkIndex();
|
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.streamRadius = radius; this.streamPerChunk = perChunk;
|
||||||
this.streamMode = true; this._hookDriven = false;
|
this.streamMode = true; this._hookDriven = false;
|
||||||
}
|
}
|
||||||
disableStream() {
|
disableStream() {
|
||||||
if (!this.streamMode) return;
|
if (!this.streamMode) return;
|
||||||
for (const arr of this.chunkRosters.values()) arr.forEach(c => this._releaseActor(c));
|
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
|
// 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.
|
// per venue off its per-venue state; the alpha single-venue call (one id) is a subset — still works.
|
||||||
setGig(venueShopId, on = true) {
|
setGig(venueShopId, on = true) {
|
||||||
if (venueShopId == null) return;
|
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 }; }
|
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 = []);
|
let occ = this._occupancy.get(id); if (!occ) this._occupancy.set(id, occ = []);
|
||||||
occ.push({ seed: c.id, enteredAt: this.timeOfDay, pedIndex: c.pedIndex });
|
occ.push({ seed: c.id, enteredAt: this.timeOfDay, pedIndex: c.pedIndex });
|
||||||
c._occShop = id;
|
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) {
|
_emerge(c) {
|
||||||
if (c._occShop != null) { this._removeOccupant(c._occShop, c.id); c._occShop = null; }
|
if (c._occShop != null) { this._removeOccupant(c._occShop, c.id); c._occShop = null; }
|
||||||
const w = c._savedWalk;
|
const w = c._savedWalk;
|
||||||
@ -415,6 +433,20 @@ export class CitizenSim {
|
|||||||
const occ = this._occupancy.get(shopId) || [];
|
const occ = this._occupancy.get(shopId) || [];
|
||||||
return { count: occ.length, occupants: occ.map(o => ({ ...o })) };
|
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
|
// 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.
|
// holds while citizens walk (the determinism gate: same seed → same crowd, twice). Both modes.
|
||||||
identitySignature() {
|
identitySignature() {
|
||||||
|
|||||||
@ -145,7 +145,13 @@ function structuralSuite(plan, label) {
|
|||||||
// The v3 GIG layer (?gigs=1). Runs the full structuralSuite (venue-aware) plus gig-specific
|
// 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.
|
// invariants. Validates the DISTRICT interface every downstream lane (C/D/B/F) hangs off this round.
|
||||||
function gigSuite(plan, label) {
|
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 venues = plan.shops.filter(s => s.venue);
|
||||||
const byId = new Map(venues.map(v => [v.id, v]));
|
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);
|
const d = Math.hypot(p.x - nx, p.z - nz);
|
||||||
if (d < best) { best = d; bestEdge = e; }
|
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)` : ''));
|
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)}`);
|
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 2–4 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 2–4 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 ──────────────────
|
// ── 4. every facade skin referenced by the registry exists on disk ──────────────────
|
||||||
section('assets on disk');
|
section('assets on disk');
|
||||||
for (const f of allFacadeSkins()) ok(existsSync(join(ASSETS, f)), `registry facade exists: ${f}`);
|
for (const f of allFacadeSkins()) ok(existsSync(join(ASSETS, f)), `registry facade exists: ${f}`);
|
||||||
|
|||||||
@ -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
|
// 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).
|
// 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
|
// 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;
|
const gig = opts && opts.gig;
|
||||||
if (gig && (gig.on || gig.genreKey)) {
|
if (gig && (gig.on || gig.genreKey)) {
|
||||||
const genreKey = gig.genreKey || (shop && shop.genreKey) || GENRE_BY_TYPE[type] || 'pubrock';
|
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,
|
places: lay.places,
|
||||||
counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose
|
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)
|
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 [])
|
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 (R12): { x,z,w,d,deckY,frontZ,bandPoses[3] } (else null)
|
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)
|
audio: audioFor(recipe.key, ctx, opts, norm.raw), // { musicKey, toneKey, gigKey? } — Lane E manifest.audio keys (R11/R12)
|
||||||
dims: { ...dims, archetype, type: recipe.key },
|
dims: { ...dims, archetype, type: recipe.key },
|
||||||
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
|
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
|
||||||
|
|||||||
@ -432,7 +432,7 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
|||||||
placed: placed.filter(p => !p.removed),
|
placed: placed.filter(p => !p.removed),
|
||||||
places, decor: decor.meshes, grid, pathOK, carved,
|
places, decor: decor.meshes, grid, pathOK, carved,
|
||||||
spawnCell, targetCell, placement, browsePoints,
|
spawnCell, targetCell, placement, browsePoints,
|
||||||
watchPoints, // venue: 6–8 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)
|
stage: stageInfo, // venue: { x,z,w,d,deckY,frontZ,bandPoses } (else null)
|
||||||
counter: {
|
counter: {
|
||||||
mesh: counter.fitting.group,
|
mesh: counter.fitting.group,
|
||||||
|
|||||||
@ -205,15 +205,13 @@ export function createAudioEngine(PROCITY, { noassets = false, mute = false } =
|
|||||||
const lot = pub && (PROCITY.plan?.lots || []).find((l) => l.id === pub.lot);
|
const lot = pub && (PROCITY.plan?.lots || []).find((l) => l.id === pub.lot);
|
||||||
if (lot) venues.push({ shop: pub, lot, genreKey: pub.genreKey || 'pubrock' });
|
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:
|
// Per-venue gig state for the muffled spill (R14: the R12 alpha alias hop is retired):
|
||||||
// 1. F R13 per-venue map window.PROCITY.gigs.byVenue[id].state
|
// 1. F's per-venue map window.PROCITY.gigs.byVenue[id] (the canonical surface since R13)
|
||||||
// 2. alpha alias window.PROCITY.gigs is the single machine → its state for its venueShopId
|
// 2. clock fallback plan.gigs (night 0) vs the current segment (works before F wires / headless)
|
||||||
// 3. clock fallback plan.gigs (night 0) vs the current segment (works before/without F)
|
|
||||||
// Returns null only when there is no gig layer at all (flags-off ⇒ byte-identical, no spill).
|
// Returns null only when there is no gig layer at all (flags-off ⇒ byte-identical, no spill).
|
||||||
function venueGigState(shopId) {
|
function venueGigState(shopId) {
|
||||||
const g = PROCITY.gigs;
|
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.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 gigs = PROCITY.plan?.gigs; if (!gigs || !gigs.length) return null;
|
||||||
const ton = gigs.find((x) => x.night === 0 && x.venueShopId === shopId);
|
const ton = gigs.find((x) => x.night === 0 && x.venueShopId === shopId);
|
||||||
if (!ton) return 'quiet'; // dark tonight — reads true
|
if (!ton) return 'quiet'; // dark tonight — reads true
|
||||||
|
|||||||
@ -137,6 +137,25 @@ export function installDBG(deps) {
|
|||||||
|| (plan.shops || []).find((s) => s.type === 'pub');
|
|| (plan.shops || []).find((s) => s.type === 'pub');
|
||||||
return shop ? poseForShop(shop, dist) : null;
|
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).
|
// 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
|
// 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 ──
|
// ── 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
|
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
|
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 = {
|
const DBG = {
|
||||||
|
|||||||
@ -185,13 +185,13 @@ export function createVenuePresentation(plan, skins, scene, { assetBase = 'asset
|
|||||||
|
|
||||||
scene.add(group);
|
scene.add(group);
|
||||||
|
|
||||||
// ── per-venue state → frontage glow. Accepts F's per-venue map { [venueShopId]: state }, a Map, an
|
// ── per-venue state → frontage glow. F passes its per-venue map `gigState.byVenue`
|
||||||
// { byVenue } wrapper, OR a bare state string (alpha compat → the primary pub). Idempotent; the shell
|
// ({ [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. ────────────────
|
// calls it every street frame, so pool/glow ramps and the night baseline lerp here. ────────────────
|
||||||
function stateGetter(gigStates) {
|
function stateGetter(gigStates) {
|
||||||
if (gigStates == null) return () => 'quiet';
|
if (gigStates == null) return () => 'quiet';
|
||||||
if (typeof gigStates === 'string') return (id) => (id === primaryId ? gigStates : 'quiet');
|
const map = gigStates.byVenue || gigStates; // F passes gigState.byVenue (per-venue map)
|
||||||
const map = gigStates.byVenue || gigStates;
|
|
||||||
return (id) => {
|
return (id) => {
|
||||||
const v = map instanceof Map ? map.get(id) : map[id];
|
const v = map instanceof Map ? map.get(id) : map[id];
|
||||||
if (v == null) return 'quiet';
|
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;
|
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() {
|
function dispose() {
|
||||||
for (const d of disposables) d.dispose && d.dispose();
|
for (const d of disposables) d.dispose && d.dispose();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user