Published R5, unconsumed until now: - glb.js KIND_TO_GLB: glassCase->glass_case, magazineRack->magazine_rack, and a NEW crateStack primitive (fittings.js — a stack of crates) -> crate_stack. yaw 0 (validated on-screen). - theme.js: crateStack in record + opshop (back-room dressing); glassCase added to milkbar; magazineRack added to pawn; milkbar magazineRack moved window->wall (dodges the door-counter congestion). - layout.js: fixed placeNearCounter — it picked one random side and gave up, so counter-zone fittings (the glass case) rarely landed. Now it tries both sides + offsets, then falls back to a free aisle spot, so a min>=1 fitting reliably lands. Validated (GLB on, baked depot): all 3 load + render planted/scaled/facing right. Appearance glassCase 12/12 (toy/pawn/milkbar), magazineRack 12/12 (milkbar/pawn), crateStack ~11/12 (record/opshop). drawSweep still <=350 with them on (worst 177 GLB-on / 157 off). Determinism 0 fails, leak-free, dig (?dig=1) still riffles batched bins (C3 re-verify). Contact sheet docs/shots/laneC/glb_orphans_r6.jpg. qa.sh --strict GREEN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
242 lines
13 KiB
JavaScript
242 lines
13 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'],
|
||
},
|
||
};
|
||
|
||
// 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);
|
||
}
|
||
}
|