// 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) 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'], }, }; // 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); } }