PROCITY/web/js/interiors/theme.js
m3ultra 05e020e3b8 Lane C: round-12 pub venue — archetype + watchPoints + audio.gigKey (?gigs=1)
v3.0-alpha band room. New pub recipe (venue:true): stage + PA + amps at the back wall (new stage/paSpeaker/ampStack fittings, GLB slots stage/pa_speaker/amp for E), bar counter (keeper), sticky carpet, blocked back door = green room.

buildInterior returns for venues: room.watchPoints[] (6-8 seeded audience poses {x,z,ry,dance} facing the stage, dance ~1/3, counter.stand ry convention); room.stage {x,z,w,d,deckY,frontZ,bandPoses[3]} for Lane D's band; room.audio.gigKey ('gig-'+genreKey) set when Lane F passes opts.gig.

Flags-off byte-identical: venue code runs only for recipe.venue on its own seed sub-streams; non-pub rooms have watchPoints:[], stage:null, unchanged placement/audio.

Verified: 40-seed pub sweep (0 throws/carves/path-fails, 8 watchPoints, dance 0.38) + 60-room mixed soak (0 det/path fails, leak geo0/tex0, worst 19ms). Non-pub unchanged. Interface for D/F: docs/LANES/LANE_C_PUB.md. scaffold + manifest green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:59:48 +10:00

263 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 AM', '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)
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 56', 'NO THONGS AFTER 7', 'COLD BEER ON TAP', 'BANDS OUT BACK'],
},
};
// 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);
}
}