- 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>
9.2 KiB
LANE C — venue interfaces (round 13, v3.0-beta: the district) → Lane D + Lane F
PROCITY-C, 2026-07-15. R13 §Lane C. The district ships three venue archetypes behind ?gigs=1 —
pub (R12), and new this round band_room and rsl. A converts a chosen shop to a venue kind in
place (shop.type = kind); C keys the interior recipe off shop.type. All three return the SAME shape
(below), so Lane D drops rigs in and Lane F reads state with no per-kind branching. Non-venue rooms are
byte-identical (venue code only runs for recipe.venue, on its own seed sub-streams).
Verified (fresh context, seeds 20261990/1990/7/42/101 + 90-build venue soak): 0 throws · 0 carves · 0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Draws below.
0. The pinned pose ry convention (ROUND13 debt #4) — READ THIS FIRST
Canonical convention (all of C's poses use it): a pose's ry is the yaw of a thing whose front /
outward normal is local −Z at ry=0, i.e. world-facing (−sin ry, −cos ry). counter.stand,
browsePoints, watchPoints and bandPoses ALL follow it, and the box placeholder (placeholder.js) is
built to it (toes at −Z). This is the long-standing convention keepers.js greet math already assumes.
The GLB fleet mesh is the outlier — it faces local +Z at ry=0. Verified two ways in a fresh context:
- toe-bone probe (
toe − foot, the anatomically-anterior direction) on 5 peds →(0.05..0.14, +0.99)= +Z. - planting a fleet rig at C's
counter.stand.ryviaspawnRig(no flip) and dotting its forward against the intended customer-facing direction → −0.99 (180° backward); atry+π→ +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.jsstays flip-free and is now correct for GLB too (fixes keepers + browsers);band.jsdropsRY_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)
room.stage = {
x, z, // deck centre (against the back wall)
w, d, // deck size (m) — varies by kind (table below)
deckY, // deck top surface height (m) — front line stands here
riserY, // drum riser top height (m) = deckY + 0.16 — the drummer sits here
frontZ, // z of the deck lip (the crowd stands in front of this, +Z)
bandPoses: [ { x, z, ry, role, y, seated? } ×4 ],
}
bandPoses[0..2]= the front line:role∈guitar/vocal/bass, across the deck lip,y = deckY.bandPoses[3]= the drummer:role: 'drums',seated: true, up-stage centre on the drum riser,y = riserY(=deckY + 0.16, thefittings.jsstage 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(wasstage.deckY; front line is unchanged, the drummer now sits 0.16 m higher on the riser).rolehints the instrument — E's GLBs viaopts.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
bandPoseslength 3 (drop[3]), and F amends itsband==smoke back to 3.
room.watchPoints — the audience, cap by kind (empty [] for non-venues)
room.watchPoints = [ { x, z, ry, dance, slotIndex } ] // seeded floor poses, all facing the stage centre
- Crowd cap by kind:
pub8 ·band_room8 ·rsl12 (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). danceseeded ~⅓ true ("a bit of both"). D:dance:false→ stand-and-watch idle;dance:true→ bob/sway with a seeded phase offset (useslotIndex/coords). Every point walkable, reachable, clear of the keeper stand + browse points + stage/PA. Deterministic pershop.seed.
room.counter / keeper — unchanged
The bar is a normal counter; the barkeep is just a keeper at room.counter.stand (R9 seam). (Subject to the
§0 facing fix — the keeper is one of the actors currently backward with the GLB fleet.)
room.audio.gigKey — the live bed (ROUND13 debt #1)
room.audio = { musicKey, toneKey, gigKey? }. The manifest key is the gigKey, canonical form
gig-<genreKey> — no mapping table. F passes gig state via opts.gig:
buildInterior(rslShop, THREE, { gig: { on: true } }) // → room.audio.gigKey === 'gig-covers'
- Genre resolves
opts.gig.genreKey→ the venue's ownshop.genreKey(A sets it) → a kind default (pub→pubrock,band_room→grunge,rsl→covers). Present only when a gig is on. - Prefer
gigKeyovermusicKeywhile the gig plays. Lane E names the bedsgig-pubrock,gig-grunge,gig-covers; a missing key → silence (audio law), band still visible.
The venue table (C-owned tuning, FYI)
| kind | archetype bias | ceiling | deck w × d, deckY |
crowd cap | genre → gigKey | dressing |
|---|---|---|---|---|---|---|
pub |
wide / hall | 3.6 m | 0.58·W × 2.0, 0.32 |
8 | pubrock → gig-pubrock |
pub tables, sticky carpet, bar |
band_room |
cosy / pokey / wide | 3.4 m (low) | 0.62·W × 1.6, 0.20 (barely a riser) |
8 | grunge → gig-grunge |
near-empty standing room, tin-shed dingy |
rsl |
hall / wide | 4.0 m (tall) | 0.50·W × 2.4, 0.40 |
12 | covers → gig-covers |
flock walls, club carpet, bistro tables, members' bar |
2. Quiet-night variant
No opts.gig → a normal interior: stage + watchPoints are still returned as data, but there's no
gigKey (room-tone + maybe the seeded radio musicKey), and D spawns no crowd. A seeded dark night reads
true — the venue is just a quiet pub/club.
3. Budgets (measured, seed 20261990, fresh context)
- Empty shells (
drawSweepglb-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).