- theme.js: band_room (small/scuzzy tin shed) + rsl (carpet-and-flock club) venue recipes, reusing the pub machinery via a new per-kind venueSpec (deck size/height + crowd cap). - layout.js: parametrized placeStage/placeWatchPoints; 4th `drums` pose seated on the riser (debt #3 — the band is now a 4-piece across all venues), stage.riserY + per-pose y. - interiors.js: robust gig-<genreKey> resolution (gig.genreKey -> shop.genreKey -> kind default) matching E's renamed manifest beds (debt #1); API doc for the 4-piece + riserY. - LANE_C_PUB.md: 3-kind venue contract + pinned ry convention and the empirically-verified keeper/browser back-to-front finding + source-normalise fix for Lane D (debt #4). Verified fresh (seed 20261990 + 90-build soak): watch caps 8/8/12, gigKeys gig-pubrock/gig-grunge/gig-covers, determinism 0, path/carve 0, gig-night worst RSL 237 draws asset-free (16 rigs) / 77 GLB <=350, leak geo0/tex0. Shot: docs/shots/laneC/rsl_gig_r13.jpg. Lane F: bump flags_check band==3 -> ==4 (tools/flags_check.py:674,794) — RED by design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
9.2 KiB
Markdown
133 lines
9.2 KiB
Markdown
# LANE C — venue interfaces (round 13, v3.0-beta: 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).*
|
||
|
||
*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.*
|
||
|
||
---
|
||
|
||
## 0. The pinned pose `ry` convention (ROUND13 debt #4) — READ THIS FIRST
|
||
|
||
**Canonical convention (all of C's poses use 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.
|
||
|
||
**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.)*
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
### `room.stage` — the band stage, now a **4-PIECE** (ROUND13 debt #3, 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
|
||
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). 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.
|
||
|
||
### `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). *(Subject to the
|
||
§0 facing fix — the keeper is one of the actors currently backward with the GLB fleet.)*
|
||
|
||
### `room.audio.gigKey` — the live bed (ROUND13 debt #1)
|
||
`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. **Lane E** names the beds `gig-pubrock`,
|
||
`gig-grunge`, `gig-covers`; a missing key → silence (audio law), band still visible.
|
||
|
||
### The venue table (C-owned tuning, FYI)
|
||
| 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. 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.
|
||
|
||
## 4. The archetypes (C-owned, FYI)
|
||
`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).
|