D moved the backline plant (66aec0f) to C's §3 corrected pose exactly
(band.js:236 = stage.x + stage.w*0.36, deckY, stage.z - stage.d*0.22).
Verified fresh (port-isolated context, seed 20261990, D's real GigCrew + GLB backline):
- matches §3 both venues; bass clearance 0.87 m (pub) / 0.92 m (rsl) — the R14
interpenetration (0.29 m) is resolved; on-deck; flanks C's primitive ampStack
(4.4 m pub / 3.5 m rsl) as a clean up-stage backline; no band/crowd clash.
- rendered pub gig confirms the read (amp up-stage behind the bassist).
LANE_C_PUB §3 updated RESOLVED; freeze otherwise stands. No code change (docs only).
Nothing else blocks on C — F runs the release (continuity wiring, alias delete, week
soak, tour, tag v3.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
9.5 KiB
Markdown
133 lines
9.5 KiB
Markdown
# 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).
|
||
|
||
*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. `guitar_amp` placement (v3.0 — RESOLVED R15)
|
||
|
||
**Resolved.** D plants E's `guitar_amp` GLB (`band.js _addBackline`) up-stage-right at **`(stage.x +
|
||
stage.w·0.36, deckY, stage.z − stage.d·0.22)`** — C's corrected pose (ledger #4). It's on-deck, up-stage of
|
||
the front line, and flanks C's own primitive ampStack (stage-left) as a clean backline. **Verified R15 (fresh
|
||
context, seed 20261990):** clearance to the bass rig **0.87 m (pub) / 0.92 m (rsl)** — no interpenetration
|
||
(the original R14 pose `stage.z + 0.15` was down-stage, 0.29 m off the bass, and clipped it); amp↔primitive
|
||
4.4 m (pub) / 3.5 m (rsl); on-deck both. C's `placeStage` plants **1–2 primitive `ampStack` fittings** at
|
||
`(±stage.w·0.28, deckY, stage.z − stage.d·0.18)` as the asset-free backline fallback (no GLB slot, so they
|
||
stay primitive). *v3.1:* full unification — C exposes a `stage.backline[]` slot list that D's GLB fills,
|
||
replacing the primitive — stays deferred (no new contract fields in v3.0).
|
||
|
||
## 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).
|