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:
parent
bf98b41b16
commit
82b711f962
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
BIN
docs/shots/laneC/rsl_gig_r13.jpg
Normal file
BIN
docs/shots/laneC/rsl_gig_r13.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 602 KiB |
@ -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,
|
||||
|
||||
@ -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 => ({
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user