PROCITY/docs/LANES/LANE_C_PUB.md
m3ultra 42b11e717d Lane C R16 (v3.1): stage.backline[] amp unification, drop primitive ampStacks (ledger #5, C half)
C exposes room.stage.backline[] — 2 seeded up-stage amp slots {x,z,ry,y} (pure data) — and
drops its primitive ampStacks from placeStage. Lane D plants E's guitar_amp GLB at backline[0]
(D's primitive fallback under ?noassets): one unified backline, GLB when present.

- backline[0] = (stage.x + stage.w*0.36, deckY, stage.z - stage.d*0.22) == D's R15-verified amp
  pose exactly, so plumbing D's plant to backline[0] moves nothing on screen. backline[1] = spare
  stage-left mirror. ry=pi (audience-facing), y=deckY.
- Verified fresh (seed 20261990): slots up-stage, on-deck, clear of all 4 bandPoses (nearest-bass
  0.87 pub / 0.70 band_room / 0.92 rsl) and all watchPoints (>=1.95 m); determinism byte-identical;
  0 leftover ampStack groups; drawSweep venues 122/120/131 (was 126/124/135, -~4 draws each) <=350.
- Determinism/golden-safe: dropping the amp loop only drops draws on the 'stage' sub-stream (nothing
  downstream reads it); placement snapshot excludes ampStacks; interiors aren't hashed; ?classic has
  no venues. Disjoint from F's flip. LANE_C_PUB §3 amended (v3.1 marker).

C->D handshake: backline shape live. Verify with D once the GLB is wired at backline[0].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 12:16:38 +10:00

140 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LANE C — venue interface contract (v3.0-FROZEN: the district) → Lane D + Lane F
> **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).
> **v3.1 amendment (2026-07-16):** §3 becomes the `stage.backline[]` contract (ledger #5 — the amp
> unification); C dropped its primitive ampStacks. Everything else in this doc is unchanged and stands.
*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 pose `ry` convention (pinned) — READ THIS FIRST
**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). `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.**
**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 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, 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) — 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 ],
}
```
- `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 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
room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // seeded floor poses, all facing the stage centre
```
- **Crowd cap by kind: `pub` 8 · `band_room` 8 · `rsl` 12** (the RSL is the crowd-cap stress case). The room
fills what geometry allows up to the cap; **the count IS the crowd cap** (F asserts crowd ≤ `watchPoints.length`).
- `dance` seeded **~⅓ true** ("a bit of both"). D: `dance:false` → stand-and-watch idle; `dance:true` → bob/sway
with a **seeded phase offset** (use `slotIndex`/coords). Every point walkable, reachable, clear of the keeper
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). Faces the
customer correctly under §0.
### `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
buildInterior(rslShop, THREE, { gig: { on: true } }) // → room.audio.gigKey === 'gig-covers'
```
- 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. 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)
| 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 |
| `band_room` | cosy / pokey / wide | **3.4 m (low)** | `0.62·W × 1.6`, **0.20** (barely a riser) | 8 | grunge → `gig-grunge` | near-empty standing room, tin-shed dingy |
| `rsl` | hall / wide | **4.0 m (tall)** | `0.50·W × 2.4`, **0.40** | **12** | covers → `gig-covers` | flock walls, club carpet, bistro tables, members' bar |
## 2. Quiet-night variant
No `opts.gig` → a normal interior: `stage` + `watchPoints` are still returned as data, but there's **no
`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. `stage.backline[]` — the amp slots (v3.1, ledger #5)
`room.stage.backline = [ { x, z, ry, y } ×2 ]` — seeded up-stage amp/spare-cab poses, **pure data** (C plants
no amp meshes as of v3.1). This is the unified backline: **Lane D plants E's `guitar_amp` GLB here** (its own
primitive fallback under `?noassets`) — one amp system, GLB when present. C's old primitive ampStacks are
**dropped** from all three venue recipes.
- **slot[0]** = stage-right (the primary amp) at `(stage.x + stage.w·0.36, deckY, stage.z stage.d·0.22)`
identical to D's R15-verified plant, so wiring D's amp to `backline[0]` moves nothing on screen.
**slot[1]** = stage-left spare cab (mirror). `ry = π` faces the audience (+Z, the §0 convention); `y = deckY`.
- **Guarantees** (verified fresh, seed 20261990, pub/band_room/rsl): every slot is **up-stage of the front
line**, flanks the drum riser, **clear of all 4 bandPoses** (nearest-bass clearance **0.87 m pub / 0.70 m
band_room / 0.92 m rsl**) and **all watchPoints** (≥1.95 m), and **on-deck** (within the deck footprint).
Deterministic per `shop.seed`. **Lane D:** verify no interpenetration once wired (C's R15 clearance discipline).
- **Draw impact:** dropping C's primitives shaved ~4 draws off each empty venue shell (drawSweep glb-off:
pub 122 · band_room 120 · rsl 131 ≤350); D's GLB/primitive at `backline[0]` adds it back on the gig night.
- *v3.2 (parked):* the drummer sit clip, instrument LOD, and C's `audioEmitter` idea.
## 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 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).