PROCITY/web/js/citygen/names.js
m3ultra acf734a1e5 Lane A round 12: v3.0-alpha gig layer (?gigs=1) — venue, plan.gigs, band names, closing-time debt
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>
2026-07-15 18:33:53 +10:00

137 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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();
}