Publishes the multi-venue plan.gigs interface C/D/B/F hang off (half-day handshake, A first). - pickVenues: seeded 2–4 shops converted in place → pub (spine end) / band_room (warehouse fringe) / rsl (off-spine); 4th = 2nd pub at far end. Registry gains weight-0 band_room+rsl kinds + venueKind/genreKey/placement. Never the openLate landmark; osm-safe fallbacks. - gigKey contract (debt #1): gigKeyFor(genreKey) => 'gig-'+genreKey, the single exported genre→bed mapping. No table anywhere; F deletes the GIG_BED bridge. - plan.gigs goes district: 7 nights × every venue, each plays a seeded 4–6 (dark reads true); cover skewed by kind; no band plays two venues on one night; custom names spread night-major. - plan.posters goes town-wide: tonight's playing venues each get frontage + seeded spine run. - Verge fix (debt #5): new corridor law roadWidth/vergeBand/poleOffset(edge) in registry.js (road-vs-verge split of edge.width, published as functions ⇒ base goldens frozen). Posters seat on the verge, never bitumen; selfcheck asserts no poster on a carriageway. - registry pub.interior 'band_room'→'pub' (unread field; avoids collision with band_room type). Gig golden re-pinned 0xa6ae5a5e→0x1f636349 (multi-venue changed output; in-commit per amendment law). selfcheck ALL GREEN 3273/3273; scaffold_check + consistency GREEN; flags-off byte-identical. CITY_SPEC §v3.0-beta + LANE_A_NOTES §Round 13 updated in this commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
147 lines
7.0 KiB
JavaScript
147 lines
7.0 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_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();
|
||
}
|