John looked at R41's proof shots and named three faults. All three are fixed, at zero draw cost, and every lane corrected somebody's numbers on the way — including its own. E-LOOK (§42.1+§42.2a) — THE PREMISE WAS WRONG. "The pub props have no texture" (my brief, quoting E's own R41 note) is false: all 37 carry COLOR_0 and three.js renders it. The colour was BLEACHED — jukebox at saturation 0.02, 15 assets under 0.05; what the source library called colour was baked AO. So the fix was a palette imposed on colour already present, in place: 46 assets, draw delta +0 and tri delta +0 on every one, proved the honest way — the concatenated POSITION/NORMAL/TEXCOORD/index bytes are sha1-identical to R41, so footprints and origins provably did not move. (A first-pass bbox check ignored node rotations and "found" 3 phantom violations; thrown out for the byte comparison.) The banquette was 1990s office teal and went burgundy; the beer umbrella was white and went brewery green. THE DECK, diagnosed before being touched: 7 greys, chassis 0.05 luminance on a 0.033 bench = 1.21:1, and the one high-contrast feature was INSIDE-OUT (white ring around a dark disc; a turntable is a dark record on a light platter). Now Technics silver at 6.74:1, black vinyl, red start/stop. Node contract re-read from the shipped GLB: 4/4 + 1/1 PASS. Filed to R43, not fixed: deck_1200_rigged has NO PLINTH (bottom 43mm of 119mm lost to R41 join damage). E-CAST (§42.3) — 19 of 24 ship. Rejected: comical_luchador_01 (Mixamo Ch43, purple mask reading EL CHUPACABRA, identified from the file's own material name), a 3-heads-tall cartoon child, a robed wuxia elder, a WW2 infantryman — and dj_phrtt_01, AN UNCLOTHED BODY whose only texture is a bare-skin atlas, which was in the DEFAULT crowd and live on the depot. It is the R41 deny-list's failure class through a different door: an innocent name from another pipeline. Only rendering all 24 and LOOKING found it. Ruling 2 (prefer CC0) was REFUSED ON MEASUREMENT: Quaternius is 25 nodes / 23 joints and ZERO of 64 canonical tracks bind — it would have loaded silently and never animated. Replacements came from Mixamo's Ch## pool, filtered to the contract-clean 65-bone family (candidates ran 65..99 bones; named characters carry facial/hair bones). THE LICENCE GAP WAS UNREAD, NOT MISSING: every source FBX carries ApplicationVendor "Mixamo, Inc." internally. Adobe royalty-free, no attribution owed. B's amber rows are now real. B (§42.5) — MY ASSIGNED FIX WAS USELESS AND B PROVED IT. document.fonts.ready shipped, then measured: the atlas canvases are already byte-identical across boots, the game ships NO webfonts (document.fonts.size === 0), and ready fires 50ms BEFORE the first fillText. The real cause of unreproducible bookmarks is the wall-clock CROWD — 983 changed px of 921,600, ALL in the pedestrian band; 0 px with the crowd out. New DBG.freeze() takes stable bookmarks 2/5 -> 5/5. Also found: ?pop=0 boots 140 citizens (0 is falsy against the default). THE BENCH: D's filing correct on both counts. Front-vs-road 90.0deg -> 0.0deg, min=med=max over 60/60 instances; the streetlight arm now reaches 1.05m over the road where it reached 0.00. Both used a base yaw that pointed +Z down the street while their own comments claimed otherwise — the side term was already right, which is why it survived 41 rounds of reading. Also: on any box with CDN access 9 of 60 benches are the depot GLB, 90deg out from the house law — normalised at the geometry, once, at load. benchStops(plan) exported as one truth. C (§42.2b) — 122 vs 123 WAS THE CAMERA ASPECT, and nobody was wrong. Draw counts are post-frustum-cull and the sweep camera inherits the window aspect: 1.25->116, 1.60->122 (C's harness), 1.778->123 (F's), 2.37->124. Pinned MEASURE_ASPECT=16/9 with a 1.25 control. E's "keep the bench dark" advice was measured and declined with reason (deck-vs-bench was already 4.73:1, over threshold). "Reads as a black box" is a claim about pixels, so C measured pixels: near-black in the booth crop 57.5% -> 26.7%, distinct colours 228 -> 389. Bench rebuilt 3->7 boxes with an open record bay; headphones on the mixer; faceDoor aims booths at the player's entry — alignment within 45deg went 49% -> 92%. Two bugs found by looking: a fitting could be planted INSIDE A WALL (0.77m of a 0.86m half-depth in plaster), and every booth part sat 25mm sunk into its own worktop. D (§42.4) — five swaps IN PLACE at the same index (pickRig indexes these arrays and pedIndex is in the identity signature and the impostor atlas; removal would renumber the town). Identity signature pre- vs post-swap: 0 differing lines of 150 — who each citizen is did not move, only which body they wear. dj_phrtt_01 verified gone AT THE WIRE, not from source: a fresh default boot fetches 32 ped GLBs, none retired. THE CONTROL ARM FOUND A 41-ROUND-OLD BUG: the ped being RETIRED, comical_boy_01, binds only 3,132 of 3,460 tracks — six thumb bones lack the mixamorig prefix. It had been silently dropping six tracks per clip since it shipped. Testing the thing being thrown away found it. Imported B's benchStops mid-round: sitters now 0.0deg to the road, signatures byte-identical across the rewire. Corrected F's filing: the browse smoke was flaky-by-construction (a 16-25s race against a live sim), not deterministically red. F (§42.6) — qa.sh --strict 24 passed / 0 failed / 0 warn / 0 skipped. New Gate 11: period law (0/5 banned; CONTROL: R41's roster trips all five) and roster licence (24/24, 0 unverified; CONTROL: an amber row turns 20 red). r41_shots.py promoted warn -> strict. R41'S BYTE-HASH BAN IS REVERSED AND ITS STATED REASON WITHDRAWN AS WRONG. A gate may pin a hash under five in-run assertions (classic+flags boot, freeze().stillLive all-false, a second freeze removing 0 draws, settle until DBG.info() stops changing, hash with the dbg panel hidden): 5/5 bookmarks now pin to one sha256 over 3 fresh boots. Two findings got it there — the chunk streamer (a fixed settle is a bet on load) and the fps readout, which with everything else frozen was the last thing moving at 53-62 px. ?pop=0 fixed (Number.isFinite) — a no-op for 35 rounds. Three red flags_check gates fixed. Street: noon 282 · NIGHT 291/300 — the nine-draw margin INTACT, every R41 row reproducing to the digit. Interior at pinned 16:9: 123 @ dept/auto, margin 227. selfcheck 157,647/157,647, 0x5f76e76 unmoved. No tag — John's playtest rules the epoch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
445 lines
29 KiB
JavaScript
445 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, faceDoor: true,
|
||
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, faceDoor: true,
|
||
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")
|
||
// [R42 §42.2b] `faceDoor` + the fallback ORDER. `stage` used to be first, and the stage flanks are
|
||
// exactly where the PA lives — measured on pub/auto/1990, the booth landed 0.24 m from a 1.8 m
|
||
// `speaker_stack` that hid it completely from the door. A DJ rig beside the PA is right in theory
|
||
// and unreadable in practice, so the open floor is tried first and the stage flanks kept as a
|
||
// fallback rather than a preference.
|
||
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 4, gap: 2.2, faceDoor: true,
|
||
fallbackZones: ['aisle', 'centre', 'stage', 'wall'], 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, faceDoor: true,
|
||
fallbackZones: ['aisle', 'centre', 'stage', 'wall'], o: { booth: 'venue' } }, // [R42] see the pub's note
|
||
],
|
||
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);
|
||
}
|
||
}
|