// PROCITY shop-type registry — the shared vocabulary (CITY_SPEC §"Shop types v1"). // Lane A owns this file; EVERY other lane imports it read-only. Keep it flat data: // no THREE, no prng, no side effects — just tables + tiny pure helpers. // // Facade filenames here MUST exist in web/assets/gen/ (Lane A selfcheck asserts this). // ── enumerations (single source of truth for the CityPlan schema strings) ────────── export const DISTRICT_KINDS = ['mainstreet', 'arcade', 'backstreets', 'warehouse', 'residential', 'market']; export const EDGE_KINDS = ['main', 'side', 'lane', 'arcade']; export const USE_KINDS = ['shop', 'anchor', 'house', 'yard', 'stall', 'infill']; export const SHOP_TYPE_IDS = ['record', 'opshop', 'toy', 'book', 'video', 'pawn', 'milkbar', 'dept', 'stall', 'pub', 'band_room', 'rsl']; // ── v3 venue kinds (?gigs=1) ─────────────────────────────────────────────────────── // The three gig venues. All three are registry shop types with NO district weights, so the base // generator never places them; the gig layer converts a chosen shop into one, in place. // `genre` is the single source of truth for the kind→genre map (ROUND13 §A1). The audio bed key is // ALWAYS `gig-` — see gigKeyFor() in citygen/gigs.js; the manifest key IS the gigKey, there is // no mapping table anywhere (ROUND13 debt #1). export const VENUE_KINDS = ['pub', 'band_room', 'rsl']; // ── the registry ────────────────────────────────────────────────────────────────── // Per type: // label human name (map legend / debug) // facades facade-*.jpg skins in web/assets/gen/ (blank signboards; game overlays the name) // sign colour/style hints for Lane B's canvas signage // interior archetype id Lane C builds the room from // fittings parametric fitting kit ids (90sDJsim fittings.js) Lane C fills the room with // storeys [min,max] inclusive // hours {open,close} default trading hours (24h); plan.js jitters + picks one weirdo // weights district-kind -> spawn weight (0/absent = never in that district) // footprint {fmin,fmax,dmin,dmax} preferred frontage/depth band in metres (plan.js hint) export const SHOP_TYPES = { record: { label: 'Record Shop', facades: ['facade-timber-teal.jpg', 'facade-arcade-tile.jpg', 'facade-djsim-record.jpg'], sign: { bg: '#141428', fg: '#ff5470', accent: '#ffd23f', style: 'neon' }, interior: 'crates_and_bins', fittings: ['record_bins', 'crates', 'counter', 'listening_corner'], storeys: [1, 2], hours: { open: 10, close: 18 }, weights: { mainstreet: 3, arcade: 4, backstreets: 1 }, footprint: { fmin: 6, fmax: 9, dmin: 12, dmax: 18 }, }, opshop: { label: 'Op Shop', facades: ['facade-weatherboard.jpg', 'facade-fibro-blue.jpg', 'facade-djsim-opshop.jpg'], sign: { bg: '#3d6b4a', fg: '#f6efdc', accent: '#e4573a', style: 'painted' }, interior: 'racks_and_shelves', fittings: ['clothes_racks', 'bricabrac_shelving', 'book_wall', 'counter'], storeys: [1, 2], hours: { open: 9, close: 17 }, weights: { mainstreet: 3, arcade: 1, backstreets: 2 }, footprint: { fmin: 7, fmax: 10, dmin: 12, dmax: 20 }, }, toy: { label: 'Toy Shop', facades: ['facade-stucco-pink.jpg', 'facade-deco-pastel.jpg', 'facade-boutique.jpg'], sign: { bg: '#ffd23f', fg: '#d6336c', accent: '#2f9e44', style: 'bubbly' }, interior: 'shelves_and_case', fittings: ['cube_shelves', 'display_tables', 'glass_case', 'counter'], storeys: [1, 2], hours: { open: 9, close: 17 }, weights: { mainstreet: 2, arcade: 2, backstreets: 1 }, footprint: { fmin: 6, fmax: 9, dmin: 12, dmax: 18 }, }, book: { label: 'Book Barn', facades: ['facade-federation.jpg', 'facade-sandstone.jpg', 'facade-redbrick.jpg'], sign: { bg: '#2b3a2e', fg: '#e9d8a6', accent: '#bb7a3b', style: 'serif' }, interior: 'halls_of_shelves', fittings: ['bookshelves', 'spinner_racks', 'armchair', 'counter'], storeys: [1, 2], hours: { open: 9, close: 17 }, weights: { mainstreet: 2, arcade: 2, backstreets: 1 }, footprint: { fmin: 7, fmax: 10, dmin: 14, dmax: 20 }, }, video: { label: 'Video Rental', facades: ['facade-stripmall.jpg', 'facade-djsim-video.jpg'], sign: { bg: '#111111', fg: '#ffe14d', accent: '#e8412b', style: 'blocky' }, interior: 'aisle_shelves', fittings: ['vhs_shelving', 'returns_slot', 'poster_wall', 'counter'], storeys: [1, 1], hours: { open: 11, close: 21 }, // rentals trade late weights: { mainstreet: 2, arcade: 0, backstreets: 1 }, footprint: { fmin: 7, fmax: 10, dmin: 14, dmax: 20 }, }, pawn: { label: 'Pawnbroker', facades: ['facade-besser.jpg', 'facade-grimy.jpg', 'facade-djsim-pawn.jpg', 'facade-corrugated.jpg'], sign: { bg: '#1c1c1c', fg: '#f4c430', accent: '#a8a8a8', style: 'goldbar' }, interior: 'counter_forward', fittings: ['glass_cabinets', 'wall_hooks', 'barred_window', 'counter'], storeys: [1, 2], hours: { open: 9, close: 17 }, weights: { mainstreet: 1, backstreets: 2, warehouse: 1 }, footprint: { fmin: 6, fmax: 9, dmin: 12, dmax: 18 }, }, milkbar: { label: 'Milk Bar', facades: ['facade-djsim-milkbar.jpg', 'facade-brickveneer.jpg'], sign: { bg: '#c92a2a', fg: '#fff5f5', accent: '#ffd23f', style: 'coke' }, interior: 'counter_and_freezer', fittings: ['counter', 'fridge', 'magazine_rack', 'freezer'], storeys: [1, 1], hours: { open: 6, close: 20 }, // the corner shop opens early, closes late weights: { residential: 1 }, // only ever a residential corner shop footprint: { fmin: 9, fmax: 12, dmin: 12, dmax: 16 }, }, dept: { label: 'Department Store', facades: ['facade-terrazzo.jpg', 'facade-djsim-dept.jpg'], sign: { bg: '#7b1e3a', fg: '#f6e7c1', accent: '#c9a227', style: 'grand' }, interior: 'grand_hall', fittings: ['mixed_sections', 'escalator_prop', 'glass_case', 'counter'], storeys: [2, 3], hours: { open: 9, close: 17 }, weights: { market: 1 }, // the anchor on the market square's edge footprint: { fmin: 22, fmax: 34, dmin: 24, dmax: 34 }, }, stall: { label: 'Market Stall', facades: ['facade-market.jpg', 'facade-corrugated.jpg'], // canvas market + corrugated shed backdrop (≥2 per Lane E) sign: { bg: '#e8e2d0', fg: '#5c3b1e', accent: '#c0392b', style: 'handpainted' }, interior: 'open_stall', fittings: ['trestle_tables', 'crates'], storeys: [1, 1], hours: { open: 8, close: 14 }, // market mornings weights: { market: 1 }, footprint: { fmin: 3, fmax: 5, dmin: 3, dmax: 5 }, }, pub: { label: 'Pub', facades: ['facade-djsim-pub.jpg', 'facade-federation.jpg'], // corner hotel; both exist in web/assets/gen/ sign: { bg: '#3a2416', fg: '#f2d98a', accent: '#c0392b', style: 'gilt' }, // `interior` is the Lane C RECIPE key, which C resolves from shop.type (theme.js canonicalType) — // so it must equal the type id. R12 shipped 'band_room' here, which was harmless only because C // keys off shop.type and nothing reads this field; now that `band_room` is itself a venue type it // would be an outright collision, so it's corrected to 'pub' (Lane A amendment, ROUND13). interior: 'pub', // Lane C pub archetype (stage + PA + bar) fittings: ['bar_counter', 'stage', 'pa_stack', 'pub_tables', 'stools'], storeys: [1, 2], hours: { open: 17, close: 23 }, // a NIGHT venue: open into NIGHT so gigs can run (gig layer sets these) weights: {}, // NEVER auto-placed — the ?gigs=1 layer converts one shop into the venue footprint: { fmin: 16, fmax: 24, dmin: 18, dmax: 26 }, venueKind: 'pub', genre: 'pubrock', // ⇒ audio bed key 'gig-pubrock' placement: 'spine_end', // a destination you follow the sound to (gigs.js pickVenues) }, band_room: { label: 'Band Room', // A tin shed on the warehouse fringe: sticky floor, low ceiling, PA louder than the room. facades: ['facade-warehouse-tin.jpg', 'facade-besser.jpg', 'facade-corrugated.jpg'], sign: { bg: '#181818', fg: '#e8e6e1', accent: '#7ac74f', style: 'stencil' }, interior: 'band_room', fittings: ['bar_counter', 'stage', 'pa_stack', 'stools'], // no tables — it's a standing room storeys: [1, 1], hours: { open: 18, close: 23 }, weights: {}, footprint: { fmin: 18, fmax: 28, dmin: 20, dmax: 30 }, venueKind: 'band_room', genre: 'grunge', // ⇒ 'gig-grunge' (John may re-flavour — names only) placement: 'warehouse_fringe', }, rsl: { label: 'RSL Club', // Carpet, bistro, a stage at one end, and the biggest crowd cap in town. facades: ['facade-brickveneer.jpg', 'facade-redbrick.jpg'], sign: { bg: '#0f2d4a', fg: '#f6e7c1', accent: '#c9a227', style: 'club' }, interior: 'rsl', fittings: ['bar_counter', 'stage', 'pa_stack', 'bistro_tables', 'stools'], storeys: [1, 2], hours: { open: 16, close: 23 }, // opens early — the bistro trades before the band weights: {}, footprint: { fmin: 20, fmax: 30, dmin: 22, dmax: 32 }, venueKind: 'rsl', genre: 'covers', // ⇒ 'gig-covers' placement: 'off_spine', }, }; // ── pure helpers (no randomness — plan.js supplies the prng stream) ───────────────── // Types that can spawn in a district, as a flat [{type,weight}] list (weight>0), stable order. export function weightedTypes(districtKind) { const out = []; for (const type of SHOP_TYPE_IDS) { const w = SHOP_TYPES[type].weights[districtKind] || 0; if (w > 0) out.push({ type, weight: w }); } return out; } // Sample one type by weight using a prng stream fn r()->[0,1). Returns null if none for district. export function pickWeightedType(r, districtKind, exclude = null) { let list = weightedTypes(districtKind); if (exclude) { const f = list.filter(t => t.type !== exclude); if (f.length) list = f; } if (!list.length) return null; let total = 0; for (const t of list) total += t.weight; let x = r() * total; for (const t of list) { x -= t.weight; if (x < 0) return t.type; } return list[list.length - 1].type; } // The genre a venue kind plays (ROUND13 §A1 kind→genre map). Unknown kind ⇒ null. export function genreForVenueKind(kind) { const reg = SHOP_TYPES[kind]; return (reg && reg.genre) || null; } // ── street corridor law (Lane A publishes it as a FUNCTION, not plan data) ────────── // `edge.width` is the full CORRIDOR — carriageway + verge/footpath, centreline ± width/2 — as // CITY_SPEC states twice ("road corridor (road + verge, centreline ± width/2)") and as chunkIndex // buckets. It is NOT the carriageway width. The plan never carried the split, so consumers had to // guess it; these helpers are the split, published so nobody guesses again (ROUND13 debt #5). // Pure functions over an existing edge ⇒ no plan fields change ⇒ base goldens stay frozen. export function roadWidth(edge) { const w = (edge && edge.width) || 0; switch (edge && edge.kind) { case 'main': return 10; // 28m corridor = 10m carriageway + two ~9m verandah'd footpaths case 'side': return w >= 18 ? 8 : 6; // the two central rungs are second high streets (wider) case 'lane': return w; // a service laneway is all carriageway — no footpath case 'arcade': return 0; // covered pedestrian lane — no carriageway at all default: return Math.min(10, w); } } // The band, as distance from the centreline, where footpath furniture lives: [kerb, corridor edge]. // Street furniture, poles, verandah posts and gig posters belong in here — never inside `kerb`. export function vergeBand(edge) { const inner = roadWidth(edge) / 2; const outer = ((edge && edge.width) || 0) / 2; return [inner, Math.max(inner, outer)]; } // Where a pole-mounted thing (poster, sign) seats: just behind the kerb, clamped into the verge. export function poleOffset(edge, clear = 1) { const [inner, outer] = vergeBand(edge); return outer <= inner ? inner : Math.min(inner + clear, outer - 0.25); } // Every facade filename referenced anywhere (Lane A selfcheck asserts each exists on disk). export function allFacadeSkins() { const set = new Set(); for (const type of SHOP_TYPE_IDS) for (const f of SHOP_TYPES[type].facades) set.add(f); return [...set]; }