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>
137 lines
6.3 KiB
JavaScript
137 lines
6.3 KiB
JavaScript
// PROCITY shop & town name generator — seeded, deterministic, 90s-Australian flavour.
|
||
// Given a shop's stable seed, produces the same {name, sign} forever. `sign` is the short form
|
||
// that fits a signboard (the game renders it big; `name` is the full directory/label string).
|
||
//
|
||
// Pure: only imports prng (mulberry32) + wordlists. No THREE, no network, no Math.random.
|
||
|
||
import { mulberry32 } from '../core/prng.js';
|
||
import * as W from './wordlists.js';
|
||
|
||
const pick = (r, arr) => arr[(r() * arr.length) | 0];
|
||
const cap = s => s.charAt(0).toUpperCase() + s.slice(1);
|
||
const initials = (r) => pick(r, ['A', 'B', 'C', 'D', 'E', 'G', 'J', 'K', 'M', 'P', 'R', 'S', 'T', 'V'])
|
||
+ ' & ' + pick(r, ['A', 'B', 'C', 'D', 'E', 'G', 'J', 'K', 'M', 'P', 'R', 'S', 'T', 'V']);
|
||
|
||
// Token → wordlist resolver. Each token draws from the bank with the given stream.
|
||
function tok(r, t) {
|
||
switch (t) {
|
||
case 'Word': return pick(r, W.WORDS);
|
||
case 'Adj': return pick(r, W.ADJ);
|
||
case 'Noun': return pick(r, W.NOUN);
|
||
case 'Suburb': return pick(r, W.SUBURB);
|
||
case 'Family': return pick(r, W.FAMILY);
|
||
case 'Saint': return pick(r, W.SAINT);
|
||
case 'Charity': return pick(r, W.CHARITY);
|
||
case 'First': return pick(r, W.FIRSTNAME);
|
||
case 'Animal': return pick(r, W.ANIMAL);
|
||
case 'Init': return initials(r);
|
||
case 'Pub': return pick(r, W.PUB_NAME); // v3: full pub name ("The Royal")
|
||
case 'BandAdj': return pick(r, W.BAND_ADJ); // v3: band-name banks
|
||
case 'BandNoun':return pick(r, W.BAND_NOUN);
|
||
case 'BandOne': return pick(r, W.BAND_ONEWORD);
|
||
default: return t;
|
||
}
|
||
}
|
||
|
||
// Fill "{Adj} {Noun}" style token patterns. Possessives are written with a literal ’s in the
|
||
// pattern itself (e.g. "{First}’s Records", "{Saint}’s Charity Barn") — one mechanism, no magic.
|
||
function fill(r, pattern) {
|
||
return pattern.replace(/\{(\w+)\}/g, (_, t) => tok(r, t));
|
||
}
|
||
|
||
// ── name patterns per shop type (≥40 across the board) ─────────────────────────────
|
||
const PATTERNS = {
|
||
record: [
|
||
'{Word} Records', '{Suburb} Sound Exchange', '{Adj} Groove', '{Word} & Wax',
|
||
'Rotation', '{First}’s Records', 'The {Animal} Sound Co.', '{Suburb} Vinyl',
|
||
],
|
||
opshop: [
|
||
'St {Saint}’s Opportunity Shop', '{Charity} Op Shop', 'The {Adj} Op Shop',
|
||
'{Suburb} Recycled', '{Saint}’s Charity Barn', 'Second Time {Suburb}', 'The Op Shop',
|
||
],
|
||
toy: [
|
||
'{First}’s Toys', 'The {Adj} Toy Box', '{Word} Toys & Games', '{Animal} Playthings',
|
||
'{Suburb} Toyworld-ish', 'Marbles & Kites', 'The Toy Nook',
|
||
],
|
||
book: [
|
||
'The {Adj} Book Barn', '{Suburb} Books', '{Word} & Sons Booksellers', 'The {Animal} Bookshop',
|
||
'Dusty Pages', '{Init} Books', 'Second-Hand Reads', 'The Book Cellar',
|
||
],
|
||
video: [
|
||
'{Suburb} Video', '{Adj} Video', 'Video {Word}', '{First}’s Video',
|
||
'The Picture Show', 'Late Nite Video', 'Video Barn',
|
||
],
|
||
pawn: [
|
||
'{Suburb} Cash Converters-ish', '{First}’s Pawn & Loan', '{Adj} Cash', 'The Cash Corner',
|
||
'{Init} Trading Co.', 'Quick Cash Pawn', 'Second Chance Loans',
|
||
],
|
||
milkbar: [
|
||
'{Family} Milk Bar', '{Suburb} Milk Bar', 'The {Adj} Corner Store', '{First}’s Milk Bar',
|
||
'{Family}’s Deli & Milk Bar', 'Corner Store', 'The Milk Bar',
|
||
],
|
||
dept: [
|
||
'{Suburb} Emporium', '{Family} & Co.', 'The Grand {Word}', '{Adj} Department Store',
|
||
'{Suburb} Trading Co.', 'Palais {Word}',
|
||
],
|
||
stall: [
|
||
'{First}’s Stall', '{Word} Stall', '{Adj} Bits', 'Trash & Treasure', '{Animal} Wares',
|
||
'Bric-a-Brac', 'Sunday Stall',
|
||
],
|
||
pub: [ // v3 venue (?gigs=1): the town pub/hotel
|
||
'{Pub}', 'The {Suburb} Hotel', 'The {Family} Hotel', 'The {Adj} Arms', '{Pub}',
|
||
],
|
||
};
|
||
|
||
// ── band-name patterns (v3 gigs, 90s Aussie pub-rock) ──────────────────────────────
|
||
const BAND_PATTERNS = [
|
||
'The {BandAdj} {BandNoun}', '{BandAdj} {BandNoun}', 'The {BandNoun}', '{BandOne}',
|
||
'{First} & The {BandNoun}', 'The {Animal}s', '{BandAdj} {Animal}', 'The {BandOne}s',
|
||
];
|
||
|
||
// ── short signboard forms (what actually fits above the door) ──────────────────────
|
||
const SIGN_PATTERNS = {
|
||
record: ['{Word}', 'RECORDS', 'VINYL', '{Suburb} SOUND'],
|
||
opshop: ['OP SHOP', 'ST {Saint}’S', '{Charity}', 'RECYCLED'],
|
||
toy: ['TOYS', '{Adj} TOYS', 'TOY BOX', 'GAMES'],
|
||
book: ['BOOKS', 'BOOK BARN', '{Suburb} BOOKS', 'READS'],
|
||
video: ['VIDEO', '{Suburb} VIDEO', 'MOVIES', 'RENTALS'],
|
||
pawn: ['CASH', 'PAWN', 'LOANS', '{Init}'],
|
||
milkbar: ['MILK BAR', '{Family}', 'DELI', 'CORNER STORE'],
|
||
dept: ['{Suburb}', 'EMPORIUM', '{Family} & CO'],
|
||
stall: ['STALL', 'BITS', 'BRIC-A-BRAC', 'WARES'],
|
||
pub: ['HOTEL', 'THE ROYAL', '{Suburb} HOTEL', 'PUB'],
|
||
};
|
||
|
||
// Public: name a shop from its stable seed + type. Returns { name, sign }.
|
||
export function shopName(seed, type) {
|
||
const r = mulberry32((seed ^ 0x9e3779b9) >>> 0); // decorrelate from other seed uses
|
||
const pats = PATTERNS[type] || PATTERNS.stall;
|
||
const signs = SIGN_PATTERNS[type] || SIGN_PATTERNS.stall;
|
||
const name = fill(r, pick(r, pats)).replace(/\s+/g, ' ').trim();
|
||
let sign = fill(r, pick(r, signs)).replace(/\s+/g, ' ').trim();
|
||
// Signboard can't be too long — fall back to a punchy word from the full name.
|
||
if (sign.length > 16) {
|
||
const word = name.split(' ').sort((a, b) => b.length - a.length)[0] || sign;
|
||
sign = word.slice(0, 16).toUpperCase();
|
||
}
|
||
return { name, sign };
|
||
}
|
||
|
||
// Public (v3 gigs): generate a band name from a stable seed. Pure — the OPTIONAL custom-name
|
||
// drop-in is mixed in by gigs.js (which calls this only when it needs a fresh generated name), so
|
||
// this stays node-testable and fetch-free. Deterministic: same seed ⇒ same name, forever.
|
||
export function bandName(seed) {
|
||
const r = mulberry32((seed ^ 0x1b56c4e9) >>> 0); // decorrelate from shop/town seed uses
|
||
return fill(r, pick(r, BAND_PATTERNS)).replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
// Public: name the town from the city seed.
|
||
export function townName(citySeed) {
|
||
const r = mulberry32((citySeed ^ 0x2545f491) >>> 0);
|
||
const root = pick(r, W.TOWN_ROOT);
|
||
const suffix = pick(r, W.TOWN_SUFFIX);
|
||
const usePrefix = r() < 0.35;
|
||
const prefix = usePrefix ? pick(r, W.TOWN_PREFIX) + ' ' : '';
|
||
return `${prefix}${cap(root)}${suffix ? ' ' + suffix : ''}`.trim();
|
||
}
|