// 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_room: [ // v3 venue (ROUND13): the tin shed on the warehouse fringe 'The {Adj} Warehouse', '{Suburb} Band Room', 'The {Animal} Room', 'The {Word} Shed', 'The Tote-ish', 'The {Adj} Room', '{Word} Rehearsal Rooms', ], rsl: [ // v3 venue (ROUND13): the returned & services league club — carpet, bistro, meat raffle '{Suburb} RSL Club', '{Suburb} RSL', '{Suburb} Services Club', 'The {Suburb} Memorial Club', '{Suburb} Workers Club', '{Suburb} Returned Servicemen’s Club', ], }; // ── 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'], band_room: ['BAND ROOM', 'LIVE MUSIC', '{Word} ROOM', 'THE SHED'], rsl: ['RSL', '{Suburb} RSL', 'SERVICES CLUB', 'MEMORIAL CLUB'], }; // 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(); }