diff --git a/C-progress.md b/C-progress.md index e482fcc..f82c943 100644 --- a/C-progress.md +++ b/C-progress.md @@ -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 + (`toe−foot = (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. --- diff --git a/docs/LANES/LANE_C_PUB.md b/docs/LANES/LANE_C_PUB.md index fc40787..e62c959 100644 --- a/docs/LANES/LANE_C_PUB.md +++ b/docs/LANES/LANE_C_PUB.md @@ -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-`** — 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). diff --git a/docs/shots/laneC/rsl_gig_r13.jpg b/docs/shots/laneC/rsl_gig_r13.jpg new file mode 100644 index 0000000..df982dc Binary files /dev/null and b/docs/shots/laneC/rsl_gig_r13.jpg differ diff --git a/web/js/interiors/interiors.js b/web/js/interiors/interiors.js index bbcdad3..76dbd0e 100644 --- a/web/js/interiors/interiors.js +++ b/web/js/interiors/interiors.js @@ -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-` — 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, diff --git a/web/js/interiors/layout.js b/web/js/interiors/layout.js index 5eeb048..3d8f5a2 100644 --- a/web/js/interiors/layout.js +++ b/web/js/interiors/layout.js @@ -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 1–2 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; // 1–2 amps on the deck behind the band + const deckY = f.deckY || deckH; // 1–2 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) — 6–8 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 => ({ diff --git a/web/js/interiors/theme.js b/web/js/interiors/theme.js index f30cc79..52df37e 100644 --- a/web/js/interiors/theme.js +++ b/web/js/interiors/theme.js @@ -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 5–6', '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.