- 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>
309 lines
18 KiB
JavaScript
309 lines
18 KiB
JavaScript
// PROCITY Lane C — type theming. Every CITY_SPEC shop type gets a distinct interior recipe so a
|
||
// `record` shop instantly reads different from a `toy` shop the moment the door opens.
|
||
//
|
||
// LANE_C says "import SHOP_TYPES from core/registry.js" — Lane A hasn't landed registry.js yet, so
|
||
// this lane stays FULLY STANDALONE: the canonical recipes live here. When registry.js exists, Lane F
|
||
// can call `mergeRegistry(SHOP_TYPES)` to let authored registry data override any field. No hard
|
||
// dependency, so the test page runs with zero other lanes.
|
||
|
||
// Fitting `kind`s referenced below must exist in fittings.js FITTINGS registry.
|
||
// Zones the layout placer understands: 'window' (front, by glazing), 'aisle' (centre rows),
|
||
// 'wall' (perimeter, uses wall-slots), 'centre' (free-standing island), 'counter' (till corner).
|
||
//
|
||
// Recipe fields:
|
||
// archetypeBias : weighted room-shape preference (keys of shell.ARCHETYPES) — 'auto' picks from here
|
||
// wallpaperBias : biased pick from web/assets/gen/wall-*.jpg (names without prefix/suffix)
|
||
// floorBias : biased pick from carpet/lino/board floor names
|
||
// clutter : 0..1 density multiplier (some shops crammed, some sparse)
|
||
// counterPos : 'door' | 'forward' | 'corner' | 'back' — where the till lives
|
||
// fittings : the mix. Each {kind, zone, min, max, priority}. Counts scale with clutter+area.
|
||
// priority: higher = more essential; flood-fill removes low-priority first if the
|
||
// door→counter path is ever blocked. Counter is always highest.
|
||
// stockKind : what stock.js renders into this shop's carriers
|
||
// signFlavour : seeded inside signage strings, themed
|
||
|
||
export const ARCH = { cosy: 'cosy', gallery: 'gallery', wide: 'wide', hall: 'hall', pokey: 'pokey' };
|
||
|
||
const RECIPES = {
|
||
// ── RECORD ─────────────────────────────────────────────────────────────────────
|
||
record: {
|
||
label: 'Record Store',
|
||
archetypeBias: [['cosy', 3], ['gallery', 2], ['pokey', 1]],
|
||
wallpaperBias: ['woodchip-white', 'stripe-sage', 'geo-orange'],
|
||
floorBias: ['boards-polished', 'carpet-greygreen', 'lino-cork'],
|
||
clutter: 0.85,
|
||
counterPos: 'corner',
|
||
fittings: [
|
||
{ kind: 'listeningCorner', zone: 'window', min: 1, max: 1, priority: 6 }, // focal hero prop — placed first + high priority
|
||
{ kind: 'recordBin', zone: 'aisle', min: 2, max: 5, priority: 5 },
|
||
{ kind: 'crate', zone: 'centre', min: 1, max: 3, priority: 3 },
|
||
{ kind: 'wallShelf', zone: 'wall', min: 1, max: 3, priority: 3 },
|
||
{ kind: 'crateStack', zone: 'wall', min: 1, max: 2, priority: 2 }, // back-room dressing (crate_stack GLB)
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
stockKind: 'sleeves',
|
||
signFlavour: ['NEW ARRIVALS', 'VINYL $5 EA', 'SORRY NO REFUNDS', '2ND HAND CDs', 'JAZZ · SOUL · FUNK'],
|
||
},
|
||
|
||
// ── OP SHOP ────────────────────────────────────────────────────────────────────
|
||
opshop: {
|
||
label: 'Op Shop',
|
||
archetypeBias: [['cosy', 3], ['wide', 2], ['hall', 1]],
|
||
wallpaperBias: ['floral-cream', 'damask-mauve', 'trellis-blue', 'floral-gold'],
|
||
floorBias: ['carpet-swirl', 'carpet-mustard', 'lino-check'],
|
||
clutter: 1.0,
|
||
counterPos: 'corner',
|
||
fittings: [
|
||
{ kind: 'clothesRack', zone: 'centre', min: 2, max: 5, priority: 5 },
|
||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 },
|
||
{ kind: 'bookshelf', zone: 'wall', min: 1, max: 2, priority: 3 },
|
||
{ kind: 'trestleTable', zone: 'aisle', min: 1, max: 2, priority: 3 },
|
||
{ kind: 'crateStack', zone: 'wall', min: 1, max: 2, priority: 2 }, // back-room dressing (crate_stack GLB)
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
stockKind: 'garments',
|
||
signFlavour: ['ALL CLOTHES $2', 'BAG SALE SAT', 'BRIC-A-BRAC', 'PROCEEDS TO CHARITY', 'HALF PRICE'],
|
||
},
|
||
|
||
// ── TOY ────────────────────────────────────────────────────────────────────────
|
||
toy: {
|
||
label: 'Toy Shop',
|
||
archetypeBias: [['wide', 3], ['cosy', 2], ['hall', 1]],
|
||
wallpaperBias: ['geo-orange', 'diamond-green', 'trellis-blue'],
|
||
floorBias: ['lino-check', 'carpet-greygreen', 'lino-cork'],
|
||
clutter: 0.75,
|
||
counterPos: 'corner',
|
||
fittings: [
|
||
{ kind: 'cubeShelf', zone: 'wall', min: 2, max: 4, priority: 5 },
|
||
{ kind: 'trestleTable', zone: 'aisle', min: 1, max: 3, priority: 3 },
|
||
{ kind: 'glassCase', zone: 'counter', min: 1, max: 1, priority: 4 },
|
||
{ kind: 'metalShelf', zone: 'aisle', min: 1, max: 2, priority: 3 },
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
stockKind: 'boxes',
|
||
signFlavour: ['NEW! ', 'AS SEEN ON TV', 'PLAY & SAVE', 'BATTERIES EXTRA', 'LAYBY WELCOME'],
|
||
},
|
||
|
||
// ── BOOK ───────────────────────────────────────────────────────────────────────
|
||
book: {
|
||
label: 'Book Barn',
|
||
archetypeBias: [['hall', 3], ['gallery', 2], ['cosy', 1]],
|
||
wallpaperBias: ['damask-mauve', 'woodchip-white', 'floral-gold'],
|
||
floorBias: ['carpet-greygreen', 'boards-polished', 'carpet-swirl'],
|
||
clutter: 0.9,
|
||
counterPos: 'corner',
|
||
fittings: [
|
||
{ kind: 'bookshelf', zone: 'aisle', min: 3, max: 5, priority: 5 },
|
||
{ kind: 'bookshelf', zone: 'wall', min: 2, max: 3, priority: 4 },
|
||
{ kind: 'spinnerRack', zone: 'centre', min: 1, max: 2, priority: 3 },
|
||
{ kind: 'armchair', zone: 'window', min: 1, max: 1, priority: 2 },
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
stockKind: 'spines',
|
||
signFlavour: ['ALL BOOKS $2', 'FICTION A–M', 'RARE & COLLECTABLE', 'NO HAGGLING (haggle at till)', 'POETRY ↑'],
|
||
},
|
||
|
||
// ── VIDEO ──────────────────────────────────────────────────────────────────────
|
||
video: {
|
||
label: 'Video Rental',
|
||
archetypeBias: [['gallery', 3], ['cosy', 2], ['wide', 1]],
|
||
wallpaperBias: ['diamond-green', 'geo-orange', 'stripe-sage'],
|
||
floorBias: ['carpet-swirl', 'carpet-greygreen', 'lino-cork'],
|
||
clutter: 0.85,
|
||
counterPos: 'door',
|
||
fittings: [
|
||
{ kind: 'arcadeCabinet', zone: 'aisle', min: 1, max: 1, priority: 6 }, // focal hero prop — placed first + high priority so the door-counter path loop won't pull it
|
||
{ kind: 'vhsAisle', zone: 'aisle', min: 2, max: 4, priority: 5 },
|
||
{ kind: 'vhsAisle', zone: 'wall', min: 1, max: 2, priority: 4 },
|
||
{ kind: 'returnsSlot', zone: 'counter', min: 1, max: 1, priority: 4 },
|
||
{ kind: 'pegboard', zone: 'wall', min: 1, max: 2, priority: 2 },
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
stockKind: 'spines',
|
||
signFlavour: ['NEW RELEASES', 'OVERNIGHT $3', 'BE KIND REWIND', 'WEEKLY $2', 'DROP RETURNS HERE'],
|
||
},
|
||
|
||
// ── PAWN ───────────────────────────────────────────────────────────────────────
|
||
pawn: {
|
||
label: 'Pawnbroker',
|
||
archetypeBias: [['pokey', 3], ['cosy', 2], ['gallery', 1]],
|
||
wallpaperBias: ['woodchip-white', 'stripe-sage', 'damask-mauve'],
|
||
floorBias: ['lino-cork', 'boards-polished', 'lino-check'],
|
||
clutter: 0.7,
|
||
counterPos: 'forward', // counter is right up front; goods behind it
|
||
fittings: [
|
||
{ kind: 'glassCase', zone: 'counter', min: 1, max: 3, priority: 6 },
|
||
{ kind: 'pegboard', zone: 'wall', min: 1, max: 3, priority: 4 }, // wall hooks
|
||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 3 },
|
||
{ kind: 'magazineRack', zone: 'wall', min: 1, max: 1, priority: 2 }, // magazine_rack GLB
|
||
{ kind: 'barredWindow', zone: 'window', min: 1, max: 1, priority: 3 },
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
stockKind: 'mixed',
|
||
signFlavour: ['WE BUY ANYTHING', 'CASH LOANS', 'GOLD · TOOLS · GEAR', 'ID REQUIRED', 'UNREDEEMED PLEDGES'],
|
||
},
|
||
|
||
// ── MILK BAR ─────────────────────────────────────────────────────────────────────
|
||
milkbar: {
|
||
label: 'Milk Bar',
|
||
archetypeBias: [['pokey', 3], ['cosy', 2]],
|
||
wallpaperBias: ['diamond-green', 'geo-orange', 'stripe-sage'],
|
||
floorBias: ['lino-check', 'lino-cork', 'carpet-mustard'],
|
||
clutter: 0.8,
|
||
counterPos: 'door', // counter faces the door — you're served on the way in
|
||
fittings: [
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
{ kind: 'fridge', zone: 'wall', min: 1, max: 3, priority: 6 },
|
||
{ kind: 'magazineRack', zone: 'wall', min: 1, max: 2, priority: 4 }, // wall (was window — conflicted with the door-counter)
|
||
{ kind: 'glassCase', zone: 'counter', min: 1, max: 1, priority: 3 }, // lolly/treat display case (glass_case GLB)
|
||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 },
|
||
{ kind: 'cubeShelf', zone: 'aisle', min: 0, max: 1, priority: 2 },
|
||
],
|
||
stockKind: 'snacks',
|
||
signFlavour: ['MILK · BREAD · SMOKES', 'PIES HOT', 'PAPERS IN', 'PHONECARDS', 'NO SCHOOL KIDS AT LUNCH'],
|
||
},
|
||
|
||
// ── DEPT (anchor) ──────────────────────────────────────────────────────────────
|
||
dept: {
|
||
label: 'Department Store',
|
||
archetypeBias: [['hall', 4], ['wide', 1]],
|
||
wallpaperBias: ['floral-gold', 'damask-mauve', 'trellis-blue'],
|
||
floorBias: ['carpet-greygreen', 'carpet-swirl', 'boards-polished'],
|
||
clutter: 0.7,
|
||
counterPos: 'corner',
|
||
fittings: [
|
||
{ kind: 'cubeShelf', zone: 'aisle', min: 2, max: 4, priority: 4 },
|
||
{ kind: 'clothesRack', zone: 'centre', min: 2, max: 4, priority: 4 },
|
||
{ kind: 'glassCase', zone: 'aisle', min: 1, max: 2, priority: 3 },
|
||
{ kind: 'metalShelf', zone: 'wall', min: 2, max: 4, priority: 3 },
|
||
{ kind: 'escalator', zone: 'centre', min: 1, max: 1, priority: 5 }, // the grand-hall centrepiece
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
stockKind: 'mixed',
|
||
signFlavour: ['MANCHESTER ↑', 'MENSWEAR', 'HABERDASHERY', 'GROUND FLOOR', 'STAFF ONLY'],
|
||
},
|
||
|
||
// ── STALL (market) ───────────────────────────────────────────────────────────────
|
||
stall: {
|
||
label: 'Market Stall',
|
||
archetypeBias: [['wide', 3], ['hall', 1]],
|
||
wallpaperBias: ['woodchip-white', 'stripe-sage'], // barely walled; open feel
|
||
floorBias: ['lino-cork', 'boards-polished'],
|
||
clutter: 0.95,
|
||
counterPos: 'forward',
|
||
fittings: [
|
||
{ kind: 'trestleTable', zone: 'aisle', min: 3, max: 6, priority: 5 },
|
||
{ kind: 'crate', zone: 'centre', min: 2, max: 5, priority: 3 },
|
||
{ kind: 'clothesRack', zone: 'wall', min: 0, max: 2, priority: 2 },
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 8 },
|
||
],
|
||
stockKind: 'boxes',
|
||
signFlavour: ['$1 EACH', 'CASH ONLY', 'FIRST IN BEST DRESSED', 'MAKE ME AN OFFER', 'FRESH TODAY'],
|
||
},
|
||
|
||
// ── PUB (venue, v3.0-alpha · round 12, behind ?gigs=1) ───────────────────────────────
|
||
// The band room. `venue: true` makes layout.js place a stage (+ PA/amps) at the back wall and emit
|
||
// room.watchPoints[] for Lane D's crowd + room.stage for the band. Bar counter = a normal counter, so
|
||
// the barkeep is just a keeper. Sticky-carpet floor; the blocked back doorway doubles as the green room.
|
||
pub: {
|
||
label: 'Pub',
|
||
archetypeBias: [['wide', 3], ['hall', 2]], // big open room for a crowd
|
||
wallpaperBias: ['damask-mauve', 'woodchip-white', 'stripe-sage'],
|
||
floorBias: ['carpet-swirl', 'carpet-mustard'], // sticky carpet
|
||
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)
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
],
|
||
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.
|
||
const ALIAS = {
|
||
music: 'record', records: 'record',
|
||
charity: 'opshop', second_hand: 'opshop', secondhand: 'opshop', antiques: 'opshop', op_shop: 'opshop',
|
||
toys: 'toy', toyshop: 'toy',
|
||
books: 'book', bookshop: 'book', bookbarn: 'book',
|
||
video_games: 'video', games: 'video', vhs: 'video', rental: 'video',
|
||
pawnbroker: 'pawn', pawnshop: 'pawn',
|
||
milk_bar: 'milkbar', deli: 'milkbar', corner_store: 'milkbar',
|
||
department: 'dept', anchor: 'dept',
|
||
market: 'stall', stalls: 'stall',
|
||
};
|
||
|
||
export const SHOP_TYPES = Object.keys(RECIPES);
|
||
|
||
export function canonicalType(type) {
|
||
if (!type) return 'opshop';
|
||
const t = String(type).toLowerCase();
|
||
if (RECIPES[t]) return t;
|
||
if (ALIAS[t]) return ALIAS[t];
|
||
return 'opshop'; // sensible default: an op shop takes anything
|
||
}
|
||
|
||
export function getRecipe(type) {
|
||
const key = canonicalType(type);
|
||
return { key, ...RECIPES[key] };
|
||
}
|
||
|
||
// Lane F hook: let an authored registry override recipe fields without editing this lane.
|
||
// registry: { [typeKey]: partialRecipe }. Shallow-merges over the built-in recipe.
|
||
export function mergeRegistry(registry) {
|
||
if (!registry) return;
|
||
for (const [k, patch] of Object.entries(registry)) {
|
||
const key = canonicalType(k);
|
||
if (RECIPES[key]) Object.assign(RECIPES[key], patch);
|
||
}
|
||
}
|