PROCITY/web/js/interiors/theme.js
m3ultra c88d8906a2 Lane C R6 C2: map the 3 orphaned fittings (glass_case/magazine_rack/crate_stack)
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>
2026-07-14 23:19:48 +10:00

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