110 fittings placed across 12 shop types via new kit41.js (96-row catalogue, 67 ids in pools) and ONE generic kitProp builder; recipes name pools, never asset ids. glb.js now honours seeded per-fitting variants, multi-GLB booths, and a MEASURED autoYaw instead of a 110-row facing table. THE INSTRUMENT WAS WRONG. drawSweep cleared every interior EXCEPT current, so every historical interior number carried a phantom second room — 'toy', untouched this round, 'gained' 4 draws. R39/R40's celebrated 'worst 188, margin 162' was inflated. Corrected, and with 110 new fittings placed: worst room in the game is 122 @ dept/auto, MARGIN 228. Per type: record 61->78, pub 45->59, band_room 39->50, stall 71->82, book 51->61, pawn 60->70, video 54->63, rsl 52->60, dept 116->122, opshop 102->105, milkbar 68->69, toy 86->86 (+0, the control). The pub finally has a bar (plywood_bar), banquettes, pokies, a stage flank. Milk bars get a bain-marie and pie warmer. The pawn shop gets an 808 and an SP-1200 on the bench. Record shops get a rigged DJ booth. Market stalls get 2-4 diggable bins each, closing R39's measured 44%-no-crate hole to 0/40. WARDROBE, route R5: 257.5 KB total (246 KB atlas + 17 KB index), ONE fetch, one material, one draw per fitting — 52 layers picked BY NAME, alpha-cropped, shelf-packed, 3.7% of source bytes, lazy behind ?stock=real. LICENCE CALL MADE EXPLICITLY: AUDIT.md says no real brands ship and the source library is prompted FROM real labels — the adidas jacket renders a legible trefoil. Screened on a contact sheet, then relabelled in parody voice (Paddy's Bootleg, Kaymart, Coogee Knits, Mombo, Stoosh). FIVE DEFECTS FOUND AND FIXED, one long-standing: buyMesh has been INVISIBLE on every GLB fitting since R9 (hiding every buyable item under ?stock=real) · DJ booths were nondeterministic (parts attached in network order) · booth gear floated at 0.92 m · batch.js dropped userData sub-tags on merge, so any post-batch flag read was already a no-op · the path guarantee could empty a room (pub/wide/4242 returned a bar, a stage and nothing else) — new restore pass, 129 pulled -> 101 restored over 480 rooms, path re-proved on every restore. Gates: 288-room sweep x 4 arms all pass; soak 60 rooms avg 3.83 ms worst 10.3 ms, leaks 0; 960-room corpus 0 path-fails 0 carves; determinism byte-equal across fresh contexts (sha256 c26b4848); ?noassets=1 60 rooms with 0 GLB / 0 manifest / 0 wardrobe / 0 stock fetches. DJ control nodes recorded on fitting.controlNodes for a future driver (glTF drops the limit constraints — a driver must clamp travel itself). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
440 lines
29 KiB
JavaScript
440 lines
29 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, o?}. 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.
|
||
// o (R39, optional): opts handed to the fitting builder. Pure data, reads no stream.
|
||
// A per-build override lives at buildInterior opts.fittingOpts[kind].
|
||
// fallbackZones / anywhere (R39, optional): opt-in placement fallbacks for a fitting
|
||
// the recipe PROMISES will be there. Absent ⇒ pre-R39 behaviour (drop it).
|
||
// stockKind : what stock.js renders into this shop's carriers
|
||
// signFlavour : seeded inside signage strings, themed
|
||
|
||
// [R41 §41.4] The kit pools live in kit41.js; recipes name a pool, never an asset id, so a manifest change
|
||
// is one edit in one file. Every kit spec below is APPENDED LAST inside its recipe on purpose: layout.js
|
||
// draws counts from `lay-count` and placements from `lay-place` in array order, so a spec added at the END
|
||
// leaves every pre-existing fitting in that recipe the SAME draws, and therefore the same placement. (The
|
||
// R39 lesson, applied at ten times the size.)
|
||
import { POOLS } from './kit41.js';
|
||
|
||
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 },
|
||
// ── R41: the 3dstore shop floor. Real record-shop fittings, not a scatter: bins to flick through in
|
||
// the aisle, tiered racks behind them, boards on the wall, a window ledge, sleeves pinned up. The
|
||
// record shop already digs (R39), so its bins mint `kind:'bin'` — this is the one type where more
|
||
// bins is unambiguously more game.
|
||
{ kind: 'kitBin', zone: 'aisle', min: 2, max: 4, priority: 4, fallbackZones: ['centre'],
|
||
o: { pool: POOLS.record_bins, dig: true, stock: 'sleeve', items: 12 } },
|
||
{ kind: 'kitRack', zone: 'aisle', min: 1, max: 3, priority: 4, fallbackZones: ['centre'],
|
||
o: { pool: POOLS.shop_racks, dig: true, stock: 'sleeve', items: 9 } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 1, max: 2, priority: 3,
|
||
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 5, maxRun: 3.0 } },
|
||
{ kind: 'kitWindow', zone: 'window', min: 0, max: 1, priority: 2, o: { pool: POOLS.window_dress } },
|
||
{ kind: 'kitSleeve', zone: 'wall', min: 1, max: 2, priority: 1,
|
||
o: { pool: POOLS.sleeve_wall, wall: true, mountY: 1.72 } }, // TRAP 2: vinyl_sleeve, wall only
|
||
// THE HERO: the rigged 1200 + a rigged TTM-54i behind the counter. 8 draws, one room, once.
|
||
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 6, gap: 2.35,
|
||
fallbackZones: ['aisle', 'centre'], anywhere: true, o: { booth: 'record_hero' } },
|
||
],
|
||
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 },
|
||
// [R39 — THE OP SHOP CAN'T BE DUG] the op shop's own floor bin. APPENDED LAST on purpose: the
|
||
// recipe loop draws counts from `lay-count` and placements from `lay-place` in array order, so a
|
||
// spec added at the end leaves every existing op-shop fitting's draws — and therefore its
|
||
// placement — byte-identical, and only adds the tub. `dig:false` is the CONTENTS gate: off, this
|
||
// is furniture (kind 'prop'); `{ rummageBin:{dig:true} }` in opts.fittingOpts makes every op shop
|
||
// diggable. WHAT IT YIELDS IS JOHN'S CALL — LANE_C_PUB §10. Do not flip the default in passing.
|
||
{ kind: 'rummageBin', zone: 'aisle', min: 1, max: 1, priority: 4,
|
||
fallbackZones: ['centre', 'window'], anywhere: true, o: { dig: false } },
|
||
// ── R41: bargain tubs + a wooden rack + a wall board. `dig` STAYS OFF — R39 measured the whole
|
||
// question ("op shops now dig RECORDS") and held the contents for John; nothing here reopens it.
|
||
{ kind: 'kitBin', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre', 'window'],
|
||
o: { pool: POOLS.bargain_tubs, stock: 'box', items: 10 } },
|
||
{ kind: 'kitRack', zone: 'wall', min: 1, max: 2, priority: 3, fallbackZones: ['aisle'],
|
||
o: { pool: POOLS.shop_racks, stock: 'box', items: 8 } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 2,
|
||
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 5, maxRun: 3.0 } },
|
||
],
|
||
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 },
|
||
// ── R41: the returns trolley (3dstore `library_trolley`) + a rack of overflow + wall boards.
|
||
{ kind: 'kitTrolley', zone: 'aisle', min: 1, max: 1, priority: 3, fallbackZones: ['centre'],
|
||
o: { pool: POOLS.library, stock: 'spine', items: 9 } },
|
||
{ kind: 'kitRack', zone: 'wall', min: 1, max: 2, priority: 2, fallbackZones: ['aisle'],
|
||
o: { pool: POOLS.shop_racks, stock: 'spine', items: 9 } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 1, max: 2, priority: 2,
|
||
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.55, stock: 'spine', items: 8, maxRun: 3.0 } },
|
||
],
|
||
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 },
|
||
// ── R41: the ex-rental bargain tub by the door + wall boards + the window display.
|
||
{ kind: 'kitBin', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre', 'window'],
|
||
o: { pool: POOLS.bargain_tubs, stock: 'box', items: 10 } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 1, max: 2, priority: 2,
|
||
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 6, maxRun: 3.0 } },
|
||
{ kind: 'kitWindow', zone: 'window', min: 0, max: 1, priority: 2, o: { pool: POOLS.window_dress } },
|
||
],
|
||
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 },
|
||
// ── R41: unredeemed pledges. Flight cases and a sub on the floor; an 808 and an SP-1200 on the
|
||
// bench behind the grille — statics, 2 draws, because a pawnbroker's gear is not a working booth.
|
||
{ kind: 'kitGear', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre'],
|
||
o: { pool: POOLS.pawn_gear } },
|
||
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 4, gap: 2.1,
|
||
fallbackZones: ['aisle', 'centre'], anywhere: true, o: { booth: 'pawn' } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 2,
|
||
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 5, maxRun: 3.0 } },
|
||
],
|
||
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 },
|
||
// ── R41: the hot box. `bain_marie` + `pie_warmer` are the two props that make a milk bar a milk bar
|
||
// (this is also PROCITY's cafe — there is no separate `cafe` type in SHOP_TYPE_IDS).
|
||
{ kind: 'kitHot', zone: 'wall', min: 1, max: 2, priority: 5, fallbackZones: ['aisle'],
|
||
o: { pool: POOLS.milkbar_hot } },
|
||
{ kind: 'kitFloor', zone: 'aisle', min: 1, max: 3, priority: 2, fallbackZones: ['window'],
|
||
o: { pool: POOLS.milkbar_floor } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 1,
|
||
o: { pool: POOLS.wall_service, wall: true, mountY: 2.35 } }, // the rattling air-con
|
||
],
|
||
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 },
|
||
// ── R41: SPEND CAREFULLY HERE. dept is the worst room in the game (GLB-on 188 pre-R41, i.e. it owns
|
||
// the whole 162-draw margin), so it gets the fewest additions of any type: columns for the hall and
|
||
// one rack row.
|
||
{ kind: 'kitColumn', zone: 'centre', min: 0, max: 2, priority: 3, o: { pool: POOLS.columns } },
|
||
{ kind: 'kitRack', zone: 'aisle', min: 1, max: 2, priority: 3,
|
||
o: { pool: POOLS.shop_racks, stock: 'box', items: 8 } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 2,
|
||
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.6, stock: 'box', items: 6, maxRun: 3.0 } },
|
||
],
|
||
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 },
|
||
// ── R41: this closes a MEASURED HOLE, not just a dressing gap. R39 counted 48 stall rooms and found
|
||
// 21 of them (44%) with no crate at all — the recipe's `crate` is zone 'centre' and the 3–6 trestles
|
||
// take the aisle first, so RED TICKET DAY could name a stall you cannot dig in. These bins carry the
|
||
// full fallback chain (aisle → centre → window → anywhere), so a market stall always has something
|
||
// to riffle. Re-measured after the change; the number is in C-progress.md §41.4.
|
||
{ kind: 'kitBin', zone: 'aisle', min: 2, max: 4, priority: 4,
|
||
fallbackZones: ['centre', 'window'], anywhere: true,
|
||
o: { pool: POOLS.record_bins, dig: true, stock: 'sleeve', items: 12 } },
|
||
{ kind: 'kitRack', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre'],
|
||
o: { pool: POOLS.shop_racks, stock: 'box', items: 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)
|
||
// venueSpec (round 13): per-kind stage/crowd tuning read by layout.js placeStage/placeWatchPoints.
|
||
// watchMax = crowd cap (== max watchPoints); deckH = deck height; deckDepth/deckWidthFrac size the
|
||
// deck. pub values == the R12 constants, so the pub stage is byte-identical (bar the new drums pose).
|
||
venueSpec: { watchMax: 8, deckH: 0.32, deckDepth: 2.0, deckWidthFrac: 0.58 },
|
||
// [R41] the bar is Lane E's `plywood_bar`, not the shop `counter_till`. Same fitting, same keeper
|
||
// stand, same service strip — only the model changes.
|
||
counterGlb: 'plywood_bar',
|
||
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 },
|
||
// ── R41 — THE PUB FINALLY GETS FURNISHED. Before this round the whole room was a bar, a stage, two
|
||
// PA cabs and up to three trestles: the gig venue with almost nothing in it. Seating hugs the WALLS
|
||
// and the floor props sit at priority 2 so the door→bar path loop pulls them FIRST — the crowd's
|
||
// standing room (watchPoints) is computed after placement and must survive furnishing. Both numbers
|
||
// (draws AND watchPoints per venue) are re-measured in C-progress.md §41.4.
|
||
// ORDER MATTERS INSIDE THIS BLOCK (and only inside it — every pre-R41 spec is above and untouched):
|
||
// the venue kit goes FIRST because the stage flanks and the side-wall slots beside the stage are
|
||
// the same 0.4 m cells. Seating placed first took them, and the gear landed 5 times in 15 rooms;
|
||
// gear first, and it lands every time while the seating simply takes the next wall slot along.
|
||
{ kind: 'kitVenue', zone: 'stage', min: 2, max: 3, priority: 2, fallbackZones: ['wall', 'aisle'],
|
||
o: { pool: POOLS.venue_kit } },
|
||
{ kind: 'kitSeat', zone: 'wall', min: 3, max: 6, priority: 3, fallbackZones: ['aisle', 'centre'],
|
||
o: { pool: POOLS.pub_seating } },
|
||
{ kind: 'kitFloor', zone: 'wall', min: 3, max: 6, priority: 2, fallbackZones: ['aisle'],
|
||
o: { pool: POOLS.pub_floor } },
|
||
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 1,
|
||
o: { pool: POOLS.wall_service, wall: true, mountY: 2.4 } },
|
||
// the DJ rig beside the bar — STATICS (E: "a static copy is 1 draw and looks the same standing still")
|
||
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 4, gap: 2.2,
|
||
fallbackZones: ['stage', 'wall', 'aisle', 'centre'], anywhere: true, o: { booth: 'venue' } },
|
||
],
|
||
stockKind: 'snacks',
|
||
signFlavour: ['LIVE MUSIC FRI', 'HAPPY HOUR 5–6', 'NO THONGS AFTER 7', 'COLD BEER ON TAP', 'BANDS OUT BACK'],
|
||
},
|
||
|
||
// ── BAND ROOM (venue, v3.0-beta · round 13, behind ?gigs=1) ──────────────────────────
|
||
// A tin shed on the warehouse fringe: small, scuzzy, low ceiling, the PA louder than the room and the
|
||
// stage barely a riser. No tables — you stand and you sweat. `genre: 'grunge'` ⇒ gigKey 'gig-grunge'.
|
||
// Reuses the pub venue machinery; only the dressing + venueSpec differ.
|
||
band_room: {
|
||
label: 'Band Room',
|
||
archetypeBias: [['cosy', 3], ['pokey', 2], ['wide', 1]], // small rooms; low ceilings come from storeys=1
|
||
wallpaperBias: ['woodchip-white', 'stripe-sage', 'damask-mauve'], // dim + dingy
|
||
floorBias: ['lino-cork', 'carpet-swirl', 'boards-polished'], // bare/sticky
|
||
clutter: 0.35, // near-empty standing room — the crowd IS the furniture
|
||
counterPos: 'corner', // a plank bar in the corner (keeper = barkeep)
|
||
venue: true,
|
||
venueSpec: { watchMax: 8, deckH: 0.2, deckDepth: 1.6, deckWidthFrac: 0.62 }, // barely a riser, wide + shallow
|
||
counterGlb: 'plywood_bar', // a plank bar is literally what this recipe says it is
|
||
fittings: [
|
||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf (tinnies)
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
// ── R41: PA, sub, flight cases, a smoke machine and a distro box shoved against the wall — the tin
|
||
// shed's furniture is the gear. No seating: you stand and you sweat (the recipe's own words).
|
||
{ kind: 'kitVenue', zone: 'stage', min: 2, max: 4, priority: 3, fallbackZones: ['wall', 'aisle'],
|
||
o: { pool: POOLS.venue_kit } },
|
||
{ kind: 'kitFloor', zone: 'wall', min: 2, max: 3, priority: 2, fallbackZones: ['aisle'],
|
||
o: { pool: POOLS.pub_floor } },
|
||
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 4, gap: 2.2,
|
||
fallbackZones: ['stage', 'wall', 'aisle'], o: { booth: 'venue' } },
|
||
],
|
||
stockKind: 'snacks',
|
||
signFlavour: ['ALL AGES SUN', 'PAY THE BAND', 'NO STAGE DIVING', 'MERCH OUT BACK', '$5 ON THE DOOR'],
|
||
},
|
||
|
||
// ── RSL CLUB (venue, v3.0-beta · round 13, behind ?gigs=1) ───────────────────────────
|
||
// Carpet, flock wallpaper, a bistro round the edge, a proper stage at one end, and the biggest crowd
|
||
// cap in town — the round-13 crowd-cap stress case. `genre: 'covers'` ⇒ gigKey 'gig-covers'.
|
||
rsl: {
|
||
label: 'RSL Club',
|
||
archetypeBias: [['hall', 3], ['wide', 2]], // big floor for the biggest crowd
|
||
wallpaperBias: ['damask-mauve', 'floral-gold', 'trellis-blue'], // clubby flock
|
||
floorBias: ['carpet-mustard', 'carpet-swirl', 'carpet-greygreen'], // patterned club carpet
|
||
clutter: 0.5, // bistro tables round the edges, floor kept open for the crowd
|
||
counterPos: 'corner', // the members' bar
|
||
venue: true,
|
||
venueSpec: { watchMax: 12, deckH: 0.4, deckDepth: 2.4, deckWidthFrac: 0.5 }, // a raised stage at one end
|
||
counterGlb: 'plywood_bar',
|
||
fittings: [
|
||
{ kind: 'trestleTable', zone: 'wall', min: 2, max: 4, priority: 2 }, // bistro tables round the edge
|
||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf
|
||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||
// ── R41: banquettes and bistro tables round the edge, the pokies and the meat-raffle drum the
|
||
// signage has been promising since R13. The 12-strong crowd cap is why everything hugs a wall.
|
||
{ kind: 'kitVenue', zone: 'stage', min: 1, max: 3, priority: 2, fallbackZones: ['wall', 'aisle'],
|
||
o: { pool: POOLS.venue_kit } },
|
||
{ kind: 'kitSeat', zone: 'wall', min: 3, max: 6, priority: 3, fallbackZones: ['aisle', 'centre'],
|
||
o: { pool: POOLS.pub_seating } },
|
||
{ kind: 'kitFloor', zone: 'wall', min: 3, max: 5, priority: 2, fallbackZones: ['aisle'],
|
||
o: { pool: POOLS.rsl_floor } },
|
||
],
|
||
stockKind: 'snacks',
|
||
signFlavour: ['MEMBERS & GUESTS', 'BISTRO OPEN', 'BINGO THURS', 'MEAT RAFFLE 7PM', 'LEST WE FORGET'],
|
||
},
|
||
};
|
||
|
||
// 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);
|
||
}
|
||
}
|