PROCITY/docs/LANES/LANE_C_PUB.md
m3ultra 82b711f962 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>
2026-07-15 23:05:03 +10:00

9.2 KiB
Raw Blame History

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=1pub (R12), and new this round band_room and rsl. A converts a chosen shop to a venue kind in place (shop.type = kind); C keys the interior recipe off shop.type. All three return the SAME shape (below), so Lane D drops rigs in and Lane F reads state with no per-kind branching. Non-venue rooms are byte-identical (venue code only runs for recipe.venue, on its own seed sub-streams).

Verified (fresh context, seeds 20261990/1990/7/42/101 + 90-build venue soak): 0 throws · 0 carves · 0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Draws below.


0. The pinned pose ry convention (ROUND13 debt #4) — READ THIS FIRST

Canonical convention (all of C's poses use it): a pose's ry is the yaw of a thing whose front / outward normal is local Z at ry=0, i.e. world-facing (sin ry, cos ry). counter.stand, browsePoints, watchPoints and bandPoses ALL follow it, and the box placeholder (placeholder.js) is built to it (toes at Z). This is the long-standing convention keepers.js greet math already assumes.

The GLB fleet mesh is the outlier — it faces local +Z at ry=0. Verified two ways in a fresh context:

  • toe-bone probe (toe foot, the anatomically-anterior direction) on 5 peds → (0.05..0.14, +0.99) = +Z.
  • planting a fleet rig at C's counter.stand.ry via spawnRig (no flip) and dotting its forward against the intended customer-facing direction → 0.99 (180° backward); at ry+π → +0.99.
  • for contrast the box placeholder's foot mesh sits at z 0.076 → Z (matches the convention).

Finding: with the GLB fleet loaded, keepers AND browsers have been facing 180° backward since the fleet was wired in. keepers.js spawns rigs at stand.ry / browsePoint.ry with no flip, and the mesh front is +Z, so the keeper faces the bar wall and the browser faces the aisle instead of the goods. The R12 band reads correctly only because band.js added RY_FLIP = π. The two actor kinds are opposite (placeholder Z, GLB rig +Z), so the current per-consumer state is inconsistent both ways:

consumer applies placeholder (Z front) GLB rig (+Z front)
keepers.js (keepers, browsers) no flip correct backward
band.js (band, crowd) +RY_FLIP (π) backward correct

Recommended canonical fix (Lane D — visual only, NO golden moves, no C poses change): normalise the fleet at the source. In rigs.js buildFigure, rotate the cloned inner by π about Y so every GLB rig's front becomes Z, matching the placeholder and every C pose. Then:

  • keepers.js stays flip-free and is now correct for GLB too (fixes keepers + browsers);
  • band.js drops RY_FLIP (set to 0) — the poses already face right under the Z convention;
  • placeholder and GLB modes become consistent (no more mode-dependent facing).

One rotation at the source, delete every consumer flip. C's data does not move (pure poses, all Z already), so no golden is touched — it's the visual fix debt #4 anticipated. Repro handed to D in C-progress.md.

(If D prefers to keep the flip per-consumer instead, the equivalent is: ADD RY_FLIP to keepers.js spawns — but that leaves placeholder-mode keepers backward, so the source-normalise is the clean one.)


1. What buildInterior returns for a venue (shop.typepub | 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: roleguitar / vocal / bass, across the deck lip, y = deckY.
  • bandPoses[3] = the drummer: role: 'drums', seated: true, up-stage centre on the drum riser, y = riserY (= deckY + 0.16, the fittings.js stage riser). The riser mesh already exists in the stage fitting — the pose just seats the kit + drummer on it.
  • Lane D: lift each member to its own pose.y (was stage.deckY; front line is unchanged, the drummer now sits 0.16 m higher on the riser). role hints the instrument — E's GLBs via opts.instrumentFor(role) (electric_guitar/bass_guitar/mic_stand/drum_kit), primitives otherwise (asset law). seated:true → seated bob.
  • ry = π faces the audience (+Z) under the pinned Z convention → apply your fleet flip per §0.
  • John may veto to a front-line trio (charter): say so before you start and C ships bandPoses length 3 (drop [3]), and F amends its band== smoke back to 3.

room.watchPoints — the audience, cap by kind (empty [] for non-venues)

room.watchPoints = [ { x, z, ry, dance, slotIndex } ]   // seeded floor poses, all facing the stage centre
  • Crowd cap by kind: pub 8 · band_room 8 · rsl 12 (the RSL is the crowd-cap stress case). The room fills what geometry allows up to the cap; the count IS the crowd cap (F asserts crowd ≤ watchPoints.length).
  • dance seeded ~⅓ true ("a bit of both"). D: dance:false → stand-and-watch idle; dance:true → bob/sway with a seeded phase offset (use slotIndex/coords). Every point walkable, reachable, clear of the keeper stand + browse points + stage/PA. Deterministic per shop.seed.

room.counter / keeper — unchanged

The bar is a normal counter; the barkeep is just a keeper at room.counter.stand (R9 seam). (Subject to the §0 facing fix — the keeper is one of the actors currently backward with the GLB fleet.)

room.audio.gigKey — the live bed (ROUND13 debt #1)

room.audio = { musicKey, toneKey, gigKey? }. The manifest key is the gigKey, canonical form gig-<genreKey> — no mapping table. F passes gig state via opts.gig:

buildInterior(rslShop, THREE, { gig: { on: true } })   // → room.audio.gigKey === 'gig-covers'
  • Genre resolves opts.gig.genreKey → the venue's own shop.genreKey (A sets it) → a kind default (pub→pubrock, band_room→grunge, rsl→covers). Present only when a gig is on.
  • Prefer gigKey over musicKey while the gig plays. Lane E names the beds gig-pubrock, gig-grunge, gig-covers; a missing key → silence (audio law), band still visible.

The venue table (C-owned tuning, FYI)

kind archetype bias ceiling deck w × d, deckY crowd cap genre → gigKey dressing
pub wide / hall 3.6 m 0.58·W × 2.0, 0.32 8 pubrock → gig-pubrock pub tables, sticky carpet, bar
band_room cosy / pokey / wide 3.4 m (low) 0.62·W × 1.6, 0.20 (barely a riser) 8 grunge → gig-grunge near-empty standing room, tin-shed dingy
rsl hall / wide 4.0 m (tall) 0.50·W × 2.4, 0.40 12 covers → gig-covers flock walls, club carpet, bistro tables, members' bar

2. Quiet-night variant

No opts.gig → a normal interior: stage + watchPoints are still returned as data, but there's no gigKey (room-tone + maybe the seeded radio musicKey), and D spawns no crowd. A seeded dark night reads true — the venue is just a quiet pub/club.

3. Budgets (measured, seed 20261990, fresh context)

  • Empty shells (drawSweep glb-off): band_room 124 · pub 126 · rsl 135 — town-wide worst 161 (opshop/hall). ≤ 350. ✓
  • Gig-night incl. FULL crowd + 4-piece (worst-case framing from the door): RSL 237 draws asset-free (16 rigs: 12 crowd + 4 band) / 77 GLB, 40.9k tris. pub 176/56. band_room 173/53. ≤ 350 draws / 200k tris. ✓ (Measured with all 12 RSL watchPoints filled — band.js still caps crowd at 8 in R12; raising it to the watchPoint cap is D's R13 item, so this is the forward-looking worst case.)
  • 90-build venue soak (pub/band_room/rsl mixed): 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo 0 / tex 0.

4. The archetypes (C-owned, FYI)

band_room = a tin shed: small (cosy/pokey), low ceiling, barely-a-riser deck, near-empty standing floor, bar in the corner — the PA is louder than the room. rsl = carpet-and-flock club: the biggest floor in town (hall/wide), a proper raised stage at one end, bistro tables round the edge, members' bar, the biggest crowd cap. Both reuse the pub venue machinery (theme.js venueSpec + layout.js placeStage/placeWatchPoints); only the dressing + venueSpec differ. Stage/PA/amp keep their GLB slots for Lane E (primitive fallback holds).