Publishes the gig interface the whole round hangs off (A runs first). Everything is
behind ?gigs=1 as a POST-HOC augmentation (gigs.js withGigs, applied by the selector),
so generatePlan is untouched and all v2 goldens stay frozen (synthetic 0x3fa36874, osm
melbourne 0x34cfdec0, katoomba 0x0f652510). Source-agnostic (synthetic + osm).
- Venue: one 'pub' per town — a spine-end plain shop converted in place (keeps lot
id/geometry, enterable), shop.venue=true, never the openLate landmark. Registry gains
a weight-0 'pub' type (interior:'band_room' for Lane C) → never auto-placed → base
plan byte-identical.
- plan.gigs: 7 nightly {gigId,venueShopId,bandName,genreKey:'pubrock',night,startSeg:5,
endSeg:5,cover}; cover ~half free / half $2-$10 (F charges at the door).
- Band names: bandName(seed) generator (90s pub-rock) + OPTIONAL web/assets/custom_bands.json
drop-in (priority into the pool; absent → pure generator, no fetch under ?noassets).
- plan.posters: ~6 seeded {id,gigId,x,z,ry} for Lane B.
- Closing-time debt paid: isOpen(shopOrHours,hour) exported (half-open law as code, D consumes).
- map.html?gigs=1: venue ring + poster pins + gig panel line. Flags-off unchanged.
Self-check ALL GREEN 3074/3074 (gigSuite ×2 sources, flags-off≡base, gig golden
0xa6ae5a5e). qa.sh --strict GREEN 6/6. Verified in-browser both modes. CITY_SPEC v3
section + LANE_A_NOTES handshake for C/D/B/F. Atomic pathspec commit (shared-tree race).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
166 lines
7.9 KiB
JavaScript
166 lines
7.9 KiB
JavaScript
// 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'];
|
|
|
|
// ── 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: 'band_room', // 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 },
|
|
},
|
|
};
|
|
|
|
// ── 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;
|
|
}
|
|
|
|
// 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];
|
|
}
|