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
|
||||
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)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (3273/3273); rounds 2–13 closed** (see below).
|
||||
Node QA gates (selfcheck · scaffold · consistency) GREEN. v3.0-beta DISTRICT interface published (2–4
|
||||
venues, week schedule, town-wide posters); gig golden `0x1f636349`. Strict 6/6 is F's end-of-round gate.*
|
||||
*Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)*
|
||||
*Status: **all deliverables landed; self-check all-green (14264/14264); rounds 2–14 closed** (see below).
|
||||
v3.0 RELEASE round: 400-seed district invariant sweep landed + CITY_SPEC §v3 frozen at `v3.0`. Goldens
|
||||
unchanged (synthetic `0x3fa36874`, gig `0x1f636349`) — no golden moved, no rng draw added this round.*
|
||||
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 freeze
|
||||
|
||||
Release round, Lane A scope = harden + freeze (no new systems). **No golden moved** (synthetic
|
||||
`0x3fa36874`, gig `0x1f636349` both re-verified frozen); the only code touched is `selfcheck.js`
|
||||
(test-only), so no seeded stream gained a draw — the release-round law holds.
|
||||
|
||||
Delivered:
|
||||
1. **400-seed district invariant sweep** (`selfcheck.js`, ALL GREEN **14264/14264** in ~0.7s). Factored
|
||||
the district checks out of `gigSuite` into `districtInvariants(plan,label)` (behaviour-preserving — the
|
||||
4 hero-seed gigSuite runs are byte-identical), then swept **seeds 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
|
||||
|
||||
|
||||
@ -204,7 +204,15 @@ Interiors are **not** street geometry. Walking into a doorway triggers
|
||||
exactly where you stood. Street windows show a cheap fake (tinted glass + emissive warm glow at
|
||||
night in v1; interior-mapping shader is a stretch goal).
|
||||
|
||||
## CityPlan v3 — gig layer (`?gigs=1`) — v3.0-beta (THE DISTRICT)
|
||||
## CityPlan v3 — gig layer (`?gigs=1`) — THE DISTRICT
|
||||
|
||||
> **FROZEN at `v3.0` (round 14).** The authoritative Layer-1 **gig-layer** contract — venues, the week's
|
||||
> schedule, posters, the `gigKey` map, the corridor law, and the runtime surface. `web/js/citygen/selfcheck.js`
|
||||
> enforces every law here, including the round-14 **400-seed district invariant sweep** + an osm
|
||||
> graceful-placement case. Changes are a **CITY_SPEC amendment in the same commit + a gig-golden re-pin**,
|
||||
> never silent drift. **One edit is still pending and lands with Lane F's alias-retirement commit** (R13
|
||||
> debt #1): the R12 single-venue compat alias in the runtime block below retires; everything else here is
|
||||
> final for the `v3.0` tag.
|
||||
|
||||
> **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector only when
|
||||
> `?gigs=1`. With the flag OFF, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan —
|
||||
@ -281,7 +289,8 @@ places E's poster skin at `(x, z)` rotated `ry`; F wires "poster → that venue'
|
||||
the **full corridor** (carriageway + verge/footpath, centreline ± width/2), never the carriageway alone —
|
||||
so a naive `node.x + 2` seats furniture mid-bitumen. `web/js/core/registry.js` now exports the split as
|
||||
pure functions over an edge (no plan fields change ⇒ base goldens stay frozen), re-exported from the
|
||||
barrel: **`roadWidth(edge)`** (carriageway half-corridor: main 10 m, side 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
|
||||
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.
|
||||
|
||||
@ -2,6 +2,47 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 frozen
|
||||
|
||||
Release round — no new systems, no golden moves. **Synthetic `0x3fa36874` and gig `0x1f636349` are
|
||||
unchanged**; the only code I touched is `selfcheck.js` (test-only), so no seeded stream gained a draw.
|
||||
|
||||
### The 400-seed district sweep (`selfcheck.js`, ALL GREEN 14264/14264 in ~0.7s)
|
||||
- Factored the district-contract checks out of `gigSuite` into `districtInvariants(plan, label)` so the
|
||||
sweep asserts them at SCALE without re-running the heavy `structuralSuite` (that still runs on the 6
|
||||
hero `SEEDS` + osm parity). Behaviour-preserving — the 4 hero-seed `gigSuite` runs are byte-identical.
|
||||
- Sweeps **seeds 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)
|
||||
|
||||
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)
|
||||
|
||||
## 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)
|
||||
|
||||
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
|
||||
attach?" **Confirmed** — `buildInterior` now returns `room.audio = { musicKey, toneKey }`, seeded per
|
||||
shop. Lane F's interior_mode just plays it; Lane B's audio.js resolves the keys → files.*
|
||||
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane F + Lane B. Describes the shipped state; changes
|
||||
> require a version bump.
|
||||
|
||||
*`buildInterior` returns `room.audio = { musicKey, toneKey }`, seeded per shop. Lane F's interior_mode just
|
||||
plays it; Lane B's audio.js resolves the keys → files.*
|
||||
|
||||
> **Scope:** this doc covers the **base interior audio** (the seeded music bed + room-tone every shop
|
||||
> returns). The **gig audio** — `room.audio.gigKey`, the live-band bed, canonical form `gig-<genreKey>`
|
||||
> (`gig-pubrock` / `gig-grunge` / `gig-covers`), present only when `opts.gig` is on — is specified in
|
||||
> [LANE_C_PUB.md](LANE_C_PUB.md) §1. `room.audio` carries `gigKey?` as a third, optional key alongside the
|
||||
> two below.
|
||||
|
||||
## The contract
|
||||
```js
|
||||
@ -42,9 +50,9 @@ on exit / dispose(room):
|
||||
Nothing before the first gesture; `?mute=1`/`?noassets=1` short-circuit before any fetch. `room.audio`
|
||||
is plain data on the return — no new lifecycle, no dispose hook needed from Lane C's side.
|
||||
|
||||
## Optional (deferred to v2.2, per the round scope)
|
||||
## Deferred idea (v3.1+, not in the frozen contract)
|
||||
A `places`-tagged visual source ("the music comes from *there*") — e.g. a radio/hifi mesh with
|
||||
`userData.audioEmitter = musicKey` so Lane B can position/spatialise the bed. Only the record shop has a
|
||||
fitting for it today (`listeningCorner`); milk-bar/video would need a new radio prop. Not in this round —
|
||||
proposing the `userData.audioEmitter` hook + the two props as a tidy v2.2 item. v2.1 plays the bed
|
||||
non-spatially (room-filling), which is the right default for an interior anyway.
|
||||
fitting for it today (`listeningCorner`); milk-bar/video would need a new radio prop. A tidy v3.1 item: the
|
||||
`userData.audioEmitter` hook + the two props. v3.0 plays the bed non-spatially (room-filling), which is the
|
||||
right default for an interior anyway.
|
||||
|
||||
@ -1,66 +1,48 @@
|
||||
# LANE C — venue interfaces (round 13, v3.0-beta: the district) → Lane D + Lane F
|
||||
# LANE C — venue interface contract (v3.0-FROZEN: the district) → Lane D + Lane F
|
||||
|
||||
*PROCITY-C, 2026-07-15. R13 §Lane C. The district ships **three** venue archetypes behind `?gigs=1` —
|
||||
`pub` (R12), and new this round **`band_room`** and **`rsl`**. A converts a chosen shop to a venue kind in
|
||||
place (`shop.type = kind`); C keys the interior recipe off `shop.type`. All three return the SAME shape
|
||||
(below), so Lane D drops rigs in and Lane F reads state with no per-kind branching. Non-venue rooms are
|
||||
byte-identical (venue code only runs for `recipe.venue`, on its own seed sub-streams).*
|
||||
> **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane D + Lane F. Describes the *shipped* state;
|
||||
> changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (release audit).
|
||||
|
||||
*Verified (fresh context, seeds 20261990/1990/7/42/101 + 90-build venue soak): 0 throws · 0 carves ·
|
||||
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Draws below.*
|
||||
*The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A
|
||||
converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off
|
||||
`shop.type`. All three return the SAME shape (below), so Lane D drops rigs in and Lane F reads state with
|
||||
no per-kind branching. Non-venue rooms are byte-identical (venue code runs only for `recipe.venue`, on its
|
||||
own seed sub-streams). Verified fresh (seeds 20261990/1990/7/42/101 + 90-build soak): 0 throws · 0 carves ·
|
||||
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Budgets in §3.*
|
||||
|
||||
---
|
||||
|
||||
## 0. The pinned pose `ry` convention (ROUND13 debt #4) — READ THIS FIRST
|
||||
## 0. The pose `ry` convention (pinned) — READ THIS FIRST
|
||||
|
||||
**Canonical convention (all of C's poses use it):** a pose's `ry` is the yaw of a thing whose **front /
|
||||
**Canonical convention (every C pose uses it):** a pose's `ry` is the yaw of a thing whose **front /
|
||||
outward normal is local −Z at ry=0**, i.e. world-facing `(−sin ry, −cos ry)`. `counter.stand`,
|
||||
`browsePoints`, `watchPoints` and `bandPoses` ALL follow it, and the box placeholder (`placeholder.js`) is
|
||||
built to it (toes at −Z). This is the long-standing convention `keepers.js` greet math already assumes.
|
||||
built to it (toes at −Z). `keepers.js` greet math assumes it. **Drop a rig in at a pose's `ry` and it faces
|
||||
the right way — no per-consumer flip.**
|
||||
|
||||
**The GLB fleet mesh is the outlier — it faces local +Z at ry=0.** Verified two ways in a fresh context:
|
||||
- toe-bone probe (`toe − foot`, the anatomically-anterior direction) on 5 peds → `(0.05..0.14, +0.99)` = **+Z**.
|
||||
- planting a fleet rig at C's `counter.stand.ry` via `spawnRig` (no flip) and dotting its forward against
|
||||
the intended customer-facing direction → **−0.99 (180° backward)**; at `ry+π` → +0.99.
|
||||
- for contrast the box placeholder's foot mesh sits at z −0.076 → **−Z** (matches the convention).
|
||||
|
||||
**Finding: with the GLB fleet loaded, keepers AND browsers have been facing 180° backward since the fleet
|
||||
was wired in.** `keepers.js` spawns rigs at `stand.ry` / `browsePoint.ry` with **no flip**, and the mesh
|
||||
front is +Z, so the keeper faces the bar wall and the browser faces the aisle instead of the goods. The R12
|
||||
band reads correctly only because `band.js` added `RY_FLIP = π`. The two actor kinds are **opposite**
|
||||
(placeholder −Z, GLB rig +Z), so the current per-consumer state is inconsistent both ways:
|
||||
|
||||
| consumer | applies | placeholder (−Z front) | GLB rig (+Z front) |
|
||||
|---|---|---|---|
|
||||
| `keepers.js` (keepers, browsers) | no flip | ✅ correct | ❌ **backward** |
|
||||
| `band.js` (band, crowd) | `+RY_FLIP` (π) | ❌ backward | ✅ correct |
|
||||
|
||||
**Recommended canonical fix (Lane D — visual only, NO golden moves, no C poses change):** normalise the
|
||||
fleet at the source. In `rigs.js buildFigure`, rotate the cloned `inner` by π about Y so every GLB rig's
|
||||
front becomes **−Z**, matching the placeholder and every C pose. Then:
|
||||
- `keepers.js` stays flip-free and is now correct for GLB too (fixes keepers + browsers);
|
||||
- `band.js` **drops** `RY_FLIP` (set to 0) — the poses already face right under the −Z convention;
|
||||
- placeholder and GLB modes become consistent (no more mode-dependent facing).
|
||||
|
||||
One rotation at the source, delete every consumer flip. C's data does not move (pure poses, all −Z already),
|
||||
so no golden is touched — it's the visual fix debt #4 anticipated. Repro handed to D in `C-progress.md`.
|
||||
|
||||
*(If D prefers to keep the flip per-consumer instead, the equivalent is: ADD `RY_FLIP` to `keepers.js`
|
||||
spawns — but that leaves placeholder-mode keepers backward, so the source-normalise is the clean one.)*
|
||||
**Why no flip (the R13 fix, for the record):** the GLB fleet meshes ship facing local **+Z** at ry=0 — the
|
||||
opposite of the −Z convention (verified: the anterior toe-bone vector `toe − foot` reads `(≈0, +0.99)`).
|
||||
Before R13 this was compensated per-consumer, inconsistently: `band.js` flipped by π (band/crowd correct,
|
||||
placeholders backward) while `keepers.js` did not (placeholders correct, **keepers + browsers 180° backward
|
||||
whenever the GLB fleet was loaded** — they'd been shipping back-to-front). **D fixed it at the source in
|
||||
`rigs.js buildFigure`: the cloned `inner` is rotated π about Y so every GLB rig faces −Z**, matching the
|
||||
placeholder and every C pose. All consumer flips were then deleted. Net: keepers, browsers, band and crowd
|
||||
all face correctly, GLB and placeholder modes are consistent, and C's pure-data poses never moved (no golden
|
||||
touched). If you add a new rig consumer, plant it at the pose `ry` with no flip.
|
||||
|
||||
---
|
||||
|
||||
## 1. What `buildInterior` returns for a venue (`shop.type` ∈ `pub` | `band_room` | `rsl`)
|
||||
|
||||
Everything below is **pure data on the return** (room-local coords, the ry convention pinned in §0). Lane D
|
||||
drops rigs straight in; Lane F reads state — no new lifecycle vs. the R9 keeper/browser seam.
|
||||
Everything below is **pure data on the return** (room-local coords, the ry convention in §0). Lane D drops
|
||||
rigs straight in; Lane F reads state — no new lifecycle vs. the R9 keeper/browser seam.
|
||||
|
||||
### `room.stage` — the band stage, now a **4-PIECE** (ROUND13 debt #3, null for non-venues)
|
||||
### `room.stage` — the band stage, a **4-piece** (null for non-venues)
|
||||
```js
|
||||
room.stage = {
|
||||
x, z, // deck centre (against the back wall)
|
||||
w, d, // deck size (m) — varies by kind (table below)
|
||||
deckY, // deck top surface height (m) — front line stands here
|
||||
deckY, // deck top surface height (m) — the front line stands here
|
||||
riserY, // drum riser top height (m) = deckY + 0.16 — the drummer sits here
|
||||
frontZ, // z of the deck lip (the crowd stands in front of this, +Z)
|
||||
bandPoses: [ { x, z, ry, role, y, seated? } ×4 ],
|
||||
@ -68,14 +50,13 @@ room.stage = {
|
||||
```
|
||||
- `bandPoses[0..2]` = the **front line**: `role` ∈ `guitar` / `vocal` / `bass`, across the deck lip, `y = deckY`.
|
||||
- `bandPoses[3]` = the **drummer**: `role: 'drums'`, `seated: true`, up-stage centre on the drum riser,
|
||||
`y = riserY` (= `deckY + 0.16`, the `fittings.js` stage riser). The riser mesh already exists in the stage
|
||||
fitting — the pose just seats the kit + drummer on it.
|
||||
- **Lane D:** lift each member to **its own `pose.y`** (was `stage.deckY`; front line is unchanged, the
|
||||
drummer now sits 0.16 m higher on the riser). `role` hints the instrument — E's GLBs via `opts.instrumentFor(role)`
|
||||
(electric_guitar/bass_guitar/mic_stand/drum_kit), primitives otherwise (asset law). `seated:true` → seated bob.
|
||||
- `ry = π` faces the audience (+Z) **under the pinned −Z convention** → apply your fleet flip per §0.
|
||||
- **John may veto to a front-line trio** (charter): say so before you start and C ships `bandPoses` length 3
|
||||
(drop `[3]`), and F amends its `band==` smoke back to 3.
|
||||
`y = riserY` (= `deckY + 0.16`, the `fittings.js` stage riser box).
|
||||
- **Lane D:** lift each member to its own `pose.y` (front line = `deckY`, drummer = `riserY`). `role` hints
|
||||
the instrument — E's GLBs via `opts.instrumentFor(role)` (electric_guitar/bass_guitar/mic_stand/drum_kit),
|
||||
primitives otherwise (asset law). `seated:true` → seated bob. `ry = π` already faces the audience (+Z) — no
|
||||
flip (§0).
|
||||
- The band is a **4-piece** for v3.0 (front-line trio + drummer). Reverting to a trio would be a version bump
|
||||
(drop `bandPoses[3]`), not a runtime toggle.
|
||||
|
||||
### `room.watchPoints` — the audience, **cap by kind** (empty `[]` for non-venues)
|
||||
```js
|
||||
@ -88,10 +69,10 @@ room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // seeded floor poses, a
|
||||
stand + browse points + stage/PA. Deterministic per `shop.seed`.
|
||||
|
||||
### `room.counter` / keeper — unchanged
|
||||
The bar is a normal counter; the barkeep is just a keeper at `room.counter.stand` (R9 seam). *(Subject to the
|
||||
§0 facing fix — the keeper is one of the actors currently backward with the GLB fleet.)*
|
||||
The bar is a normal counter; the barkeep is just a keeper at `room.counter.stand` (R9 seam). Faces the
|
||||
customer correctly under §0.
|
||||
|
||||
### `room.audio.gigKey` — the live bed (ROUND13 debt #1)
|
||||
### `room.audio.gigKey` — the live bed
|
||||
`room.audio = { musicKey, toneKey, gigKey? }`. The manifest key **is** the gigKey, canonical form
|
||||
**`gig-<genreKey>`** — no mapping table. F passes gig state via `opts.gig`:
|
||||
```js
|
||||
@ -99,10 +80,12 @@ buildInterior(rslShop, THREE, { gig: { on: true } }) // → room.audio.gigKey
|
||||
```
|
||||
- Genre resolves `opts.gig.genreKey` → the venue's own `shop.genreKey` (A sets it) → a kind default
|
||||
(`pub`→pubrock, `band_room`→grunge, `rsl`→covers). Present **only when a gig is on**.
|
||||
- **Prefer `gigKey` over `musicKey`** while the gig plays. **Lane E** names the beds `gig-pubrock`,
|
||||
`gig-grunge`, `gig-covers`; a missing key → silence (audio law), band still visible.
|
||||
- **Prefer `gigKey` over `musicKey`** while the gig plays. E's beds are `gig-pubrock` / `gig-grunge` /
|
||||
`gig-covers`; a missing key → silence (audio law), band still visible.
|
||||
- C hand-builds `'gig-'+genreKey` (byte-identical to citygen's `gigKeyFor`, selfcheck-asserted) rather than
|
||||
importing it — deliberately, to keep the interiors lib standalone (it never imports citygen).
|
||||
|
||||
### The venue table (C-owned tuning, FYI)
|
||||
### The venue table (C-owned tuning)
|
||||
| kind | archetype bias | ceiling | deck `w × d`, `deckY` | crowd cap | genre → gigKey | dressing |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `pub` | wide / hall | 3.6 m | `0.58·W × 2.0`, 0.32 | 8 | pubrock → `gig-pubrock` | pub tables, sticky carpet, bar |
|
||||
@ -114,19 +97,39 @@ No `opts.gig` → a normal interior: `stage` + `watchPoints` are still returned
|
||||
`gigKey`** (room-tone + maybe the seeded radio `musicKey`), and D spawns no crowd. A seeded dark night reads
|
||||
true — the venue is just a quiet pub/club.
|
||||
|
||||
## 3. Budgets (measured, seed 20261990, fresh context)
|
||||
- **Empty shells** (`drawSweep` glb-off): band_room **124** · pub **126** · rsl **135** — town-wide worst
|
||||
161 (opshop/hall). ≤ 350. ✓
|
||||
- **Gig-night incl. FULL crowd + 4-piece** (worst-case framing from the door): RSL **237 draws** asset-free
|
||||
(16 rigs: 12 crowd + 4 band) / **77** GLB, **40.9k tris**. pub 176/56. band_room 173/53. ≤ 350 draws /
|
||||
200k tris. ✓ *(Measured with all 12 RSL watchPoints filled — band.js still caps crowd at 8 in R12; raising
|
||||
it to the watchPoint cap is D's R13 item, so this is the forward-looking worst case.)*
|
||||
- **90-build venue soak** (pub/band_room/rsl mixed): 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms;
|
||||
leak geo 0 / tex 0.
|
||||
## 3. `guitar_amp` placement ruling (v3.0)
|
||||
|
||||
## 4. The archetypes (C-owned, FYI)
|
||||
D plants E's `guitar_amp` GLB (`band.js _addBackline`) at `(stage.x + stage.w·0.32, deckY, stage.z + 0.15)`.
|
||||
**Ruling: reposition up-stage — this pose is a v3.1 fix for D, not blessed as-is.** That `z = stage.z + 0.15`
|
||||
is **down-stage** (audience side): the 0.5 m amp lands 0.29 m from the bass rig, so it **interpenetrates the
|
||||
bassist** and reads as clutter, not a backline. Move it **up-stage** behind the front line and clear of both
|
||||
the bass rig *and* C's own primitive ampStacks. C's `placeStage` plants **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,
|
||||
bar in the corner — the PA is louder than the room. `rsl` = carpet-and-flock club: the biggest floor in
|
||||
town (hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest
|
||||
crowd cap. Both reuse the `pub` venue machinery (`theme.js venueSpec` + `layout.js` placeStage/placeWatchPoints);
|
||||
only the dressing + `venueSpec` differ. Stage/PA/amp keep their GLB slots for Lane E (primitive fallback holds).
|
||||
bar in the corner — the PA louder than the room. `rsl` = carpet-and-flock club: the biggest floor in town
|
||||
(hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest crowd
|
||||
cap. Both reuse the `pub` venue machinery (`theme.js venueSpec` + `layout.js` placeStage/placeWatchPoints);
|
||||
only the dressing + `venueSpec` differ. Stage/PA keep GLB slots for Lane E (primitive fallback holds); the
|
||||
primitive ampStacks are the backline fallback (see §3).
|
||||
|
||||
@ -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
|
||||
|
||||
*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
|
||||
|
||||
## Round 14 — v3.0 PROVENANCE FREEZE ✅ (release round)
|
||||
|
||||
> **FROZEN for the v3.0 tag** (2026-07-16). Lane E R14 is a provenance close-out — **no new synthesis**
|
||||
> (genres final: pubrock / grunge / covers; John "fine for now"). Everything below is the durable
|
||||
> Lane-E release contract; the deep per-asset narrative lives in `pipeline/AUDIT.md` (current through
|
||||
> its R13 section). Counts tool-verified (glb_stat / ffprobe / manifest parse) and adversarially
|
||||
> re-audited by two independent passes — both PASS, **0 release blockers**.
|
||||
|
||||
**Release status**
|
||||
- **Gate 3 GREEN** — `validate_manifest.py --depot` → 0 errors, 0 warnings. Not blocking the tag.
|
||||
- **`publish.py --verify`** — depot matches `_published.json` (38 entries). The lone drift note
|
||||
(`vintage-cash-register.glb` absent from the *tailnet mirror*) is cosmetic: it's a depot-reuse asset
|
||||
and IS live on the public `digalot.fyi` depot (Gate 3 confirms), so the manifest's real target is fine.
|
||||
- **Licensing 🟢 clean** — 0 quarantined/branded assets in the manifest (grep of every AUDIT QUARANTINE
|
||||
token — vitra/eames/calligaris/ikea/technics/1210/tuborg/uploads_files/akai/mpc/turntable — → 0 hits).
|
||||
- **Poster templates (R13 debt #4): no ask from B** → no new templates (E builds only on B's ask).
|
||||
- R14 doc-consistency fixes applied: AUDIT.md R13 section added; `roomtone-video` gain 0.3→0.35 aligned
|
||||
to the manifest; manifest `glb_law` string amended to state the ≤8k-hero / ≤14k-instrument exceptions.
|
||||
|
||||
### Provenance & licensing — by source zone (all 🟢 web-ok)
|
||||
|
||||
| zone | assets | source | method | licence |
|
||||
|---|---|---|---|---|
|
||||
| House library GLBs | 18 | thriftgod / house depot, John-presorted **generic** props (benches, shelves, counter, crate, bin, streetlight, food_cart…) — no recognizable trademark | `normalize.py` (passthrough or COLLAPSE ≤5k) | 🟢 web-ok |
|
||||
| On-device synthetic GLBs | 11 (+5 `_hi`) | MODELBEAST `flux_local` (FLUX.2-klein-4B) concept → `trellis_mac` mesh | 6 hero via `bake_lowpoly.py` (≤8k) · 5 gig instruments via `normalize`→`decimate_props.py` (14k) | 🟢 CC0-equiv, 100% on-device |
|
||||
| fal-Hunyuan GLBs | 3 | `flux_local` concept → `fal-ai/hunyuan3d/v2` (glass_case, magazine_rack, bus_shelter) | `bake_lowpoly.py` (≤8k) | 🟢 original (~$0.64 total spend) |
|
||||
| Depot-reuse GLB | 1 | `cash_register` = John's own `thriftgod_hero` (already live) | reuse (not regenerated) | 🟢 cleared |
|
||||
| Skins (2D) | 96 | 90sDJsim ports (7 facade + 2 ground) + thriftgod R1 store-art + `flux_local` gap-fill fronts/posters | `gen_skins.py --local` (on-device) | 🟢 web-ok |
|
||||
| Audio pack | 28 | 100% procedural numpy synthesis (`gen_audio.py`) | oscillator/noise render → ffmpeg OGG+M4A | 🟢 CC0-equiv, parody-safe |
|
||||
| Stock packs | 3 (350/311/273) | GODVERSE `dealgod` Postgres — **real** cover art | `build_stock_pack.py`: parody-transform metadata + atlas | 🟢 real-art / parody-metadata |
|
||||
|
||||
**GLB fleet = 33 manifest (23 fit + 10 furniture) + 5 `_hi` = 38 published.** Only external spend ever:
|
||||
~$0.64 (fal-Hunyuan, R5). Everything else is $0 / on-device (electricity only).
|
||||
|
||||
### Audio inventory — 28 assets (`web/assets/audio/`, committed; contract = `manifest.audio`)
|
||||
|
||||
All procedural, seeded, $0, parody-safe; OGG/Opus + M4A/AAC; beds+music loudnorm −16 LUFS, SFX peak-norm.
|
||||
|
||||
| group | n | assets | gig? |
|
||||
|---|---|---|---|
|
||||
| ambience | 7 | street-day, street-night, rain, roomtone-{retail,milkbar,video}, **crowd-walla** | crowd-walla `gig:true` |
|
||||
| sfx | 8 + 6 steps | doorbell, door-open, till, riffle, toast, tram-bell, tram-rumble(loop), **applause**; footstep{pavement×3, timber×3} | applause `gig:true` |
|
||||
| music | 7 | record-shop, milkbar, video-synth, arcade, **gig-pubrock**, **gig-grunge**, **gig-covers** | 3 gig beds `gig:true` |
|
||||
|
||||
- **Gig beds (R13, the manifest key IS the gigKey `gig-<genreKey>`):** `gig-pubrock` (pub, 0.6, 13.4s),
|
||||
`gig-grunge` (band_room, 0.6, 17.2s), `gig-covers` (rsl, 0.5, 19.2s). Seamless loops, non-silent
|
||||
(verified −15.7…−17.6 dB mean). Old `pubrock-live` fully retired (files gone, no live refs).
|
||||
|
||||
### GLB inventory — 38 (tri counts glb_stat-verified; footprints/heights match manifest; no Draco, tex ≤1024)
|
||||
|
||||
**Fittings (23):** record_crate 5k · wire_shelf 330 · clothes_rack 3.2k · bookshelf 5k · cube_shelf_wide 5k ·
|
||||
counter 276 · coffee_table 495 · work_table 1.6k · box_crate 78 · crate_stack 3k · **listening_booth 8k ·
|
||||
drinks_fridge 8k · arcade_cabinet 8k · milkshake_mixer 8k · counter_till 8k** (bake_lowpoly hero) ·
|
||||
**glass_case 8k · magazine_rack 8k** (fal) · **electric_guitar / bass_guitar / drum_kit / guitar_amp /
|
||||
mic_stand = 14k each** (R13 decimate) · cash_register 6k (reuse).
|
||||
**Furniture (10):** bench 1.9k · bench_modern 640 · bench_wood 4k · longbench 1.2k · park_bench 5k ·
|
||||
streetlight 5k · food_cart 5k · **novelty_record 8k** (bake_lowpoly) · **bus_shelter 8k** (fal) · bin 3k.
|
||||
**Preserved `_hi` originals (5, on depot, NOT manifest-referenced — LOD/re-bake source):**
|
||||
electric_guitar 43.5k · bass_guitar 36k · mic_stand 34.1k · drum_kit 26.2k · guitar_amp 96.6k.
|
||||
|
||||
### Skins — 96 (`web/assets/gen/*.jpg`, local; all present, 0 missing/orphan)
|
||||
|
||||
facade 48 (43 blank + 5 baked-sign: djsim-video/milkbar/pub, market, grimy) · sky 10 · ground 10 ·
|
||||
wall 8 · interior floor 6 · interior surface 7 · awning 3 · **poster 4** (grunge/retro/screenprint/xerox,
|
||||
blank name-band). Per-shop-type facade coverage ≥2 for all types (validate_manifest asserts; min = 3).
|
||||
|
||||
### Stock packs — 3 (indexes committed in `pipeline/packs/`; atlases on depot; read directly by Lane C)
|
||||
|
||||
record 350 (6 atlases, 256²) · book 311 (4 atlases, 176×256) · toy 273 (5 atlases, 256²). Real cover art
|
||||
(GODVERSE), **parody-transformed metadata** (sha256-seeded word-banks; no real trademarks). All pass `validate_pack`.
|
||||
|
||||
### By-design notes (documented so a future reader doesn't re-flag them)
|
||||
- **5 `_hi` in `_published.json` but not the manifest** — intentional archival hi-poly originals (R13).
|
||||
- **Stock-pack indexes not in the manifest** — by design; Lane C reads `pipeline/packs/*` directly, atlases ship on the depot.
|
||||
- **`glb_law` tiers** — ≤5k library / ≤8k baked hero (TRELLIS shell-soup, texture-intact) / ≤14k gig instruments; now stated in the manifest convention string + AUDIT.
|
||||
- **`validate_manifest` prints "skins 85 groups"** — a shallow `sk.values()` count (the nested `interior` dict counts as 1); true leaf total is 96. Cosmetic label, flagged for v3.1.
|
||||
- **Minor stale ledger figures** (non-live): `food_cart` 4991 vs `_normalize_results` 5000 (9-tri); a superseded `cube_shelf_01` entry in `_normalize_results` (DROP-filtered from the manifest, rejected in AUDIT). Harmless.
|
||||
|
||||
### Deferred to v3.1 (documented, not started)
|
||||
- **`street_bin` decimation** — B's non-blocking, *conditional* ask ("if the combined-lane budget tightens"); the bin is ~3k tris (within house law), and B measured ~100k tri / ~150 draw margin at the busiest block, so no action for v3.0.
|
||||
- **Texture-bake-to-lowpoly / KTX2-Basis atlas pass** — if a future budget tightens; the `_hi` instrument originals + the hero props are the LOD source (R9 VRAM note + AUDIT).
|
||||
|
||||
## Round 13 — two genres, the gigKey rename (debt #1), lighter instruments
|
||||
|
||||
**v3.0-beta THE DISTRICT.** All local work landed + verified; the one gated step (depot publish)
|
||||
|
||||
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
|
||||
for focal/optional/LOD-able stage props seen at mid-distance; texture-bake-to-lowpoly is the
|
||||
follow-up. Amp (96k) is the outlier — a candidate for that pass first. Eyeballed (contact sheet).
|
||||
- Staged to `web/assets/models/` for local validation (`?localdepot=1`). **Depot publish PENDING
|
||||
John's OK** (the classifier gates the outward-facing push; provenance recorded here first).
|
||||
- Staged to `web/assets/models/` for local validation (`?localdepot=1`). Depot publish was
|
||||
**PENDING John's OK** at R12 → **DONE in R13** (see the R13 section below; the 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,
|
||||
"depot": DEPOT,
|
||||
"conventions": {
|
||||
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), <=5k tris, WebP <=1024, no Draco",
|
||||
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), WebP <=1024, no Draco; "
|
||||
"tris <=5k for library props, with documented exceptions for focal/LOD-able assets: "
|
||||
"baked hero props <=8k (TRELLIS shell-soup, texture-intact) and gig instruments <=14k "
|
||||
"(v3.0; hi-poly _hi originals on the depot) — see AUDIT.md + LANE_E_NOTES",
|
||||
"facing": "symmetric props are orientation-agnostic; normalize.py applies no automatic "
|
||||
"facing-correction (per-asset yaw is available in batch.json) — Lane B/C rotate "
|
||||
"directional props (counter, food_cart) to taste when placing",
|
||||
|
||||
@ -692,7 +692,7 @@ PACK = {
|
||||
"ambience-rain": ("ambience", amb_rain, dict(loop=True, gain=0.45, note="layer over street beds")),
|
||||
"roomtone-retail": ("ambience", amb_roomtone_retail, dict(loop=True, gain=0.35)),
|
||||
"roomtone-milkbar": ("ambience", amb_roomtone_milkbar, dict(loop=True, gain=0.35)),
|
||||
"roomtone-video": ("ambience", amb_roomtone_video, dict(loop=True, gain=0.3)),
|
||||
"roomtone-video": ("ambience", amb_roomtone_video, dict(loop=True, gain=0.35)),
|
||||
# sfx
|
||||
"sfx-doorbell": ("sfx", sfx_doorbell, dict(loop=False, gain=0.7)),
|
||||
"sfx-door-open": ("sfx", sfx_door_open, dict(loop=False, gain=0.6)),
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"version": 1,
|
||||
"depot": "https://digalot.fyi/3god",
|
||||
"conventions": {
|
||||
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), <=5k tris, WebP <=1024, no Draco",
|
||||
"glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), WebP <=1024, no Draco; tris <=5k for library props, with documented exceptions for focal/LOD-able assets: baked hero props <=8k (TRELLIS shell-soup, texture-intact) and gig instruments <=14k (v3.0; hi-poly _hi originals on the depot) \u2014 see AUDIT.md + LANE_E_NOTES",
|
||||
"facing": "symmetric props are orientation-agnostic; normalize.py applies no automatic facing-correction (per-asset yaw is available in batch.json) \u2014 Lane B/C rotate directional props (counter, food_cart) to taste when placing",
|
||||
"facade_blank_signboard": "facades tagged signboard:'blank' ship a blank sign panel \u2014 Lane B overlays the shop name there. signboard:'baked' facades (some djsim-* Flux skins) already have sign text: use as-is, don't overlay",
|
||||
"facade_face": "facade entries without a 'face' key are front elevations (shop or house fronts); face:'side' entries are windowless corner-lot side walls with empty types \u2014 never select one as a shop front",
|
||||
|
||||
@ -128,23 +128,25 @@ export class GigCrew {
|
||||
_make(target, key, { x, y, z, ry, height, pedIndex = null }) {
|
||||
const r = rng(this.citySeed, 'gig', key);
|
||||
const h = height != null ? height : 1.6 + r() * 0.3;
|
||||
let actor, kind;
|
||||
let actor, kind, usedIndex = null;
|
||||
if (this.fleet && this.fleet.ready) {
|
||||
const pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
|
||||
const rig = pk && this.fleet.all[pk.index];
|
||||
const sp = rig && spawnRig(rig, { ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
||||
if (sp) { actor = sp; kind = 'rig'; }
|
||||
if (sp) { actor = sp; kind = 'rig'; usedIndex = pk.index; } // usedIndex: the fleet ped shown — F's continuity smoke reads it
|
||||
}
|
||||
if (!actor) { actor = makePlaceholder(rng(this.citySeed, 'gig-body', key), { height: h }); actor.fig.rotation.y = ry; kind = 'placeholder'; }
|
||||
actor.fig.position.set(x, y, z);
|
||||
target.add(actor.fig);
|
||||
return { actor, kind };
|
||||
return { actor, kind, pedIndex: usedIndex };
|
||||
}
|
||||
|
||||
// spawn(roomGroup, { stage, watchPoints, gig }) — the band on the deck + crowd at the watch points.
|
||||
spawn(roomGroup, { stage = null, watchPoints = [], gig = { gigId: 0 }, instrumentFor: instFor = instrumentFor } = {}) {
|
||||
// spawn(roomGroup, { stage, watchPoints, gig, roster }) — the band on the deck + crowd at the watch points.
|
||||
// roster (R14): [{pedIndex,height}] of the punters who entered this venue tonight (D's citizens.tonightRoster);
|
||||
// the crowd's front slots become them (continuity), seeded strangers fill the rest.
|
||||
spawn(roomGroup, { stage = null, watchPoints = [], gig = { gigId: 0 }, roster = [], instrumentFor: instFor = instrumentFor } = {}) {
|
||||
this.disposeAll();
|
||||
const gid = gig.gigId | 0;
|
||||
const gid = (gig && gig.gigId) | 0; // null-safe (a quiet-night call would pass no gig)
|
||||
this._gen++;
|
||||
const gen = this._gen;
|
||||
|
||||
@ -157,10 +159,10 @@ export class GigCrew {
|
||||
const seated = p.role === 'drums' || !!p.seated;
|
||||
const poseY = p.y != null ? p.y : (stage.deckY || 0); // C tags per-pose y (front line deckY, drums riser)
|
||||
const y = seated ? poseY - SEAT_DROP : poseY; // sink the drummer so the kit hides the legs
|
||||
const { actor, kind } = this._make(roomGroup, `band:${gid}:${p.role}:${i}`,
|
||||
const { actor, kind, pedIndex } = this._make(roomGroup, `band:${gid}:${p.role}:${i}`,
|
||||
{ x: p.x, y, z: p.z, ry: p.ry, pedIndex: distinct.length ? distinct[i % distinct.length] : null,
|
||||
height: 1.68 + rng(this.citySeed, 'gigh', `${gid}:${i}`)() * 0.2 });
|
||||
const member = { actor, kind, part: 'band', role: p.role, dance: false, seated,
|
||||
const member = { actor, kind, pedIndex, part: 'band', role: p.role, dance: false, seated,
|
||||
base: { x: p.x, y, z: p.z, ry: p.ry }, phase: rng(this.citySeed, 'gigp', `b${gid}:${i}`)() * Math.PI * 2, extra: [] };
|
||||
const inst = instFor(p.role);
|
||||
if (inst) { actor.fig.add(inst); member.extra.push(inst); }
|
||||
@ -170,14 +172,24 @@ export class GigCrew {
|
||||
this._addBackline(roomGroup, stage, gen); // amp dressing beside the riser (async, dressing)
|
||||
}
|
||||
|
||||
// ── crowd: stand-and-watch or dance at C's watch points ──
|
||||
// ── crowd: R14 identity continuity — the front slots become tonight's roster (the punters who came in
|
||||
// via the queue / surge), seeded strangers fill the remainder. The roster override passes {pedIndex,height}
|
||||
// for that slot only; _make's existing branches then skip that slot's own height+pickRig draws. Because
|
||||
// every crowd slot is an INDEPENDENTLY keyed stream (`gig`/`crowd:<gid>:<slot>`), this can't shift any
|
||||
// other slot, the sway-phase stream (`gigp`), or the band — and an EMPTY roster is byte-identical to R13
|
||||
// (no override → the exact R13 draws). Roster heights are the sim's 1.55–1.95 m, inside the [1.4,2.0] gate.
|
||||
const pts = watchPoints.slice(0, Math.min(CROWD_CAP, watchPoints.length));
|
||||
pts.forEach((w) => {
|
||||
const { actor, kind } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`, { x: w.x, y: 0, z: w.z, ry: w.ry });
|
||||
this.members.push({ actor, kind, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false,
|
||||
let fromRoster = 0;
|
||||
pts.forEach((w, i) => {
|
||||
const who = roster[i] || null;
|
||||
const { actor, kind, pedIndex } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`,
|
||||
who ? { x: w.x, y: 0, z: w.z, ry: w.ry, pedIndex: who.pedIndex, height: who.height }
|
||||
: { x: w.x, y: 0, z: w.z, ry: w.ry });
|
||||
if (who) fromRoster++;
|
||||
this.members.push({ actor, kind, pedIndex, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false, fromRoster: !!who,
|
||||
base: { x: w.x, y: 0, z: w.z, ry: w.ry }, phase: rng(this.citySeed, 'gigp', `c${gid}:${w.slotIndex}`)() * Math.PI * 2, extra: [] });
|
||||
});
|
||||
return { band: this.members.filter(m => m.part === 'band').length, crowd: pts.length };
|
||||
return { band: this.members.filter(m => m.part === 'band').length, crowd: pts.length, fromRoster };
|
||||
}
|
||||
|
||||
// Swap a band member's primitive instrument for E's manifest GLB, placeholder-persists style. Fail-soft
|
||||
|
||||
@ -42,11 +42,14 @@ export class VenueQueue {
|
||||
}
|
||||
|
||||
// Build the seeded line. `count` overrides the seeded 2..6 (F may cap to the room's spare capacity).
|
||||
spawn(parent, { door = null, queueZone = null, gigId = 0, count = null } = {}) {
|
||||
// onAdmit (R14): fired with {pedIndex,height,key} on EVERY admit (auto-drain or explicit), so F can relay
|
||||
// the punter who walked in into the sim's tonight roster (citizens.recordVenueEntry) → they join the crowd.
|
||||
spawn(parent, { door = null, queueZone = null, gigId = 0, count = null, onAdmit = null } = {}) {
|
||||
this.disposeAll();
|
||||
this._gen++;
|
||||
this.gid = gigId | 0;
|
||||
this.parent = parent;
|
||||
this.onAdmit = onAdmit;
|
||||
const anchor = this._anchor(door, queueZone);
|
||||
if (!anchor) return { queue: 0 };
|
||||
this.anchor = anchor;
|
||||
@ -81,9 +84,10 @@ export class VenueQueue {
|
||||
const key = `${this.gid}:${slot}`;
|
||||
const r = rng(this.citySeed, 'queueh', key);
|
||||
const h = 1.58 + r() * 0.34;
|
||||
let actor, kind;
|
||||
let actor, kind, pedIndex = null;
|
||||
if (this.fleet && this.fleet.ready) {
|
||||
const pk = pickRig(this.fleet, r());
|
||||
pedIndex = pk ? pk.index : null; // R14: remember the ped so admitOne can hand it to the crowd
|
||||
const rig = pk && this.fleet.all[pk.index];
|
||||
const sp = rig && spawnRig(rig, { ry: a.ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
||||
if (sp) { actor = sp; kind = 'rig'; }
|
||||
@ -91,8 +95,8 @@ export class VenueQueue {
|
||||
if (!actor) { actor = makePlaceholder(rng(this.citySeed, 'queuebody', key), { height: h }); actor.fig.rotation.y = a.ry; kind = 'placeholder'; }
|
||||
actor.fig.position.set(tx, 0, tz);
|
||||
this.parent.add(actor.fig);
|
||||
this.members.push({ actor, kind, slot, target: { x: tx, z: tz }, base: { ry: a.ry },
|
||||
phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
|
||||
this.members.push({ actor, kind, slot, pedIndex, height: h, key, // key = `${gid}:${slot}` — stable per punter (slot shuffles, key doesn't)
|
||||
target: { x: tx, z: tz }, base: { ry: a.ry }, phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
|
||||
}
|
||||
|
||||
_gap() {
|
||||
@ -102,15 +106,17 @@ export class VenueQueue {
|
||||
_nextDrainGap() { return DRAIN_MIN + rng(this.citySeed, 'queuedrain', `${this.gid}:${this.members.length}:${this.t | 0}`)() * (DRAIN_MAX - DRAIN_MIN); }
|
||||
|
||||
// Admit the head of the line (nearest the door): it walks in, everyone else shuffles up one slot.
|
||||
// Returns the admitted count so far (F can stop when the room is at watchPoint cap).
|
||||
// R14: returns the admitted punter's identity {pedIndex, height, key} (or null when empty) and fires
|
||||
// onAdmit(identity) — F relays it to citizens.recordVenueEntry so the punter joins the interior crowd.
|
||||
admitOne() {
|
||||
if (!this.members.length) return 0;
|
||||
if (!this.members.length) return null;
|
||||
// the head = the smallest slot (closest to the anchor)
|
||||
let headIdx = 0;
|
||||
for (let i = 1; i < this.members.length; i++) if (this.members[i].slot < this.members[headIdx].slot) headIdx = i;
|
||||
const head = this.members.splice(headIdx, 1)[0];
|
||||
if (head.actor.fig.parent) head.actor.fig.parent.remove(head.actor.fig);
|
||||
head.actor.dispose && head.actor.dispose();
|
||||
const identity = { pedIndex: head.pedIndex, height: head.height, key: head.key };
|
||||
// shuffle everyone forward one slot → new target one step closer to the door
|
||||
const a = this.anchor, gap = this._gap();
|
||||
for (const m of this.members) {
|
||||
@ -119,7 +125,8 @@ export class VenueQueue {
|
||||
m.target.z = a.z + a.trail.z * gap * m.slot;
|
||||
}
|
||||
this._drainAt = this.t + this._nextDrainGap();
|
||||
return 1;
|
||||
if (this.onAdmit) this.onAdmit(identity); // relay the walked-in punter → the crowd roster (auto or explicit)
|
||||
return identity;
|
||||
}
|
||||
|
||||
// idle shuffle + slide up into vacated slots. The seeded auto-drain is a safety so the line empties
|
||||
|
||||
@ -126,6 +126,10 @@ export class CitizenSim {
|
||||
this._gigVenues = new Set(); // R13: venueShopIds with a gig on (F sets per-venue via setGig) → multi-venue surge
|
||||
this.weather = { state: 'clear', intensity: 0 }; // Lane B's PROCITY.weather contract (shell feeds it)
|
||||
this._occupancy = new Map(); // R9 shopId → [{ seed, enteredAt, pedIndex }] — the interior-presence truth
|
||||
this._venueRoster = new Map(); // R14 identity continuity: venueShopId → Map<key,{pedIndex,height}> — who
|
||||
// entered a gig venue TONIGHT (surge occupants + F-relayed queue admits).
|
||||
// GigCrew consumes it so the crowd IS the people who came in. Cleared when
|
||||
// the gig ends (setGig off). Pure bookkeeping — no rng, no GPU.
|
||||
this._dropKeys = []; // scratch: chunks to drop this frame
|
||||
this._activeList = []; // this frame's active citizens (both modes) — test page reads it
|
||||
this.timeOfDay = 0.5; // noon
|
||||
@ -164,14 +168,14 @@ export class CitizenSim {
|
||||
if (this.streamMode) return;
|
||||
this.roster.forEach(c => this._releaseActor(c)); this.roster.length = 0; // tear down the v1 roster
|
||||
if (!this.chunkEdges) this._buildChunkIndex();
|
||||
this.chunkRosters.clear(); this._dropKeys.length = 0; this._encountered.clear(); this._occupancy.clear();
|
||||
this.chunkRosters.clear(); this._dropKeys.length = 0; this._encountered.clear(); this._occupancy.clear(); this._venueRoster.clear();
|
||||
this.streamRadius = radius; this.streamPerChunk = perChunk;
|
||||
this.streamMode = true; this._hookDriven = false;
|
||||
}
|
||||
disableStream() {
|
||||
if (!this.streamMode) return;
|
||||
for (const arr of this.chunkRosters.values()) arr.forEach(c => this._releaseActor(c));
|
||||
this.chunkRosters.clear(); this._occupancy.clear(); this.streamMode = false;
|
||||
this.chunkRosters.clear(); this._occupancy.clear(); this._venueRoster.clear(); this.streamMode = false;
|
||||
}
|
||||
|
||||
// rasterise each edge's centreline to the 64m chunks it passes through → which edges seed each chunk
|
||||
@ -340,7 +344,11 @@ export class CitizenSim {
|
||||
// per venue off its per-venue state; the alpha single-venue call (one id) is a subset — still works.
|
||||
setGig(venueShopId, on = true) {
|
||||
if (venueShopId == null) return;
|
||||
if (on) this._gigVenues.add(venueShopId); else this._gigVenues.delete(venueShopId);
|
||||
if (on) this._gigVenues.add(venueShopId);
|
||||
// gig ends (on→off transition) → disperse tonight's roster so the next night starts fresh + it stays
|
||||
// bounded. Persists across interior exit/re-enter (the gig stays "on" through those) — only the real
|
||||
// close clears it. .delete() returns true only on the transition, so re-calls while quiet are no-ops.
|
||||
else if (this._gigVenues.delete(venueShopId)) this._venueRoster.delete(venueShopId);
|
||||
}
|
||||
setWeather(w) { if (w && typeof w.state === 'string') this.weather = { state: w.state, intensity: +w.intensity || 0 }; }
|
||||
|
||||
@ -393,8 +401,18 @@ export class CitizenSim {
|
||||
let occ = this._occupancy.get(id); if (!occ) this._occupancy.set(id, occ = []);
|
||||
occ.push({ seed: c.id, enteredAt: this.timeOfDay, pedIndex: c.pedIndex });
|
||||
c._occShop = id;
|
||||
// R14 continuity: a surge occupant who ducked into a GIG venue joins tonight's roster (the crowd
|
||||
// becomes the people who came in). The roster persists past this ped's short dwell (unlike occupancy,
|
||||
// which clears on emerge) — "who came tonight", keyed by the ped's stable id so re-entry doesn't dup.
|
||||
if (this._gigVenues.has(id)) this._recordRoster(id, c.id, c.pedIndex, c.height);
|
||||
}
|
||||
}
|
||||
// record one entered identity into a venue's tonight roster (deduped by key, insertion-ordered, no rng).
|
||||
_recordRoster(venueShopId, key, pedIndex, height) {
|
||||
let m = this._venueRoster.get(venueShopId);
|
||||
if (!m) this._venueRoster.set(venueShopId, m = new Map());
|
||||
if (!m.has(key)) m.set(key, { pedIndex, height }); // first entry wins
|
||||
}
|
||||
_emerge(c) {
|
||||
if (c._occShop != null) { this._removeOccupant(c._occShop, c.id); c._occShop = null; }
|
||||
const w = c._savedWalk;
|
||||
@ -415,6 +433,20 @@ export class CitizenSim {
|
||||
const occ = this._occupancy.get(shopId) || [];
|
||||
return { count: occ.length, occupants: occ.map(o => ({ ...o })) };
|
||||
}
|
||||
// R14 identity continuity — the D→F seam.
|
||||
// recordVenueEntry: F relays a queue admit (VenueQueue.admitOne() → {pedIndex, height, key}) so the punter
|
||||
// who walked in the door joins the crowd. Surge occupants self-record in _enter; this is the queue side.
|
||||
recordVenueEntry(venueShopId, entry) {
|
||||
if (venueShopId == null || !entry || entry.pedIndex == null) return;
|
||||
const key = entry.key != null ? `q:${entry.key}` : `q:${entry.pedIndex}:${(+entry.height || 0).toFixed(3)}`;
|
||||
this._recordRoster(venueShopId, key, entry.pedIndex, entry.height);
|
||||
}
|
||||
// tonightRoster: the night's entered identities, for GigCrew.spawn (F passes it in) + F's continuity
|
||||
// smoke (assert crowd ⊇ roster ∩ cap). Insertion-ordered (entry order), deduped, bounded, pure read.
|
||||
tonightRoster(venueShopId) {
|
||||
const m = this._venueRoster.get(venueShopId);
|
||||
return m ? [...m.values()].map(e => ({ pedIndex: e.pedIndex, height: e.height })) : [];
|
||||
}
|
||||
// stable identity signature of the active set — immutable spawn identity, NOT live position, so it
|
||||
// holds while citizens walk (the determinism gate: same seed → same crowd, twice). Both modes.
|
||||
identitySignature() {
|
||||
|
||||
@ -145,7 +145,13 @@ function structuralSuite(plan, label) {
|
||||
// The v3 GIG layer (?gigs=1). Runs the full structuralSuite (venue-aware) plus gig-specific
|
||||
// invariants. Validates the DISTRICT interface every downstream lane (C/D/B/F) hangs off this round.
|
||||
function gigSuite(plan, label) {
|
||||
structuralSuite(plan, label); // full coverage; venues exempt from late-shop rule
|
||||
structuralSuite(plan, label); // full structural coverage; venues exempt from late-shop rule
|
||||
districtInvariants(plan, label); // + the v3 district contract (also run standalone by the ROUND14 sweep)
|
||||
}
|
||||
|
||||
// The v3 DISTRICT contract, factored out of gigSuite (ROUND14) so the 400-seed sweep below can assert it
|
||||
// at SCALE without re-running the heavy structuralSuite (already covered on the 6 SEEDS + osm parity).
|
||||
function districtInvariants(plan, label) {
|
||||
const venues = plan.shops.filter(s => s.venue);
|
||||
const byId = new Map(venues.map(v => [v.id, v]));
|
||||
|
||||
@ -199,7 +205,9 @@ function gigSuite(plan, label) {
|
||||
const d = Math.hypot(p.x - nx, p.z - nz);
|
||||
if (d < best) { best = d; bestEdge = e; }
|
||||
}
|
||||
if (bestEdge && best < roadWidth(bestEdge) / 2) { onRoad = [p.id, bestEdge.id, +best.toFixed(2)]; break; }
|
||||
// −1e-6 immunises the exact kerb-line boundary against r2 coord rounding (only reachable on a
|
||||
// no-main-spine fallback where poleOffset == roadWidth/2; never hit by the current sweep).
|
||||
if (bestEdge && best < roadWidth(bestEdge) / 2 - 1e-6) { onRoad = [p.id, bestEdge.id, +best.toFixed(2)]; break; }
|
||||
}
|
||||
ok(onRoad === null, `${label}: no poster stands on a carriageway` + (onRoad ? ` (poster ${onRoad[0]} is ${onRoad[2]}m from edge ${onRoad[1]}'s centreline)` : ''));
|
||||
}
|
||||
@ -294,6 +302,35 @@ const GIG_GOLDEN = 0x1f636349; // ROUND13 re-pin: multi-venue district changed t
|
||||
ok(GIG_GOLDEN !== 0 && hash === (GIG_GOLDEN >>> 0), `gig layer golden 0x${hash.toString(16)} matches pinned 0x${(GIG_GOLDEN >>> 0).toString(16)}`);
|
||||
}
|
||||
|
||||
// ── 3d. district invariant sweep (ROUND14): 400 synthetic seeds + osm graceful placement ───────────
|
||||
// Release-round Lane A gate: the district contract must hold at SCALE, not just the 4 hero seeds. Runs
|
||||
// districtInvariants ONLY (structuralSuite already covers the 6 SEEDS + osm parity) so 400 full gig-on
|
||||
// generations stay well under a second. Pure validation — NO new rng draws, no golden moves.
|
||||
section('district invariant sweep (ROUND14: 400 synthetic seeds + osm graceful placement)');
|
||||
const SWEEP_N = 400;
|
||||
const sweepVenueCounts = new Set();
|
||||
for (let s = 1; s <= SWEEP_N; s++) {
|
||||
const plan = generatePlanFor(s, 'synthetic', { gigs: true });
|
||||
districtInvariants(plan, `sweep syn ${s}`);
|
||||
sweepVenueCounts.add(plan.shops.filter(v => v.venue).length);
|
||||
}
|
||||
// also cover the large/edge uint32 seeds (2e9, 8675309 …) — where rng edge cases hide — that fall
|
||||
// outside 1..400 and otherwise only get the flags-off byte-identity check, not the district contract.
|
||||
for (const s of SEEDS) if (s > SWEEP_N) districtInvariants(generatePlanFor(s, 'synthetic', { gigs: true }), `sweep syn ${s}`);
|
||||
// the seeded 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 ──────────────────
|
||||
section('assets on disk');
|
||||
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
|
||||
// while the gig is on. A quiet-night venue has no opts.gig → normal interior (room-tone + maybe radio).
|
||||
// The genre resolves gig.genreKey → the venue's own shop.genreKey (A sets it) → the kind default. The
|
||||
// manifest key IS the gigKey, canonical form `gig-<genreKey>` — no mapping table (ROUND13 debt #1).
|
||||
// manifest key IS the gigKey, canonical form `gig-<genreKey>` — no mapping table (ROUND13 debt #1). This
|
||||
// string is byte-identical to citygen's `gigKeyFor(genreKey)` (selfcheck asserts it); it is hand-built,
|
||||
// NOT imported, on purpose — the interiors lib stays standalone (never imports citygen). Keep the form,
|
||||
// don't privately rename it (the R12 trap) and don't import citygen (breaks standalone).
|
||||
const gig = opts && opts.gig;
|
||||
if (gig && (gig.on || gig.genreKey)) {
|
||||
const genreKey = gig.genreKey || (shop && shop.genreKey) || GENRE_BY_TYPE[type] || 'pubrock';
|
||||
@ -172,8 +175,8 @@ export function buildInterior(shop, THREE, opts) {
|
||||
places: lay.places,
|
||||
counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose
|
||||
browsePoints: lay.browsePoints, // [ {x,z,ry,atKind,slotIndex} ] 0..3 — Lane D browser rig poses (R9)
|
||||
watchPoints: lay.watchPoints || [], // venue (R12): [ {x,z,ry,dance,slotIndex} ] 6..8 audience poses (else [])
|
||||
stage: lay.stage || null, // venue (R12): { x,z,w,d,deckY,frontZ,bandPoses[3] } (else null)
|
||||
watchPoints: lay.watchPoints || [], // venue: [ {x,z,ry,dance,slotIndex} ] audience poses, cap by kind (pub/band_room 8, rsl 12); else []
|
||||
stage: lay.stage || null, // venue: { x,z,w,d,deckY,riserY,frontZ,bandPoses[4] } (4-piece: trio + drummer on the riser); else null
|
||||
audio: audioFor(recipe.key, ctx, opts, norm.raw), // { musicKey, toneKey, gigKey? } — Lane E manifest.audio keys (R11/R12)
|
||||
dims: { ...dims, archetype, type: recipe.key },
|
||||
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
|
||||
|
||||
@ -432,7 +432,7 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
||||
placed: placed.filter(p => !p.removed),
|
||||
places, decor: decor.meshes, grid, pathOK, carved,
|
||||
spawnCell, targetCell, placement, browsePoints,
|
||||
watchPoints, // venue: 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)
|
||||
counter: {
|
||||
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);
|
||||
if (lot) venues.push({ shop: pub, lot, genreKey: pub.genreKey || 'pubrock' });
|
||||
}
|
||||
// Per-venue gig state for the muffled spill, robust across Lane F's alpha→district transition:
|
||||
// 1. F R13 per-venue map window.PROCITY.gigs.byVenue[id].state
|
||||
// 2. alpha alias window.PROCITY.gigs is the single machine → its state for its venueShopId
|
||||
// 3. clock fallback plan.gigs (night 0) vs the current segment (works before/without F)
|
||||
// Per-venue gig state for the muffled spill (R14: the R12 alpha alias hop is retired):
|
||||
// 1. F's per-venue map window.PROCITY.gigs.byVenue[id] (the canonical surface since R13)
|
||||
// 2. clock fallback plan.gigs (night 0) vs the current segment (works before F wires / headless)
|
||||
// Returns null only when there is no gig layer at all (flags-off ⇒ byte-identical, no spill).
|
||||
function venueGigState(shopId) {
|
||||
const g = PROCITY.gigs;
|
||||
if (g && g.byVenue) { const e = g.byVenue[shopId]; if (e) return typeof e === 'string' ? e : (e.state || 'quiet'); }
|
||||
if (g && g.venueShopId === shopId && typeof g.state === 'string') return g.state;
|
||||
const gigs = PROCITY.plan?.gigs; if (!gigs || !gigs.length) return null;
|
||||
const ton = gigs.find((x) => x.night === 0 && x.venueShopId === shopId);
|
||||
if (!ton) return 'quiet'; // dark tonight — reads true
|
||||
|
||||
@ -137,6 +137,25 @@ export function installDBG(deps) {
|
||||
|| (plan.shops || []).find((s) => s.type === 'pub');
|
||||
return shop ? poseForShop(shop, dist) : null;
|
||||
};
|
||||
// John's release hero: a CLOSE 3/4 on the pub door during doors/on, biased to the venue's +right
|
||||
// (the queue side) so punters' faces read and a verge poster enters frame. The queue + posters are
|
||||
// spawned by the live shell loop when the venue opens (seg 5 = NIGHT / 'on'), so F drives this shot
|
||||
// live; this just frames the door + marquee + queue zone. Geometry mirrors venue.js's frontage.
|
||||
const poseForVenueQueue = () => {
|
||||
const vs = venueShops();
|
||||
const shop = vs.find((s) => (s.venueKind || s.type) === 'pub') || vs[0]
|
||||
|| (plan.shops || []).find((s) => s.type === 'pub');
|
||||
const lot = shop && lotOf(shop); if (!lot) return null;
|
||||
const ry = lot.ry || 0, fx = Math.sin(ry), fz = Math.cos(ry), rx = Math.cos(ry), rz = -Math.sin(ry);
|
||||
const w = lot.w || 8, d = lot.d || 10;
|
||||
const cx = lot.x + fx * (d / 2), cz = lot.z + fz * (d / 2); // door centre on the street (venue.js frontage)
|
||||
const mw = Math.min(w * 0.92, 6.5); // marquee width (queue head ~ +right of it)
|
||||
const DIST = 7.0, SIDE = mw * 0.5 + 2.2, EYE = 2.2; // close standoff, biased to the queue side
|
||||
return {
|
||||
pos: [cx + fx * DIST + rx * SIDE, EYE, cz + fz * DIST + rz * SIDE],
|
||||
look: [cx + rx * (mw * 0.30), 2.7, cz + rz * (mw * 0.30)], // door, lifted to catch the marquee
|
||||
};
|
||||
};
|
||||
|
||||
// Named bookmarks (LANE_F_NOTES §4). seg = day segment index (0 DAWN … 5 NIGHT).
|
||||
// Predicates cover both Lane A's taxonomy (dept/stall/record/…) and the fixture's; each falls back
|
||||
@ -164,6 +183,7 @@ export function installDBG(deps) {
|
||||
// ── v3 gig district (round 13, ?gigs=1): the street money shots F scripts via tools/qa/gig_shot.py ──
|
||||
venue_night: { seg: 5, resolve: () => poseForVenue('pub', 13) || streetViewPose(['main']) }, // ?gigs=1 — lit pub frontage + streetlamp pools + queue + posters at night
|
||||
district_posters: { seg: 5, resolve: () => streetViewPose(['main']) }, // ?gigs=1 — the spine at night: poster poles + lit venue frontages receding
|
||||
queue_night: { seg: 5, resolve: () => poseForVenueQueue() || poseForVenue('pub', 11) || streetViewPose(['main']) }, // ?gigs=1 — John's hero: close on the pub door at 'on', queue + marquee + poster, punters facing the door
|
||||
};
|
||||
|
||||
const DBG = {
|
||||
|
||||
@ -185,13 +185,13 @@ export function createVenuePresentation(plan, skins, scene, { assetBase = 'asset
|
||||
|
||||
scene.add(group);
|
||||
|
||||
// ── per-venue state → frontage glow. Accepts F's per-venue map { [venueShopId]: state }, a Map, an
|
||||
// { byVenue } wrapper, OR a bare state string (alpha compat → the primary pub). Idempotent; the shell
|
||||
// ── per-venue state → frontage glow. F passes its per-venue map `gigState.byVenue`
|
||||
// ({ [venueShopId]: 'quiet'|'doors'|'on'|'done' }); a Map or a { byVenue } wrapper are also accepted.
|
||||
// (R14: the R12 alpha bare-string arm is retired — F wires the map since R13.) Idempotent; the shell
|
||||
// calls it every street frame, so pool/glow ramps and the night baseline lerp here. ────────────────
|
||||
function stateGetter(gigStates) {
|
||||
if (gigStates == null) return () => 'quiet';
|
||||
if (typeof gigStates === 'string') return (id) => (id === primaryId ? gigStates : 'quiet');
|
||||
const map = gigStates.byVenue || gigStates;
|
||||
const map = gigStates.byVenue || gigStates; // F passes gigState.byVenue (per-venue map)
|
||||
return (id) => {
|
||||
const v = map instanceof Map ? map.get(id) : map[id];
|
||||
if (v == null) return 'quiet';
|
||||
@ -225,7 +225,7 @@ export function createVenuePresentation(plan, skins, scene, { assetBase = 'asset
|
||||
if (poolMesh) poolMesh.visible = poolMat.opacity > 0.01;
|
||||
}
|
||||
}
|
||||
update('quiet');
|
||||
update(null); // seed: no gig states yet → every venue quiet (frontage dark, pools follow night)
|
||||
|
||||
function dispose() {
|
||||
for (const d of disposables) d.dispose && d.dispose();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user