PROCITY/web/js/interiors/theme.js
m3ultra 34926324bd Lane C R4: hero props into rooms (arcade/booth/fridge/mixer/register)
Round-4 §Lane C — map the 4 new hero props + cash_register (manifest now
14 fittings), validate, measure, clean up.

- glb.js: map listening_booth→listeningCorner (record), drinks_fridge→fridge
  (milkbar), arcade_cabinet→new arcadeCabinet primitive (video focal prop).
  New attachCounterTop() places cash_register on every counter's till slot
  (hides the primitive till) + milkshake_mixer on the milk-bar benchtop;
  counter primitive + keeper stand untouched. All yaw 0 (no correction).
- fittings.js: new arcadeCabinet primitive (footprint/height matched to the
  GLB); counter exposes counterTop attach points + tags the primitive till.
- theme.js: arcadeCabinet in the video recipe; the 3 focal props (arcade,
  listeningCorner already, +) raised to priority 6 + placed first so the
  door-counter path loop can't pull them → 24/24 appearance.
- interiors.js: thread recipe.key to upgradeRoom (gates milkshake_mixer to
  milk bars).
- interior_test.html: load GLBs via the live depot: path (drop the deleted
  _local_glb localBase shim; ?localdepot=1 still works offline).

Validated (all via live depot): primitive sweep 810 {throws 0, pathFail 0,
detFail 0}; GLB-on soak {throws 0, path 0, det 0, leakGeo 0, leakTex 0,
worst 7.3ms}; all 5 new props render planted/scaled/facing right (contact
sheet docs/shots/laneC/glb_heroprops_r4.jpg). Room tris measured: milk-bar
GLB-on ~468k (drinks_fridge 77k + milkshake_mixer 73k) — reported to Lane E
as the #1 bake-pass target (not fixed, per integrator decision #2; interiors
render one-at-a-time so gate 3 is unaffected). record_crate still broken +
counter still oversized — both await Lane E re-export (C task 5). Deleted the
stale web/assets/_local_glb/ validation copy. qa.sh --strict GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 18:44:09 +10:00

238 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: '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: '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: '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: 'window', min: 1, max: 2, priority: 4 },
{ 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);
}
}