Lane C R13 (v3.0-beta, the district): band_room + rsl archetypes, 4-piece band, RY audit (?gigs=1)

- 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>
This commit is contained in:
m3ultra 2026-07-15 23:05:03 +10:00
parent bf98b41b16
commit 82b711f962
6 changed files with 275 additions and 58 deletions

View File

@ -3,7 +3,78 @@
*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 12) · owner: PROCITY-C · reviewer: Fable
Last updated: 2026-07-15 (round 13) · owner: PROCITY-C · reviewer: Fable
---
## Update 2026-07-15 (round 13, v3.0-beta: the district) — two more rooms + the drummer
R13 §Lane C. The district: two new venue archetypes + the 4-piece band + the RY-facing audit. Interfaces
(re)published in [docs/LANES/LANE_C_PUB.md](docs/LANES/LANE_C_PUB.md) — **read §0 first (the pinned ry
convention + the keeper/browser back-to-front finding for Lane D).** Committed C-only by pathspec (the tree
had E/B/D changes landing concurrently — [[procity-shared-tree-hazard]]).
**Shipped (C-owned files only): `theme.js`, `layout.js`, `interiors.js`.**
- **`band_room` + `rsl` recipes** (`theme.js`, both `venue:true`), reusing the pub venue machinery with a new
`venueSpec` field. `band_room` = small/scuzzy tin shed (cosy/pokey bias, low 3.4 m ceiling, deck barely a
riser `deckY 0.2`, near-empty standing floor). `rsl` = carpet-and-flock club (hall/wide bias, tall 4.0 m,
proper stage `deckY 0.4`, bistro tables, the biggest crowd cap). A converts a shop with `shop.type = kind`;
C keys the recipe off it. `SHOP_TYPES` now auto-enrols both in `drawSweep`/`soak`.
- **4-piece band (debt #3):** `placeStage` grows `stage.bandPoses` to **4** — front-line trio
(guitar/vocal/bass, `y=deckY`) + a **drummer** (`role:'drums'`, `seated:true`, up-stage on the drum riser,
`y=riserY=deckY+0.16`). Each pose now carries its own `y`; `stage.riserY` added. Applies to all three
archetypes (the pub gains a drummer too). Pure geometry — no new rng draw, determinism intact.
- **Per-kind venue tuning:** `placeStage`/`placeWatchPoints` read `recipe.venueSpec` → deck size/height +
**crowd cap** (pub/band_room **8**, rsl **12** — the crowd-cap stress case). Pub values == the R12
constants, so the pub stage geometry is byte-identical (bar the new drummer).
- **Robust gigKey (debt #1):** `audio.gigKey = 'gig-'+genreKey`, genre resolving `opts.gig.genreKey`
`shop.genreKey` (A sets it) → a kind default. Matches E's renamed manifest beds `gig-pubrock`/`gig-grunge`/
`gig-covers` exactly (verified: manifest has all three, zero stale `pubrock-live`). One key, no mapping.
**RY-facing audit (debt #4) — see LANE_C_PUB.md §0 for the full write-up + fix. Headline:**
- Canonical convention pinned: **rig-front = local Z at ry=0** (every C pose + the box placeholder use it).
- **The GLB fleet mesh faces +Z (opposite).** Verified in a fresh context two ways: toe-bone probe
(`toefoot = (0,+0.99)` on 5 peds) and dotting a fleet rig planted at `counter.stand.ry` against the
intended customer-facing (`0.99` = 180° backward; `+π``+0.99`).
- **Finding: keepers + browsers have been facing 180° backward whenever the GLB fleet is loaded**
`keepers.js` spawns at `pose.ry` with no flip, and the mesh front is +Z. The R12 band reads right only
via `band.js RY_FLIP=π`. Placeholder (Z) and GLB (+Z) are opposite, so it's inconsistent both ways.
- **Recommended fix for Lane D (visual-only, no golden moves):** normalise at the source — rotate the clone
`inner` by π in `rigs.js buildFigure` so every GLB rig faces Z; then `keepers.js` stays flip-free (now
correct) and `band.js` DROPS `RY_FLIP`. Repro below.
**→ Lane F (flags_check.py): two smokes are RED by design — bump `band == 3``== 4`.** My 4th bandPose
makes the band a 4-piece (D's `band.js` already iterates `bandPoses`, so it spawns 4 today), which trips F's
R12 assertions verbatim as the round-13 instructions anticipated ("F's band==3 smoke moves to 4"):
- `tools/flags_check.py:674``if night['band'] == 3``== 4`.
- `tools/flags_check.py:794``if na['inRoom'] and na['band'] == 3 and na['crowd'] > 0``== 4`. *(The
noassets gig still happens — band 4, crowd 8, 0 fetches, 0 console errors; silent-and-happy holds. It only
"fails" because 3 is hardcoded.)* These are the ONLY qa reds from C; the other 5 gates pass.
**→ Lane D:** lift each band member to `pose.y` (front line `deckY` unchanged; drummer sits at `riserY`);
`seated:true` on the drums pose → seated bob; raise `band.js CROWD_CAP` to the watchPoint cap so the RSL's 12
fill (I measured the full 12). And apply the §0 facing fix. Repro for the audit (paste in the interior test page):
```js
const rigs = await import('./js/citizens/rigs.js'); const THREE = await import('three');
const fleet = rigs.loadPedFleet('models/peds/'); await fleet.whenReady;
const sp = rigs.spawnRig(fleet.all[2], { ry: 0, height: 1.75 }); sp.fig.updateWorldMatrix(true, true);
let foot, toe, wp = new THREE.Vector3();
sp.inner.traverse(o => { if (o.isBone){ o.getWorldPosition(wp);
if (/LeftFoot$/i.test(o.name)) foot = wp.clone(); if (/LeftToeBase$/i.test(o.name)) toe = wp.clone(); }});
console.log('fleet forward at ry=0:', toe.clone().sub(foot).setY(0).normalize()); // → (0, +1) = +Z
```
**Budgets (measured, fresh context, seed 20261990):**
- Empty shells (`drawSweep` glb-off): band_room 124 · pub 126 · rsl 135 (worst town-wide 161 opshop/hall) ≤350.
- **Gig-night incl. full crowd + 4-piece:** worst **RSL 237 draws** asset-free (16 rigs) / 77 GLB, 40.9k tris.
pub 176/56, band_room 173/53. ≤350 draws / 200k tris. ✓
- 90-build venue soak: 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo0/tex0. `soak(60)` in
the page: PASS 60 rooms, leak 0/0, determinism identical, worst draws 85. qa figure-scale gate: 24 figs
human-sized. Shot: [docs/shots/laneC/rsl_gig_r13.jpg](docs/shots/laneC/rsl_gig_r13.jpg) — RSL, 4-piece
facing the crowd, crowd facing the stage (facing reads correct via band.js's flip).
**Drummer veto (John):** default is Fable's 4-piece ruling (executed). To revert to a front-line trio, drop
`bandPoses[3]` in `layout.js placeStage` (1 line) + F reverts the smoke to `== 3` — say so before D wires it.
---

View File

@ -1,55 +1,132 @@
# LANE C — pub venue interface (round 12, v3.0-alpha) → for Lane D + Lane F
# LANE C — venue interfaces (round 13, v3.0-beta: the district) → Lane D + Lane F
*PROCITY-C, 2026-07-15. R12 §Lane C. The band room ships behind `?gigs=1` (A only emits `pub` shops
when the flag is on). Interface published here + in this commit's code; non-pub rooms are byte-identical
(the venue code only runs for `recipe.venue`, on its own seed sub-streams). Verified: 40-seed pub sweep +
60-room mixed soak — 0 throws · 0 carves · 0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst 19 ms.*
*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).*
## What `buildInterior` returns for a pub (type `'pub'`)
Everything below is **pure data on the return** (room-local coords, `counter.stand` ry convention:
rig-front = local Z), so Lane D drops rigs straight in and Lane F reads state — no new lifecycle.
*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.*
### `room.stage` — the band stage (null for non-venues)
---
## 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)
deckY, // top surface height (m) — band rigs stand at y = deckY
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 } ×3 ], // guitar / vocal / bass, across the deck front, facing the crowd (ry=π → +Z)
bandPoses: [ { x, z, ry, role, y, seated? } ×4 ],
}
```
**Lane D:** put the 3-piece band at `bandPoses` (lift to `y = deckY`); `role` hints instrument. Drummer
can sit up-stage on the riser (there's a raised riser at deck-centre-back) — your call, C just gives 3 front poses.
Instruments = E's GLBs when present, primitives otherwise (asset law).
- `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 (empty [] for non-venues)
### `room.watchPoints` — the audience, **cap by kind** (empty `[]` for non-venues)
```js
room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // 6..8 seeded floor poses, all facing the stage centre
room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // seeded floor poses, all facing the stage centre
```
- `dance` is seeded **~⅓ true** ("a bit of both"). **Lane D:** `dance:false` → stand-and-watch idle
(subtle weight-shift); `dance:true` → bob/sway/step loop with a **seeded phase offset** so the crowd
doesn't metronome (use `slotIndex` or the pose coords to derive the phase).
- Every point is walkable, reachable from the door, and clear of the keeper stand, browse points, and the
stage/PA footprint. Deterministic per shop.seed. Crowd cap = `watchPoints.length` (F's smoke asserts ≤).
- **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` (existing R9 seam).
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
`room.audio = { musicKey, toneKey, gigKey? }`. **Lane F** passes gig state via `opts.gig` to `buildInterior`:
### `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(pubShop, THREE, { gig: { on: true, genreKey: 'pubrock' } })
// → room.audio.gigKey === 'gig-pubrock' (gigKey = 'gig-' + genreKey)
buildInterior(rslShop, THREE, { gig: { on: true } }) // → room.audio.gigKey === 'gig-covers'
```
- `gigKey` is present **only when a gig is on**. A quiet-night pub has no `opts.gig` → normal interior
(`toneKey` room-tone + maybe the seeded radio `musicKey`).
- **Prefer `gigKey` over `musicKey`** while the gig plays: `play(room.audio.gigKey || room.audio.musicKey)`.
- **Lane E:** please name the pub-rock live bed **`gig-pubrock`** in `manifest.audio` (music or a `gig`
section) so the key resolves; missing key → silence (audio law), band still visible.
- 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 archetype (C-owned, FYI)
`pub` recipe (`theme.js`, `venue:true`): stage + PA + amps at the back wall, bar counter (`corner`),
sticky-carpet floor, pub tables + bar-back shelf against the walls, the blocked back doorway doubles as
the **green room**. Primitive stage/PA/amp fittings today with **GLB slots for Lane E**: `stage`,
`pa_speaker`, `amp` (house GLB law; primitive fallback holds — a gig with no GLBs is still a gig).
### 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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

View File

@ -22,9 +22,13 @@
// audio: { musicKey, toneKey, gigKey? }, // R11/R12 — keys into Lane E manifest.audio (music bed +
// // room-tone), seeded per shop. musicKey may be null. gigKey set
// // only when opts.gig is on (venue) — F prefers gigKey over musicKey.
// watchPoints: [ {x,z,ry,dance} ], // VENUE (R12, pub only, else []) — 6..8 audience poses facing the
// // stage; dance ~1/3 true. Same frame + ry convention as counter.stand.
// stage: { x,z,w,d,deckY,frontZ,bandPoses }, // VENUE (R12, else null) — the band stage; bandPoses[3] on the deck.
// watchPoints: [ {x,z,ry,dance,slotIndex} ], // VENUE (R12/13, else []) — audience poses facing the stage;
// // dance ~1/3 true. Cap by kind: pub/band_room 8, rsl 12. Same
// // frame + ry convention as counter.stand (rig-front = local Z).
// stage: { x,z,w,d,deckY,riserY,frontZ,bandPoses }, // VENUE (R12/13, else null) — the band stage. bandPoses[4]
// // (R13): front-line trio (guitar/vocal/bass) on the deck + a drummer
// // (role 'drums', seated) up-stage on the riser. Each pose carries y
// // (front line = deckY, drummer = riserY). ry=π faces the audience.
// dims: { W, D, H, archetype, type }, // room metrics
// placement, // deterministic placement summary (deep-equal per seed)
// pathOK, // door→counter connectivity held (always true; carved if needed)
@ -91,8 +95,12 @@ const MUSIC_BY_TYPE = { // manifest.audio.music dedi
milkbar: 'milkbar', video: 'video-synth',
dept: 'arcade', arcade: 'arcade',
}; // default → null (no dedicated bed)
// The genre a venue kind plays — mirrors registry.SHOP_TYPES[kind].genre so C resolves the gigKey even
// standalone (the gig plan carries shop.genreKey; this is the fallback when a test builds a bare venue
// without A's conversion). ROUND13 §Lane C: pub → pubrock, band_room → grunge, rsl → covers.
const GENRE_BY_TYPE = { pub: 'pubrock', band_room: 'grunge', rsl: 'covers' };
function audioFor(type, ctx, opts) {
function audioFor(type, ctx, opts, shop) {
const toneKey = ROOMTONE_BY_TYPE[type] || 'roomtone-retail';
let musicKey = MUSIC_BY_TYPE[type] || null;
// Shops with no dedicated bed: a seeded ~1-in-3 keeps a general radio on (the milk-bar bed carries the
@ -100,10 +108,15 @@ function audioFor(type, ctx, opts) {
// revisit and adding it never shifts another subsystem's seeded picks.
if (!musicKey && ctx.stream('audio')() < 0.34) musicKey = 'milkbar';
const audio = { musicKey, toneKey };
// Venue gig (R12): Lane F passes the live gig state via opts.gig; F should prefer gigKey over musicKey
// while the gig is on. A quiet-night pub has no opts.gig → normal interior (room-tone + maybe radio).
// 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).
const gig = opts && opts.gig;
if (gig && (gig.on || gig.genreKey)) audio.gigKey = 'gig-' + (gig.genreKey || 'pubrock');
if (gig && (gig.on || gig.genreKey)) {
const genreKey = gig.genreKey || (shop && shop.genreKey) || GENRE_BY_TYPE[type] || 'pubrock';
audio.gigKey = 'gig-' + genreKey;
}
return audio;
}
@ -161,7 +174,7 @@ export function buildInterior(shop, THREE, opts) {
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)
audio: audioFor(recipe.key, ctx, opts), // { musicKey, toneKey, gigKey? } — Lane E manifest.audio keys (R11/R12)
audio: audioFor(recipe.key, ctx, opts, norm.raw), // { musicKey, toneKey, gigKey? } — Lane E manifest.audio keys (R11/R12)
dims: { ...dims, archetype, type: recipe.key },
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
placement: lay.placement,

View File

@ -213,10 +213,11 @@ function placeBrowsePoints(grid, placed, counterStand, spawnCell, r) {
// Places a low stage centred on the back wall, PA cabinets flanking it on the floor, and 12 amps on the
// deck. Stage + PA go into `placed` at priority 8 so the door→counter path loop never pulls them. Returns
// the stage frame the crowd faces + 3 seeded band poses on the deck (Lane D drops the band trio in).
function placeStage(ctx, grid, W, D, roomGroup, r) {
const sw = Math.min(W - 1.4, Math.max(3.0, W * 0.58)), sd = 2.0;
function placeStage(ctx, grid, W, D, roomGroup, r, spec = {}) {
const wf = spec.deckWidthFrac || 0.58, deckH = spec.deckH || 0.32; // per-kind (round 13): band_room
const sw = Math.min(W - 1.4, Math.max(3.0, W * wf)), sd = spec.deckDepth || 2.0; // barely a riser, rsl bigger
const cx = 0, cz = -D / 2 + sd / 2 + 0.2; // back wall, centred
const f = buildFitting('stage', ctx, { w: sw, d: sd }, r);
const f = buildFitting('stage', ctx, { w: sw, d: sd, h: deckH }, r);
f.group.position.set(cx, 0, cz); roomGroup.add(f.group);
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, 0);
const rect = rectCells(grid, cx, cz, hw, hd); stampRect(grid, rect);
@ -233,23 +234,32 @@ function placeStage(ctx, grid, W, D, roomGroup, r) {
out.push({ fitting: pf, x: px, z: pz, ry: 0, hw: phw, hd: phd, rect: prect, priority: 8, kind: 'prop' });
}
const deckY = f.deckY || 0.32; // 12 amps on the deck behind the band
const deckY = f.deckY || deckH; // 12 amps on the deck behind the band
for (let i = 0, n = 1 + (r() < 0.5 ? 1 : 0); i < n; i++) {
const af = buildFitting('ampStack', ctx, {}, r);
af.group.position.set(cx + (i === 0 ? -1 : 1) * sw * 0.28, deckY, cz - sd * 0.18);
roomGroup.add(af.group);
}
const bandPoses = [-sw * 0.28, 0, sw * 0.28].map((bx, i) => ({ // 3 across the deck front, facing +Z (audience)
x: round(cx + bx), z: round(cz + sd * 0.12), ry: round(Math.PI), role: ['guitar', 'vocal', 'bass'][i],
// 4-PIECE BAND (round 13, debt #3): front-line trio across the deck lip + a drummer up-stage on the
// riser. ry=π faces the audience (+Z) under the rig-front=Z house convention (see LANE_C_PUB.md). Each
// pose carries its own stand height `y` — the front line on the deck (deckY), the drummer on the drum
// riser (deckY+0.16, the fittings.js stage riser). D lifts each member to pose.y (was: stage.deckY).
const riserY = round(deckY + 0.16); // drum riser top surface (fittings.js: 0.16 m box)
const front = [-sw * 0.28, 0, sw * 0.28].map((bx, i) => ({
x: round(cx + bx), z: round(cz + sd * 0.12), ry: round(Math.PI), role: ['guitar', 'vocal', 'bass'][i], y: round(deckY),
}));
return { placed: out, stage: { x: cx, z: cz, w: sw, d: sd, deckY, frontZ: round(cz + sd / 2), bandPoses } };
const drums = { x: round(cx), z: round(cz - sd / 4), ry: round(Math.PI), role: 'drums', y: riserY, seated: true };
const bandPoses = [...front, drums];
return { placed: out, stage: { x: cx, z: cz, w: sw, d: sd, deckY, riserY, frontZ: round(cz + sd / 2), bandPoses } };
}
// ── WATCH POINTS (venue, round 12) — 68 seeded audience floor poses facing the stage ──
// ── WATCH POINTS (venue) — seeded audience floor poses facing the stage ──
// Same pure-data pattern as browse points: walkable free cells, reachable from spawn, clear of the keeper
// stand + browse points + stage, spaced for a crowd. `dance` seeded ~1/3 true (John: "a bit of both").
function placeWatchPoints(grid, stage, spawnCell, counterStand, browsePoints, r) {
// `watchMax` = the crowd cap per kind (round 13): pub/band_room 8, RSL 12 (the crowd-cap stress case).
// The room fills what geometry allows up to the cap; the count IS the crowd cap (F asserts crowd ≤ it).
function placeWatchPoints(grid, stage, spawnCell, counterStand, browsePoints, r, watchMax = 8) {
let start = spawnCell;
if (grid.occ[start] !== 0) { const s = nearestFree(grid, start); if (s < 0) return []; start = s; }
const reach = floodFill(grid, start);
@ -270,7 +280,7 @@ function placeWatchPoints(grid, stage, spawnCell, counterStand, browsePoints, r)
return true;
};
for (const c of cands) {
if (pts.length >= 8) break;
if (pts.length >= watchMax) break;
if (!valid(c.x, c.z)) continue;
const dx = stage.x - c.x, dz = stage.z - c.z; // face the stage centre (counter.stand ry convention)
pts.push({ x: round(c.x), z: round(c.z), ry: round(Math.atan2(-dx, -dz)), dance: r() < 0.34, slotIndex: pts.length });
@ -341,7 +351,7 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
// for recipe.venue; other rooms never touch the 'stage'/'watch' streams → flags-off byte-identical.
let stageInfo = null, watchPoints = [];
if (recipe.venue) {
const st = placeStage(ctx, grid, W, D, roomGroup, ctx.stream('stage'));
const st = placeStage(ctx, grid, W, D, roomGroup, ctx.stream('stage'), recipe.venueSpec);
stageInfo = st.stage;
for (const sp of st.placed) placed.push(sp);
}
@ -410,7 +420,7 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
// 6b) WATCH POINTS (venue, round 12) — audience floor poses facing the stage, computed after the path
// guarantee + browse points (so they avoid both), on cells reachable from the door.
if (stageInfo) watchPoints = placeWatchPoints(grid, stageInfo, spawnCell, counter.stand, browsePoints, ctx.stream('watch'));
if (stageInfo) watchPoints = placeWatchPoints(grid, stageInfo, spawnCell, counter.stand, browsePoints, ctx.stream('watch'), (recipe.venueSpec && recipe.venueSpec.watchMax) || 8);
// placement summary for the determinism deep-equal test
const placement = placed.filter(p => !p.removed).map(p => ({

View File

@ -213,6 +213,10 @@ const RECIPES = {
clutter: 0.45, // keep the floor open — the crowd fills it
counterPos: 'corner', // the bar, along the east wall (keeper = barkeep)
venue: true, // → stage + PA + watchPoints (layout.js)
// venueSpec (round 13): per-kind stage/crowd tuning read by layout.js placeStage/placeWatchPoints.
// watchMax = crowd cap (== max watchPoints); deckH = deck height; deckDepth/deckWidthFrac size the
// deck. pub values == the R12 constants, so the pub stage is byte-identical (bar the new drums pose).
venueSpec: { watchMax: 8, deckH: 0.32, deckDepth: 2.0, deckWidthFrac: 0.58 },
fittings: [
{ kind: 'trestleTable', zone: 'wall', min: 1, max: 3, priority: 2 }, // pub tables against the walls
{ kind: 'metalShelf', zone: 'wall', min: 0, max: 1, priority: 2 }, // bar back-shelf (bottles)
@ -221,6 +225,48 @@ const RECIPES = {
stockKind: 'snacks',
signFlavour: ['LIVE MUSIC FRI', 'HAPPY HOUR 56', 'NO THONGS AFTER 7', 'COLD BEER ON TAP', 'BANDS OUT BACK'],
},
// ── BAND ROOM (venue, v3.0-beta · round 13, behind ?gigs=1) ──────────────────────────
// A tin shed on the warehouse fringe: small, scuzzy, low ceiling, the PA louder than the room and the
// stage barely a riser. No tables — you stand and you sweat. `genre: 'grunge'` ⇒ gigKey 'gig-grunge'.
// Reuses the pub venue machinery; only the dressing + venueSpec differ.
band_room: {
label: 'Band Room',
archetypeBias: [['cosy', 3], ['pokey', 2], ['wide', 1]], // small rooms; low ceilings come from storeys=1
wallpaperBias: ['woodchip-white', 'stripe-sage', 'damask-mauve'], // dim + dingy
floorBias: ['lino-cork', 'carpet-swirl', 'boards-polished'], // bare/sticky
clutter: 0.35, // near-empty standing room — the crowd IS the furniture
counterPos: 'corner', // a plank bar in the corner (keeper = barkeep)
venue: true,
venueSpec: { watchMax: 8, deckH: 0.2, deckDepth: 1.6, deckWidthFrac: 0.62 }, // barely a riser, wide + shallow
fittings: [
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf (tinnies)
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
],
stockKind: 'snacks',
signFlavour: ['ALL AGES SUN', 'PAY THE BAND', 'NO STAGE DIVING', 'MERCH OUT BACK', '$5 ON THE DOOR'],
},
// ── RSL CLUB (venue, v3.0-beta · round 13, behind ?gigs=1) ───────────────────────────
// Carpet, flock wallpaper, a bistro round the edge, a proper stage at one end, and the biggest crowd
// cap in town — the round-13 crowd-cap stress case. `genre: 'covers'` ⇒ gigKey 'gig-covers'.
rsl: {
label: 'RSL Club',
archetypeBias: [['hall', 3], ['wide', 2]], // big floor for the biggest crowd
wallpaperBias: ['damask-mauve', 'floral-gold', 'trellis-blue'], // clubby flock
floorBias: ['carpet-mustard', 'carpet-swirl', 'carpet-greygreen'], // patterned club carpet
clutter: 0.5, // bistro tables round the edges, floor kept open for the crowd
counterPos: 'corner', // the members' bar
venue: true,
venueSpec: { watchMax: 12, deckH: 0.4, deckDepth: 2.4, deckWidthFrac: 0.5 }, // a raised stage at one end
fittings: [
{ kind: 'trestleTable', zone: 'wall', min: 2, max: 4, priority: 2 }, // bistro tables round the edge
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
],
stockKind: 'snacks',
signFlavour: ['MEMBERS & GUESTS', 'BISTRO OPEN', 'BINGO THURS', 'MEAT RAFFLE 7PM', 'LEST WE FORGET'],
},
};
// Aliases: accept thriftgod / Overpass-style type names and map to canonical recipes.