Light release round — C interior code unchanged since R13 (venue geometry stable by construction); this is the audit, the amp ruling, and the v3.0-FROZEN docs. - Re-verified vs the current tree (D's continuity + RY-fix + real instrument GLBs landed): contract byte-identical (caps 8/8/12, deckY/riserY, gigKeys, determinism), drawSweep <=350 (worst 161), gig-night draws pub 60 / band_room 52 / rsl 71 <=350. Release-law confirmed: D's rng namespace is disjoint from C's ctx.stream; no golden can move. - guitar_amp ruling (LANE_C_PUB §3): NOT blessed as-is. D's pose (stage.z+0.15, down-stage) interpenetrates the bass rig by 0.29 m; move it up-stage-right ~ (stage.w*0.36, deckY, stage.z - stage.d*0.22), clear of the bass + C's primitive ampStacks. Full amp unification = v3.1. Cosmetic + ?gigs opt-in -> not a tag blocker. C->D handshake. - Docs freeze: LANE_C_PUB.md + LANE_C_AUDIO.md marked v3.0-FROZEN; RY §0 reframed past-tense (the +Z fleet was fixed at source in D's buildFigure, R13 - no per-consumer flips); budgets updated to R14; in-flight handoffs removed. - Comment-only code (zero runtime/golden/rng change): fixed stale R12 inline return-shape comments (watchPoints 6..8 -> 8/8/12; stage bandPoses[3] -> [4] + riserY) in interiors.js + layout.js; documented the standalone gigKey hand-build. theme.js untouched. Note for E/F: settled gig-night interior is ~182k tris (E instruments 14k each x5); F's smoke reports ~34k measuring before the async GLBs load. Interior draw gate passes; v3.1 LOD candidate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.8 KiB
LANE C — venue interface contract (v3.0-FROZEN: the district) → Lane D + Lane F
v3.0-FROZEN · 2026-07-16 · durable contract for Lane D + Lane F. Describes the shipped state; changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (release audit).
The district ships three venue archetypes behind ?gigs=1 — pub, band_room, rsl. Lane 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 runs only for recipe.venue, on its
own seed sub-streams). Verified fresh (seeds 20261990/1990/7/42/101 + 90-build soak): 0 throws · 0 carves ·
0 path-fails · 0 determinism-fails · leak geo0/tex0 · worst build 8.3 ms. Budgets in §3.
0. The pose ry convention (pinned) — READ THIS FIRST
Canonical convention (every C pose uses 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). keepers.js greet math assumes it. Drop a rig in at a pose's ry and it faces
the right way — no per-consumer flip.
Why no flip (the R13 fix, for the record): the GLB fleet meshes ship facing local +Z at ry=0 — the
opposite of the −Z convention (verified: the anterior toe-bone vector toe − foot reads (≈0, +0.99)).
Before R13 this was compensated per-consumer, inconsistently: band.js flipped by π (band/crowd correct,
placeholders backward) while keepers.js did not (placeholders correct, keepers + browsers 180° backward
whenever the GLB fleet was loaded — they'd been shipping back-to-front). D fixed it at the source in
rigs.js buildFigure: the cloned inner is rotated π about Y so every GLB rig faces −Z, matching the
placeholder and every C pose. All consumer flips were then deleted. Net: keepers, browsers, band and crowd
all face correctly, GLB and placeholder modes are consistent, and C's pure-data poses never moved (no golden
touched). If you add a new rig consumer, plant it at the pose ry with no flip.
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 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, a 4-piece (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) — the 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 box).- Lane D: lift each member to its own
pose.y(front line =deckY, drummer =riserY).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 = πalready faces the audience (+Z) — no flip (§0). - The band is a 4-piece for v3.0 (front-line trio + drummer). Reverting to a trio would be a version bump
(drop
bandPoses[3]), not a runtime toggle.
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). Faces the
customer correctly under §0.
room.audio.gigKey — the live bed
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. E's beds aregig-pubrock/gig-grunge/gig-covers; a missing key → silence (audio law), band still visible. - C hand-builds
'gig-'+genreKey(byte-identical to citygen'sgigKeyFor, selfcheck-asserted) rather than importing it — deliberately, to keep the interiors lib standalone (it never imports citygen).
The venue table (C-owned tuning)
| 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. guitar_amp placement ruling (v3.0)
D plants E's guitar_amp GLB (band.js _addBackline) at (stage.x + stage.w·0.32, deckY, stage.z + 0.15).
Ruling: reposition up-stage — this pose is a v3.1 fix for D, not blessed as-is. That z = stage.z + 0.15
is down-stage (audience side): the 0.5 m amp lands 0.29 m from the bass rig, so it interpenetrates the
bassist and reads as clutter, not a backline. Move it up-stage behind the front line and clear of both
the bass rig and C's own primitive ampStacks. C's placeStage plants 1–2 primitive ampStack fittings
at (±stage.w·0.28, deckY, stage.z − stage.d·0.18) (up-stage, always rendered, the asset-free backline
fallback — no GLB slot, so they stay primitive). A clean spot for D's GLB: (stage.w·0.36, deckY, stage.z − stage.d·0.22) (up-stage-right, clears the bass and offsets from the primitives). With the GLB up-stage it
reads as a flanking backline with C's primitive. Redundancy note: the primitive + GLB coexist (2–3 amps);
fully unifying them — C exposes a stage.backline[] slot list that D's GLB fills, replacing the primitive —
is a v3.1 item (no new contract fields in the frozen v3.0). Since the interpenetration is cosmetic and
?gigs=1 is opt-in, it does not block the v3.0 tag; D corrects the pose when convenient.
4. Budgets (measured R14, seed 20261990, fresh context)
- Empty shells (
drawSweepglb-off): band_room 124 · pub 126 · rsl 135 — town-wide worst 161 (opshop/hall). ≤ 350. ✓ (identical to R13 — C geometry unchanged). - Gig-night interior draws, D's
GigCrewwith the real instrument GLBs + amp (localdepot): pub 60 · band_room 52 · rsl 71. Asset-free (placeholder crowd, no GLBs) worst RSL 237 (16 rigs). ≤ 350. ✓ - Tris: fleet peds ~2.7k each; E's instrument GLBs 14k each (decimated to the ≤15k target), 5 on a stage = 70k — the instruments dominate. RSL gig-night ≈ 182k tris once the async GLBs settle. The interior gate is draws (passes); tris are the street budget (200k), so this is not an interior gate. Note for E/F: F's gig-night smoke reports ~34k because it measures before the async instrument GLBs load; the settled scene is ~180k. A camera-distance LOD on instruments is a v3.1 candidate.
- Determinism / release-law: C's interior code is unchanged since R13; D's continuity + RY-fix ride
disjoint rng namespaces (
ctx.stream(salt)vsrng(citySeed,kind,id)) and cannot perturb C's placement or any golden. 90-build venue soak: 0 determinism · 0 carve · 0 path-fail · worst 8.3 ms; leak geo0/tex0.
5. The archetypes (C-owned)
band_room = a tin shed: small (cosy/pokey), low ceiling, barely-a-riser deck, near-empty standing floor,
bar in the corner — the PA 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 keep GLB slots for Lane E (primitive fallback holds); the
primitive ampStacks are the backline fallback (see §3).