PROCITY/docs/LANES/LANE_C_PUB.md
m3ultra c7ca39b4f7 Lane C R20 (v4.0-beta): publish OSM-class -> archetype mapping (ledger #2, C's half)
The density widening's C half: real towns fetch general retail, not just secondhand.
Published the OSM shop-class -> registry-archetype mapping in LANE_C_PUB.md §6 (new
v4.0-beta amendment; frozen venue contract §0-§5 untouched). C->E handshake: E builds to it.

- No new archetypes. General retail wears existing recipes: food/grocer/newsagent/chemist
  -> milkbar; clothes/hardware/big-retail -> dept; produce/florist -> stall.
- Secondhand stays the hero (charity->opshop, books->book, music->record, video->video,
  pawn->pawn, toys->toy); never subsampled. Unknown/unlisted -> opshop (R18-proven).
- Subtle per John: texture not takeover. Copy-paste JSON map + tasteful fetch list (skip
  vehicle/funeral/industrial/pure-services) + guardrail for E to subsample if retail drowns
  the secondhand strip (count in norm.dropped).
- Mechanic: plan_osm keeps registry types, so E bakes a registry type per the map into each
  cache's shop.type -> no C code change, budgets unchanged.

Pre-validated before E builds to it: milkbar/dept/stall/opshop all build valid at a real
katoomba lot (8.5x14, 0 carves, <=143 draws); dept reads as a clean dressed-retail floor
(shelves+racks+counter, escalator does not dominate the 7.9x11.7 hall) -> clothes/hardware
-> dept confirmed. Full five-town door-soak (ledger #6) follows E's re-emitted caches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:32:05 +10:00

15 KiB
Raw Blame History

LANE C — interiors contract (venues + real-town class mapping) → Lanes A/D/E/F

v3.0-FROZEN · 2026-07-16 · durable contract for Lane D + Lane F. §0§5 describe the shipped venue state; changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (audit). v3.1 amendment: §3 became the stage.backline[] contract (ledger #5 — the amp unification). v4.0-beta amendment (2026-07-16): §6 added — the real-town shop-class → archetype mapping (ledger #2, the density widening; C→E handshake). The frozen venue contract (§0§5) is unchanged.

The district ships three venue archetypes behind ?gigs=1pub, 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.typepub | 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: 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 box).
  • Lane D: lift each member to its own pose.y (front line = deckY, drummer = riserY). 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 = π 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: 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). 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 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. E's beds are gig-pubrock / gig-grunge / gig-covers; a missing key → silence (audio law), band still visible.
  • C hand-builds 'gig-'+genreKey (byte-identical to citygen's gigKeyFor, 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. stage.backline[] — the amp slots (v3.1, ledger #5)

room.stage.backline = [ { x, z, ry, y } ×2 ] — seeded up-stage amp/spare-cab poses, pure data (C plants no amp meshes as of v3.1). This is the unified backline: Lane D plants E's guitar_amp GLB here (its own primitive fallback under ?noassets) — one amp system, GLB when present. C's old primitive ampStacks are dropped from all three venue recipes.

  • slot[0] = stage-right (the primary amp) at (stage.x + stage.w·0.36, deckY, stage.z stage.d·0.22) — identical to D's R15-verified plant, so wiring D's amp to backline[0] moves nothing on screen. slot[1] = stage-left spare cab (mirror). ry = π faces the audience (+Z, the §0 convention); y = deckY.
  • Guarantees (verified fresh, seed 20261990, pub/band_room/rsl): every slot is up-stage of the front line, flanks the drum riser, clear of all 4 bandPoses (nearest-bass clearance 0.87 m pub / 0.70 m band_room / 0.92 m rsl) and all watchPoints (≥1.95 m), and on-deck (within the deck footprint). Deterministic per shop.seed. Lane D: verify no interpenetration once wired (C's R15 clearance discipline).
  • Draw impact: dropping C's primitives shaved ~4 draws off each empty venue shell (drawSweep glb-off: pub 122 · band_room 120 · rsl 131 ≤350); D's GLB/primitive at backline[0] adds it back on the gig night.
  • v3.2 (parked): the drummer sit clip, instrument LOD, and C's audioEmitter idea.

4. Budgets (measured R14, seed 20261990, fresh context)

  • Empty shells (drawSweep glb-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 GigCrew with 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) vs rng(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 backline amp is D's, planted at stage.backline[0] (see §3).


6. Real-town shop-class → interior archetype mapping (v4.0-beta, ledger #2) → Lane E

Directive (John, kickoff — SUBTLE: texture, not takeover). The secondhand shops stay the heroes of the town (they are the game). The widening adds general retail as texture — the everyday main-street staples a real Australian town has — mapped onto existing archetypes (no new archetypes this round). The player should feel the town's more alive, not that the game changed.

How it's applied (E owns the emission). plan_osm keeps only registry types (SHOP_TYPES — unknowns flatten to opshop). So E maps each fetched OSM shop=* / amenity=* value to a registry type below and bakes it into the cache's shop.type. Every target is an existing type, so C's recipes, A's registry, and the budgets are all unchanged. Unknown/unlisted classes degrade safely to opshop (R18-proven: 18 real OSM types all built valid).

Heroes — secondhand (unchanged; NEVER subsampled):

OSM class → type
charity, second_hand, antiques, vintage opshop
books, bookshop book
music record
video, games video
pawnbroker pawn
toys toy

Widened — everyday main-street texture (general retail):

OSM class(es) → type wears it as
bakery, cafe†, confectionery, pastry, chocolate, deli/delicatessen, butcher, cheese, coffee, tea milkbar the food counter
convenience, kiosk, general, dairy milkbar the corner store
newsagent, stationery milkbar papers + magazines
chemist, pharmacy, cosmetics, perfumery milkbar dispensary counter
clothes, fashion, boutique, shoes, bag, jewelry/jewellery dept the dressed retail floor
department_store, variety_store, supermarket dept big general retail
hardware, doityourself, trade, electronics, mobile_phone, furniture, houseware/homeware dept shelved goods
greengrocer, florist, farm, garden_centre stall produce/plants on trestles

cafe/fast_food are amenity=*, not shop=* — fetch them from the amenity tag; everything else is shop=*. Fallback: any fetched class not listed → opshop.

Copy-paste map for E (build_towns.py):

{
  "charity":"opshop","second_hand":"opshop","antiques":"opshop","vintage":"opshop",
  "books":"book","bookshop":"book","music":"record","video":"video","games":"video",
  "pawnbroker":"pawn","toys":"toy",
  "bakery":"milkbar","cafe":"milkbar","confectionery":"milkbar","pastry":"milkbar","chocolate":"milkbar",
  "deli":"milkbar","delicatessen":"milkbar","butcher":"milkbar","cheese":"milkbar","coffee":"milkbar","tea":"milkbar",
  "convenience":"milkbar","kiosk":"milkbar","general":"milkbar","dairy":"milkbar",
  "newsagent":"milkbar","stationery":"milkbar",
  "chemist":"milkbar","pharmacy":"milkbar","cosmetics":"milkbar","perfumery":"milkbar",
  "clothes":"dept","fashion":"dept","boutique":"dept","shoes":"dept","bag":"dept","jewelry":"dept","jewellery":"dept",
  "department_store":"dept","variety_store":"dept","supermarket":"dept",
  "hardware":"dept","doityourself":"dept","trade":"dept","electronics":"dept","mobile_phone":"dept",
  "furniture":"dept","houseware":"dept","homeware":"dept",
  "greengrocer":"stall","florist":"stall","farm":"stall","garden_centre":"stall"
}

Default (not in the map) → opshop.

Subtlety guardrails (E):

  • Fetch only these tasteful staples — not every OSM shop=*. Skip vehicle/tyres/fuel, funeral, storage, industrial, and pure services the player never browses (hairdresser, beauty, travel_agency, bank, laundry, estate_agent).
  • Never subsample the secondhand heroes.
  • If a town's general-retail count would visually drown the secondhand strip, seed-subsample the texture classes (the milkbar/dept/stall targets) toward ~23× the secondhand count, and count the drops in norm.dropped like every other drop.

Why these targets: milkbar (counter + fridge + magazine rack + glass case) is the canonical Australian counter shop — it wears cafe/bakery/deli/newsagent/chemist naturally; dept (cube shelves + clothes racks + glass cases) wears clothes + hardware + big retail; stall (trestles + crates) wears produce + florist. Three general-retail looks + the six secondhand heroes + the three venues = variety with no new archetypes. Pre-verified (seed 20261990, real-town lot 8.5×14): milkbar/dept/stall/opshop all build valid (0 carves, ≤143 draws); dept reads as a clean dressed-retail floor (cube shelves + clothes racks + counter, the escalator does not dominate the ~7.9×11.7 room), so clothes/hardwaredept is confirmed. C re-checks the full result in the five-town door-soak (ledger #6) once E's caches land. (FYI: C's theme.js canonicalType/ALIAS already recognizes several of these — deli/corner_store→milkbar etc. — for any direct buildInterior(rawClass); the authoritative mapping for the caches is this table.)