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>
238 lines
13 KiB
JavaScript
238 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: '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 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: '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);
|
||
}
|
||
}
|