// 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); 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', ], }; // ── 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'], }; // 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: 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(); }