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>
This commit is contained in:
parent
22a0741ad1
commit
acf734a1e5
@ -1,9 +1,47 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (1727/1727); rounds 2–9 closed** (see below).
|
||||
`qa.sh --strict` GREEN.*
|
||||
*Status: **all deliverables landed; self-check all-green (3074/3074); rounds 2–12 closed** (see below).
|
||||
`qa.sh --strict` GREEN. v3.0-alpha gig interface published.*
|
||||
*Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 12 (2026-07-15) — v3.0-alpha GIG LAYER (`?gigs=1`): venue + gigs + band names + closing-time debt
|
||||
|
||||
Lane A ran **first** this round (the whole round hangs off the `plan.gigs` handshake). Everything is
|
||||
behind `?gigs=1` (prime flag law): **flags-off is byte-identical — all v2 goldens frozen** (synthetic
|
||||
`0x3fa36874`, osm melbourne `0x34cfdec0`, katoomba `0x0f652510`). Self-check **ALL GREEN 3074/3074**
|
||||
(+ the whole gig suite ×2 sources), `qa.sh --strict` GREEN (6/6). Verified in-browser both modes.
|
||||
|
||||
**Architecture — a post-hoc augmentation layer, not a generator change.** `web/js/citygen/gigs.js`
|
||||
`withGigs(plan, seed, opts)` takes a finished plan and adds the venue + gigs + posters; the selector
|
||||
(`index.js generatePlanFor`) applies it **only** when `opts.gigs`. So `generatePlan` is untouched
|
||||
(goldens can't move) and the layer is source-agnostic (synthetic + osm both get gigs).
|
||||
|
||||
Delivered (all behind the flag):
|
||||
1. **Venue** — one `pub` per town: a spine-END plain shop converted in place to `type:'pub'` (keeps lot
|
||||
id/geometry ⇒ enterable, no overlap), `shop.venue=true`, hours `[17,23]`. **Never** the openLate shop.
|
||||
Registry gains a weight-0 `pub` type (facades `djsim-pub`+`federation`, `interior:'band_room'` for
|
||||
Lane C, fittings). Weight-0 ⇒ never auto-placed ⇒ base plan unchanged.
|
||||
2. **`plan.gigs`** — 7 nightly entries `{gigId, venueShopId, bandName, genreKey:'pubrock', night, startSeg:5,
|
||||
endSeg:5, cover}`; `cover` seeded ~half free / half `$2–$10`. F picks tonight's + charges at the door.
|
||||
3. **Band names** — `bandName(seed)` generator (90s Aussie pub-rock wordlists) **+ the drop-in**
|
||||
`web/assets/custom_bands.json` (`{bands:[…]}`, ships with 10 examples incl. "The Feral Galahs"):
|
||||
present → priority into the pool (up to 7 surface); absent → pure generator, zero fetch (`?noassets`).
|
||||
Deterministic given (seed, file). The shell (F) does the async load and passes `customBands` in.
|
||||
4. **`plan.posters`** — ~6 seeded poster placements `{id, gigId, x, z, ry}` (venue + spine) for Lane B.
|
||||
5. **Closing-time debt PAID** — `isOpen(shopOrHours, hour)` exported from the barrel: the canonical
|
||||
half-open law (`open ≤ h < close`) as code, for D's gig-night patronage surge (was prose in round 9).
|
||||
6. **Debug map** — `map.html?gigs=1` shows the venue ring + "♪ PUB" label, poster pins, and a panel gig
|
||||
line ("♪ The Thornbury Hotel · tonight: Screaming Utes · $10 cover · 7 nights · 6 posters"). Loads the
|
||||
band drop-in for the view. Flags-off map is unchanged (adds a benign `pub (0)` legend row).
|
||||
|
||||
New self-check coverage: `gigSuite` runs the full structural suite (venue-aware — the pub is exempt
|
||||
from the single-late-shop rule via `!sh.venue`, a no-op on base plans) + gig invariants (one pub, gig
|
||||
schema, cover ∈ {0}∪[2,10], band names resolved, posters ref real gigs), on synthetic + both osm towns;
|
||||
a `?gigs`-off≡base byte-identity check per seed; a custom-band priority + determinism check; and a
|
||||
**gig golden `0xa6ae5a5e`**. CITY_SPEC gained a "CityPlan v3 — gig layer" section; handshake interfaces
|
||||
for C/D/B/F are in `LANE_A_NOTES.md`. Sample seed 20261990 → venue "The Thornbury Hotel" (a converted
|
||||
spine-end pawn shop), openLate video preserved.
|
||||
|
||||
## Round 9 (2026-07-15) — freeze the v2 contract + closing-time ruling (ship round)
|
||||
|
||||
Small round; the most "done" lane makes it official.
|
||||
|
||||
@ -204,6 +204,53 @@ Interiors are **not** street geometry. Walking into a doorway triggers
|
||||
exactly where you stood. Street windows show a cheap fake (tinted glass + emissive warm glow at
|
||||
night in v1; interior-mapping shader is a stretch goal).
|
||||
|
||||
## CityPlan v3 — gig layer (`?gigs=1`) — v3.0-alpha
|
||||
|
||||
> **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector only when
|
||||
> `?gigs=1`. With the flag OFF, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan —
|
||||
> **all v2 goldens stay frozen**. So this section adds fields that exist *only on gig-on plans*; it does
|
||||
> not touch the v2 contract above. Deterministic from `(citySeed, customBands)`; works on synthetic AND
|
||||
> osm towns. Node-pure (no THREE, no runtime fetch — the shell loads the band drop-in and passes it in).
|
||||
|
||||
**Turning it on** (Lane F): `generatePlanFor(seed, source, { gigs:true, customBands }) → plan` with the
|
||||
gig fields below (else the base plan, untouched). `generatePlanFor` stays synchronous — F fetches
|
||||
`custom_bands.json` in the bootstrap and passes the array. `withGigs(plan, seed, opts)` is also exported.
|
||||
|
||||
**The venue (one `pub` per town).** A spine-END plain `shop` is converted **in place** to `type:'pub'`
|
||||
(keeps its lot id + geometry ⇒ enterable like any shop, no overlap), tagged `shop.venue === true`. It is
|
||||
**never** the openLate landmark (the video survives; the town still has exactly one openLate shop). The
|
||||
registry gains a `pub` type (facades, `interior:'band_room'`, fittings, hours `[17,23]`) with **no
|
||||
district weights** so it is never auto-placed. Venue hours `[17,23]` keep it open through DUSK+NIGHT so
|
||||
the gig runs and D's patronage surge fills it — and it drains at close per the closing-time ruling above.
|
||||
The pub is the *only* gig-plan shop allowed to close ≥22 besides the openLate video (selfcheck exempts it).
|
||||
|
||||
**`plan.gigs`** — the nightly schedule (present only on gig-on plans):
|
||||
```
|
||||
gigs: [ { gigId, venueShopId, bandName, genreKey, night, startSeg, endSeg, cover } ]
|
||||
```
|
||||
- 7 nightly entries at the one venue; `night` = 0-based index, **F selects tonight's** (alpha: `gigs[0]`).
|
||||
- `venueShopId` = the pub shop id. `genreKey = 'pubrock'` (alpha's one genre). `bandName` — see drop-in.
|
||||
- `startSeg = 5` (NIGHT / 22:00), `endSeg = 5`. F's state machine: `quiet → doors (startSeg−1 = DUSK)
|
||||
→ on → done`. (Day-segment→hour map is `lighting.js`: `0 DAWN … 5 NIGHT`.)
|
||||
- `cover` — integer: **~half `0` (free), the rest `$2–$10`**. F debits the wallet at the door; free = walk in.
|
||||
|
||||
**`plan.posters`** — gig advertising (Lane B renders): `posters: [ { id, gigId, x, z, ry } ]` (~6 —
|
||||
one on the venue frontage, a seeded few along the spine, on poles/walls B already streams). B places E's
|
||||
poster skin at `(x, z)` rotated `ry`; F wires "poster → tonight's-gig discoverability".
|
||||
|
||||
**Band names + the drop-in.** `bandName(seed)` generates 90s-Aussie pub-rock names from wordlists.
|
||||
`web/assets/custom_bands.json` is an **OPTIONAL** editable drop-in (schema `{ "bands": ["The Feral
|
||||
Galahs", …] }`, Lane-A-owned per ROUND12): present → those names get **priority** on the schedule
|
||||
(shuffled by seed, so a town surfaces up to its 7 nightly slots of them); **absent → pure generator, zero
|
||||
errors, zero fetch** (the `?noassets=1` path). Deterministic given `(seed, file contents)`. John edits the
|
||||
file and reloads.
|
||||
|
||||
**Closing-time law as code (v2 debt paid).** `isOpen(shopOrHours, hour)` is exported from the barrel — the
|
||||
canonical half-open predicate (`open ≤ hour < close`) for D's gig-night patronage surge and everyone else.
|
||||
|
||||
**Gig golden** (guards gig-layer drift; pure-generator path, no custom file), seed `20261990` synthetic:
|
||||
`0xa6ae5a5e`. Asserted by `selfcheck.js` alongside the frozen v2 goldens; changing gig output re-pins it.
|
||||
|
||||
## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read)
|
||||
|
||||
> **`web/js/core/registry.js` is the machine-readable source of truth** — import `SHOP_TYPES` for
|
||||
@ -232,6 +279,7 @@ night in v1; interior-mapping shader is a stretch goal).
|
||||
| `milkbar` | djsim-milkbar, brickveneer | counter + freezer | counter, fridge, magazine rack, freezer |
|
||||
| `dept` (anchor) | terrazzo, djsim-dept | grand hall | mixed sections, escalator prop, glass case, counter |
|
||||
| `stall` (market) | market, corrugated | open stall | trestle tables, crates |
|
||||
| `pub` (v3 venue) | djsim-pub, federation | band_room | bar counter, stage, PA stack, tables, stools |
|
||||
|
||||
## NPC contract (Lane D)
|
||||
|
||||
@ -268,6 +316,7 @@ night in v1; interior-mapping shader is a stretch goal).
|
||||
| `web/js/interiors/*`, `web/interior_test.html` | Lane C |
|
||||
| `web/js/citizens/*`, `web/citizens_test.html`, `web/models/*` | Lane D |
|
||||
| `pipeline/*`, `web/assets/*` + `web/assets/manifest.json` | Lane E |
|
||||
| `web/assets/custom_bands.json` | Lane A (v3 band drop-in — carve-out from `web/assets/*`, per ROUND12) |
|
||||
| `web/package.json` | scaffold/shared — `{"type":"module"}` only, added by Lane A |
|
||||
| `docs/*` | everyone, additively |
|
||||
|
||||
|
||||
@ -2,6 +2,60 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 12 (2026-07-15) — v3.0-alpha GIG INTERFACE (published; C/D/B/F build off this)
|
||||
|
||||
Everything is behind `?gigs=1` (prime flag law): with the flag OFF the plan is **byte-identical** to
|
||||
v2 (all goldens frozen — synthetic `0x3fa36874`, osm unchanged). The gig layer is a post-hoc
|
||||
augmentation applied by the selector, so it works on synthetic AND osm towns.
|
||||
|
||||
### How the gig layer turns on (Lane F wires this)
|
||||
```js
|
||||
// F's shell bootstrap, when ?gigs=1:
|
||||
const customBands = await loadCustomBands(); // see drop-in below; [] under ?noassets or if absent
|
||||
const plan = citygen.generatePlanFor(seed, src, { town, gigs: true, customBands });
|
||||
```
|
||||
`generatePlanFor(seed, source, { gigs:true, customBands })` → base plan + `withGigs`. Without
|
||||
`gigs:true`, the base plan is returned untouched. `generatePlanFor` stays **synchronous** — F does the
|
||||
async `custom_bands.json` fetch in the bootstrap and passes the array in.
|
||||
|
||||
### `plan.gigs` — the nightly schedule (NEW, present only when gigs on)
|
||||
```
|
||||
plan.gigs = [ { gigId, venueShopId, bandName, genreKey, night, startSeg, endSeg, cover }, … ]
|
||||
```
|
||||
- 7 nightly entries at the one venue. `night` = 0-based index; **F picks tonight's** (alpha: `gigs[0]`).
|
||||
- `venueShopId` = the pub shop's id. `genreKey` = `'pubrock'` (alpha's one genre; E ships that bed).
|
||||
- `startSeg=5` (NIGHT, 22:00) `endSeg=5`. F's state machine: `quiet → doors (startSeg−1 = DUSK seg4)
|
||||
→ on (seg5) → done`. Segment→hour map is lighting.js (`0 DAWN … 5 NIGHT`).
|
||||
- `cover`: integer. **~half are 0 (free, walk in); the rest $2–$10** (F debits the wallet at the door).
|
||||
|
||||
### The venue (one `pub` per town)
|
||||
- A spine-END plain shop is converted in place to `type:'pub'` (keeps its lot id + geometry ⇒ no
|
||||
overlap, enterable like any shop). Tagged **`shop.venue === true`**; also `plan.shops.find(s=>s.type==='pub')`.
|
||||
- **Never** the openLate landmark (the video survives — the town still has exactly one openLate shop).
|
||||
- Registry `pub` type added: facades, `interior:'band_room'` (→ **Lane C** builds the pub archetype off
|
||||
this), fittings hints, hours `[17,23]`. It has **no district weights** so it's never auto-placed.
|
||||
- Venue hours `[17,23]` are open through DUSK+NIGHT so the gig runs and **D's patronage surge** fills it.
|
||||
|
||||
### `plan.posters` — gig advertising (NEW; **Lane B** renders)
|
||||
```
|
||||
plan.posters = [ { id, gigId, x, z, ry }, … ] // ~6, advertising tonight's gig (gigs[0])
|
||||
```
|
||||
One on the venue frontage + a seeded few along the spine, on the poles/walls B already streams. B
|
||||
places E's poster skin at `(x,z)` rotated `ry`. F wires "poster → tonight's-gig discoverability".
|
||||
|
||||
### → Lane D: the closing-time debt is PAID — consume `isOpen`
|
||||
`citygen.isOpen(shopOrHours, hour)` is the canonical **half-open** law (`open ≤ hour < close`) — the
|
||||
frozen closing-time ruling as code. Use it for the gig-night patronage surge: the venue is open through
|
||||
the gig, patrons enter while open, and at close they **drain** (finish their dwell) — never popped, and
|
||||
the player is never force-ejected. Your `sim.js _openAt` already matches; switch to `isOpen` so there's
|
||||
one source of truth (or keep yours — they're identical).
|
||||
|
||||
### → John: the band-name drop-in (`web/assets/custom_bands.json`)
|
||||
Edit the file, reload — your names get **priority** on the town's posters. Schema:
|
||||
`{ "bands": ["The Feral Galahs", …] }`. **Delete it and the generator takes over** (no errors, no fetch
|
||||
under `?noassets=1`). Deterministic given (seed, file contents). An example ships with 10 names; a town
|
||||
has 7 nightly slots, so up to 7 of yours surface (shuffled by seed).
|
||||
|
||||
## Round 9 (2026-07-15)
|
||||
|
||||
### → Lane D/F: closing-time occupant ruling (you already built it right — this ratifies + freezes it)
|
||||
|
||||
15
web/assets/custom_bands.json
Normal file
15
web/assets/custom_bands.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"_comment": "OPTIONAL band-name drop-in (v3 gigs). Edit the list and reload — these names get PRIORITY on the town's gig posters. Delete this file and the generator takes over (no errors). Schema: { \"bands\": [ \"name\", ... ] }. Owned by Lane A per ROUND12.",
|
||||
"bands": [
|
||||
"The Feral Galahs",
|
||||
"Chunderbolt",
|
||||
"Screaming Utes",
|
||||
"The Rissoles",
|
||||
"Dazza & The Dropbears",
|
||||
"Sunburnt Country",
|
||||
"The Flannel Shirts",
|
||||
"Amyl & The Servos",
|
||||
"Boganvillea",
|
||||
"The Dead Ringers"
|
||||
]
|
||||
}
|
||||
108
web/js/citygen/gigs.js
Normal file
108
web/js/citygen/gigs.js
Normal file
@ -0,0 +1,108 @@
|
||||
// PROCITY CityGen — v3 GIG LAYER (?gigs=1). Post-hoc augmentation: takes a finished CityPlan and
|
||||
// adds the venue (one pub), the nightly gig schedule (`plan.gigs`), and poster assignments
|
||||
// (`plan.posters`). Applied ONLY when the flag is on (selector: generatePlanFor(..,{gigs:true})),
|
||||
// so the base plan stays byte-identical and the frozen goldens don't move (CITY_SPEC prime flag law).
|
||||
//
|
||||
// Source-agnostic: works on synthetic AND osm plans (it only reads shops/lots/edges). Deterministic
|
||||
// from citySeed (+ optional custom band names). NO THREE, no network — pure data (CITY_SPEC law).
|
||||
|
||||
import { rng, seedFor, mulberry32, pick, shuffle } from '../core/prng.js';
|
||||
import { SHOP_TYPES } from '../core/registry.js';
|
||||
import { shopName, bandName } from './names.js';
|
||||
|
||||
const r2 = v => Math.round(v * 100) / 100;
|
||||
const r4 = v => Math.round(v * 10000) / 10000;
|
||||
|
||||
// Alpha model: one genre, a night gig (doors at DUSK seg4, on during NIGHT seg5 — see lighting.js
|
||||
// segment→hour map). A week of nightly entries at the one venue; F's clock picks tonight's.
|
||||
const NIGHTS = 7;
|
||||
const GIG_START_SEG = 5; // NIGHT (22:00) — gig is "on"
|
||||
const GIG_END_SEG = 5; // single coarse night segment; F does doors at startSeg−1 (DUSK)
|
||||
const GENRE = 'pubrock'; // the alpha genre (Lane E ships the pub-rock live bed)
|
||||
|
||||
// Pick the venue: prefer a spine-END plain shop (a 'shop'-use lot fronting a 'main' edge, farthest
|
||||
// from the origin — a destination you follow the sound to), NEVER the openLate landmark. Fallback
|
||||
// for osm / no-spine towns: the farthest-from-centre plain shop. Seeded pick among the farthest few.
|
||||
function pickVenue(plan, citySeed) {
|
||||
const lotById = new Map(plan.lots.map(l => [l.id, l]));
|
||||
const candidates = plan.shops.filter(s => {
|
||||
if (s.openLate) return false; // must not displace the night landmark
|
||||
const lot = lotById.get(s.lot);
|
||||
return lot && lot.use === 'shop'; // convert a plain shop → keeps SOLID + geometry
|
||||
});
|
||||
if (!candidates.length) return null;
|
||||
const mainEdges = new Set(plan.streets.edges.filter(e => e.kind === 'main').map(e => e.id));
|
||||
const dist2 = s => { const l = lotById.get(s.lot); return l.x * l.x + l.z * l.z; };
|
||||
let pool = candidates.filter(s => mainEdges.has(lotById.get(s.lot).frontEdge));
|
||||
if (!pool.length) pool = candidates; // osm / no spine → any plain shop
|
||||
pool = pool.slice().sort((a, b) => dist2(b) - dist2(a) || a.id - b.id); // farthest first, id tiebreak
|
||||
const topN = pool.slice(0, Math.min(5, pool.length));
|
||||
return pick(rng(citySeed, 'venue', 0), topN);
|
||||
}
|
||||
|
||||
// Band names across the nights: John's OPTIONAL custom names (from web/assets/custom_bands.json,
|
||||
// loaded by the shell and passed as opts.customBands) get PRIORITY — shuffled deterministically and
|
||||
// filled first, so a town surfaces up to NIGHTS of them — and the rest are generated. Deterministic
|
||||
// given (citySeed, customBands). Absent/empty custom list ⇒ pure generator (the ?noassets path).
|
||||
function assignBands(citySeed, nGigs, customBands) {
|
||||
const custom = (Array.isArray(customBands) ? customBands : [])
|
||||
.filter(b => typeof b === 'string' && b.trim()).map(b => b.trim());
|
||||
const shuffled = custom.length ? shuffle(rng(citySeed, 'bands', 0), custom.slice()) : [];
|
||||
const out = [];
|
||||
for (let i = 0; i < nGigs; i++) {
|
||||
out.push(i < shuffled.length ? shuffled[i] : bandName(seedFor(citySeed, 'band', i)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Posters advertising tonight's gig (night 0), near the venue + along the central streets B already
|
||||
// streams (poles/walls). Modest count for the one-pub alpha; positions seeded + deterministic. B
|
||||
// renders E's poster skin at (x,z) rotated ry; F wires "poster → tonight's gig" discoverability.
|
||||
function buildPosters(plan, citySeed, venue, gig) {
|
||||
const posters = [];
|
||||
const add = (x, z, ry) => posters.push({ id: posters.length, gigId: gig.gigId, x: r2(x), z: r2(z), ry: r4(ry) });
|
||||
const vlot = plan.lots.find(l => l.id === venue.lot); // the venue's LOT carries the coords (shop doesn't)
|
||||
if (vlot) add(vlot.x, vlot.z, vlot.ry); // one on the venue frontage itself
|
||||
const mainNodeIds = new Set();
|
||||
for (const e of plan.streets.edges) if (e.kind === 'main') { mainNodeIds.add(e.a); mainNodeIds.add(e.b); }
|
||||
let cand = plan.streets.nodes.filter(n => mainNodeIds.has(n.id));
|
||||
if (cand.length < 4) cand = plan.streets.nodes.slice();
|
||||
const chosen = shuffle(rng(citySeed, 'posters', 0), cand.slice()).slice(0, Math.min(5, cand.length));
|
||||
for (const n of chosen) add(n.x + 2, n.z, Math.atan2(n.x, n.z)); // small verge offset; face town centre
|
||||
return posters;
|
||||
}
|
||||
|
||||
// Public: augment a finished plan with the gig layer. Mutates + returns `plan` (caller passes a fresh
|
||||
// plan). Adds `plan.gigs`, `plan.posters`, and converts one shop into the `pub` venue in place.
|
||||
export function withGigs(plan, citySeed, opts = {}) {
|
||||
citySeed = citySeed >>> 0;
|
||||
const venue = pickVenue(plan, citySeed);
|
||||
if (!venue) { plan.gigs = []; plan.posters = []; return plan; } // degenerate town (no plain shops)
|
||||
|
||||
// Convert the chosen shop into the pub venue, in place (keeps its lot id + geometry ⇒ no overlap).
|
||||
const reg = SHOP_TYPES.pub;
|
||||
const vseed = seedFor(citySeed, 'venue', venue.id);
|
||||
const vr = mulberry32(vseed);
|
||||
venue.type = 'pub';
|
||||
venue.facadeSkin = pick(vr, reg.facades);
|
||||
const nm = shopName(vseed, 'pub');
|
||||
venue.name = nm.name; venue.sign = nm.sign;
|
||||
venue.storeys = Math.max(venue.storeys | 0, 2); // a corner hotel reads as ≥2 storeys
|
||||
venue.hours = [reg.hours.open, reg.hours.close]; // [17,23] — open into NIGHT so the gig runs
|
||||
venue.venue = true; // easy lookup alongside venueShopId
|
||||
|
||||
// Nightly gig schedule at the venue.
|
||||
const bands = assignBands(citySeed, NIGHTS, opts.customBands);
|
||||
plan.gigs = [];
|
||||
for (let n = 0; n < NIGHTS; n++) {
|
||||
const gr = mulberry32(seedFor(citySeed, 'gig', n));
|
||||
const cover = gr() < 0.5 ? 0 : 2 + ((gr() * 9) | 0); // ~half free, else integer $2–$10
|
||||
plan.gigs.push({
|
||||
gigId: n, venueShopId: venue.id, bandName: bands[n], genreKey: GENRE,
|
||||
night: n, startSeg: GIG_START_SEG, endSeg: GIG_END_SEG, cover,
|
||||
});
|
||||
}
|
||||
|
||||
plan.posters = buildPosters(plan, citySeed, venue, plan.gigs[0]);
|
||||
return plan;
|
||||
}
|
||||
@ -7,9 +7,10 @@
|
||||
// (Originated as a Lane F integration shim because the shell imports ./js/citygen/index.js while the
|
||||
// generator ships as plan.js; adopted here as the real Lane A entry point, per the shim's own note.)
|
||||
|
||||
export { generatePlan, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap } from './plan.js';
|
||||
export { shopName, townName } from './names.js';
|
||||
export { generatePlan, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap, isOpen } from './plan.js';
|
||||
export { shopName, townName, bandName } from './names.js';
|
||||
export { generatePlanOSM, osmTownKeys } from './plan_osm.js';
|
||||
export { withGigs } from './gigs.js';
|
||||
|
||||
// planSource selector (round 6; multi-town round 8). Default 'synthetic' → the byte-identical
|
||||
// golden-hash generator; 'osm' → the real-data fixture importer. Lane F wires ?plansrc=osm (+ optional
|
||||
@ -17,8 +18,12 @@ export { generatePlanOSM, osmTownKeys } from './plan_osm.js';
|
||||
// ran. Unknown source → synthetic; unknown town → the default town.
|
||||
import { generatePlan as _synth } from './plan.js';
|
||||
import { generatePlanOSM as _osm } from './plan_osm.js';
|
||||
import { withGigs as _withGigs } from './gigs.js';
|
||||
export function generatePlanFor(seed, source = 'synthetic', opts = {}) {
|
||||
return source === 'osm' ? _osm(seed, opts.town, opts) : _synth(seed);
|
||||
const plan = source === 'osm' ? _osm(seed, opts.town, opts) : _synth(seed);
|
||||
// v3 gig layer, gated: ?gigs=1 → shell passes { gigs:true, customBands } and we augment; otherwise
|
||||
// the base plan is returned untouched (byte-identical, goldens frozen — CITY_SPEC prime flag law).
|
||||
return opts.gigs ? _withGigs(plan, seed, opts) : plan;
|
||||
}
|
||||
|
||||
// Convenience default so `citygen.default` also resolves to the (synthetic) generator.
|
||||
|
||||
@ -25,6 +25,10 @@ function tok(r, t) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -73,8 +77,17 @@ const PATTERNS = {
|
||||
'{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'],
|
||||
@ -86,6 +99,7 @@ const SIGN_PATTERNS = {
|
||||
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 }.
|
||||
@ -103,6 +117,14 @@ export function shopName(seed, type) {
|
||||
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);
|
||||
|
||||
@ -20,6 +20,17 @@ const clampHour = h => Math.max(0, Math.min(23, h | 0));
|
||||
// and the `openLate` field are equivalent, and both identify that single shop (Lane F night gate).
|
||||
const LATE_HOUR = 22;
|
||||
|
||||
// Canonical CLOSING-TIME LAW (frozen v2 ruling; Lane D/F consume this — the "debt" made code).
|
||||
// Openness is HALF-OPEN: a shop is open for `open ≤ hour < close`, so a shop closing at 17 is shut
|
||||
// AT 17:00. New entry is refused when closed; occupants already inside finish their dwell and drain
|
||||
// (never popped); the player is never force-ejected. `hour` is a 0–24 float (Lane F's currentHour()).
|
||||
// Pass a shop (or any { hours:[open,close] }). No hours ⇒ treated as always open.
|
||||
export function isOpen(shopOrHours, hour) {
|
||||
const h = Array.isArray(shopOrHours) ? shopOrHours : (shopOrHours && shopOrHours.hours);
|
||||
if (!h) return true;
|
||||
return hour >= h[0] && hour < h[1];
|
||||
}
|
||||
|
||||
// World-space corners of a lot's rotated footprint. Convention matches selfcheck + Lane B:
|
||||
// a lot faces its street via `ry`; corners are [±w/2, ±d/2] rotated by ry about the lot centre.
|
||||
export function lotCorners(l) {
|
||||
|
||||
@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { generatePlan, chunkIndex, lotCorners, obbOverlap, CHUNK } from './plan.js';
|
||||
import { generatePlanOSM, osmTownKeys } from './plan_osm.js';
|
||||
import { generatePlanFor } from './index.js';
|
||||
import { allFacadeSkins, SHOP_TYPES } from '../core/registry.js';
|
||||
import { xmur3 } from '../core/prng.js';
|
||||
|
||||
@ -130,7 +131,10 @@ function structuralSuite(plan, label) {
|
||||
// exactly one late-night landmark; openLate field ⟺ hours[1]≥22 (same shop); it's the video (or,
|
||||
// for a town with no video at all, the fallback landmark). Holds for synthetic AND osm.
|
||||
const late = plan.shops.filter(sh => sh.openLate);
|
||||
const ge22 = plan.shops.filter(sh => sh.hours[1] >= 22);
|
||||
// The v3 gig venue (`sh.venue`) is a night place that also runs ≥22:00 by design, so it's exempt
|
||||
// from the single-late-shop rule. Base plans have no venue ⇒ this filter is a no-op there (goldens
|
||||
// unaffected); gig plans exclude the pub so the openLate video is still the lone landmark.
|
||||
const ge22 = plan.shops.filter(sh => sh.hours[1] >= 22 && !sh.venue);
|
||||
ok(late.length === 1, `${label}: exactly one open-late shop (${late.length})`);
|
||||
ok(ge22.length === 1, `${label}: exactly one shop closes ≥22:00 (${ge22.length})`);
|
||||
ok(late.length === 1 && ge22.length === 1 && late[0] === ge22[0], `${label}: openLate field ⟺ hours[1]≥22 (same shop)`);
|
||||
@ -138,6 +142,25 @@ function structuralSuite(plan, label) {
|
||||
ok(late.length === 1 && (late[0].type === 'video' || !plan.shops.some(x => x.type === 'video')), `${label}: the open-late shop is the video rental (or fallback if no video)`);
|
||||
}
|
||||
|
||||
// The v3 GIG layer (?gigs=1). Runs the full structuralSuite (venue-aware) plus gig-specific
|
||||
// invariants. Validates the interface every downstream lane (C/D/B/F) hangs off this round.
|
||||
function gigSuite(plan, label) {
|
||||
structuralSuite(plan, label); // full coverage; venue exempt from late-shop rule
|
||||
const pubs = plan.shops.filter(s => s.type === 'pub');
|
||||
ok(pubs.length === 1, `${label}: exactly one pub venue (${pubs.length})`);
|
||||
const pub = pubs[0] || {};
|
||||
ok(pub.venue === true && !pub.openLate, `${label}: pub tagged venue, not the openLate landmark`);
|
||||
ok((pub.hours || [0, 0])[1] >= 22, `${label}: venue open into night so the gig can run`);
|
||||
ok(Array.isArray(plan.gigs) && plan.gigs.length >= 1, `${label}: has a nightly gig schedule`);
|
||||
ok(plan.gigs.every(g => g.venueShopId === pub.id), `${label}: every gig plays the venue`);
|
||||
ok(plan.gigs.every(g => Number.isInteger(g.cover) && (g.cover === 0 || (g.cover >= 2 && g.cover <= 10))), `${label}: cover ∈ {0}∪[2,10]`);
|
||||
ok(plan.gigs.every(g => typeof g.bandName === 'string' && g.bandName && !/[{}]|undefined/.test(g.bandName)), `${label}: band names resolved`);
|
||||
ok(plan.gigs.every(g => Number.isInteger(g.startSeg) && Number.isInteger(g.endSeg) && g.startSeg >= 0 && g.endSeg <= 5 && g.startSeg <= g.endSeg), `${label}: gig segment window sane`);
|
||||
ok(plan.gigs.every(g => g.genreKey === 'pubrock' && Number.isInteger(g.night)), `${label}: alpha genre + night index`);
|
||||
const gigIds = new Set(plan.gigs.map(g => g.gigId));
|
||||
ok(Array.isArray(plan.posters) && plan.posters.every(p => gigIds.has(p.gigId) && isFiniteNum(p.x) && isFiniteNum(p.z) && isFiniteNum(p.ry)), `${label}: posters reference a real gig with finite coords`);
|
||||
}
|
||||
|
||||
// ── 1. determinism: two runs byte-identical ─────────────────────────────────────────
|
||||
section('determinism (deep-equal across two runs)');
|
||||
for (const s of SEEDS) {
|
||||
@ -203,6 +226,30 @@ for (const town of osmTownKeys()) {
|
||||
console.log(` ${town}: ${report.shops} shops · normalized {typesRemapped:${report.typesRemapped}, hoursClamped:${report.hoursClamped}, openLate:${report.openLate}}`);
|
||||
}
|
||||
|
||||
// ── 3c. v3 GIG layer (?gigs=1): flags-off identity + gig invariants (synthetic + osm) ──
|
||||
section('v3 gig layer (?gigs=1) — flags-off identity + gig invariants');
|
||||
// PRIME FLAG LAW: ?gigs off must be byte-identical to the base generator (goldens can't move).
|
||||
for (const s of SEEDS) ok(JSON.stringify(generatePlanFor(s)) === JSON.stringify(generatePlan(s)), `seed ${s}: ?gigs off ≡ base (byte-identical)`);
|
||||
const GIG_SEEDS = [20261990, 1, 42, 777];
|
||||
for (const s of GIG_SEEDS) gigSuite(generatePlanFor(s, 'synthetic', { gigs: true }), `gig syn ${s}`);
|
||||
for (const town of osmTownKeys()) for (const s of GIG_SEEDS) gigSuite(generatePlanFor(s, 'osm', { gigs: true, town }), `gig osm/${town} ${s}`);
|
||||
// determinism of the gig layer (same seed ⇒ byte-identical gig plan)
|
||||
ok(JSON.stringify(generatePlanFor(42, 'synthetic', { gigs: true })) === JSON.stringify(generatePlanFor(42, 'synthetic', { gigs: true })), `gig layer deterministic`);
|
||||
// custom-band drop-in: passed names surface with priority, deterministically given (seed, list)
|
||||
{
|
||||
const custom = ['AAA Custom One', 'BBB Custom Two', 'CCC Custom Three'];
|
||||
const pc = generatePlanFor(20261990, 'synthetic', { gigs: true, customBands: custom });
|
||||
ok(pc.gigs.map(g => g.bandName).filter(b => custom.includes(b)).length === custom.length, `custom bands: all ${custom.length} passed names surface`);
|
||||
ok(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true, customBands: custom })) === JSON.stringify(pc), `custom bands: deterministic given (seed, list)`);
|
||||
}
|
||||
// gig-layer golden (guards gig-layer output drift; pure-generator path, no custom file)
|
||||
const GIG_GOLDEN = 0xa6ae5a5e;
|
||||
{
|
||||
const hash = xmur3(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true })))() >>> 0;
|
||||
if (GIG_GOLDEN === 0) console.log(` ⚠ gig golden UNPINNED → set GIG_GOLDEN = 0x${hash.toString(16).padStart(8, '0')}`);
|
||||
ok(GIG_GOLDEN !== 0 && hash === (GIG_GOLDEN >>> 0), `gig layer golden 0x${hash.toString(16)} matches pinned 0x${(GIG_GOLDEN >>> 0).toString(16)}`);
|
||||
}
|
||||
|
||||
// ── 4. every facade skin referenced by the registry exists on disk ──────────────────
|
||||
section('assets on disk');
|
||||
for (const f of allFacadeSkins()) ok(existsSync(join(ASSETS, f)), `registry facade exists: ${f}`);
|
||||
|
||||
@ -82,3 +82,30 @@ export const TOWN_SUFFIX = [
|
||||
'Flat', 'Creek', 'Springs', 'Gully', 'Junction', 'Crossing', 'Downs', 'Vale', 'Heads',
|
||||
'Plains', 'Ridge', 'Hollow', 'Bend', 'Reach', 'Wells', '', '', '', // blanks: bare root sometimes
|
||||
];
|
||||
|
||||
// ── v3 gigs (?gigs=1): band names (90s Aussie pub-rock) + pub/venue names ────────────
|
||||
// Band adjectives (feral, sunburnt, sweaty-front-bar register)
|
||||
export const BAND_ADJ = [
|
||||
'Feral', 'Screaming', 'Dead', 'Rusty', 'Filthy', 'Mangy', 'Sunburnt', 'Flannel', 'Amyl',
|
||||
'Cosmic', 'Chunder', 'Drunken', 'Wild', 'Electric', 'Grinning', 'Roaring', 'Deadset',
|
||||
'Salty', 'Dusty', 'Bitter', 'Feral', 'Howling', 'Molten', 'Ratbag', 'Bogan',
|
||||
];
|
||||
// Band nouns (plural — "The {Adj} {Noun}")
|
||||
export const BAND_NOUN = [
|
||||
'Galahs', 'Utes', 'Cheezels', 'Dropbears', 'Rissoles', 'Chisels', 'Larrikins', 'Dropkicks',
|
||||
'Woodducks', 'Cockies', 'Servos', 'Sherbets', 'Ferals', 'Yobbos', 'Ratbags', 'Cane Toads',
|
||||
'Redbacks', 'Blowflies', 'Mullets', 'Eskies', 'Goannas', 'Numbats', 'Longnecks', 'Bull Bars',
|
||||
];
|
||||
// Single-word band names (Regurgitator / Grinspoon / Spiderbait parody tier)
|
||||
export const BAND_ONEWORD = [
|
||||
'Chunderbolt', 'Boganvillea', 'Roadkill', 'Grommet', 'Feralcat', 'Backburn', 'Sunstroke',
|
||||
'Bindii', 'Deadwood', 'Longneck', 'Dropkick', 'Bullbar', 'Stinger', 'Slabhead', 'Gutful',
|
||||
'Chundermuffin', 'Wombatt', 'Screwtop', 'Goonsack', 'Ferntongue',
|
||||
];
|
||||
|
||||
// Pub / hotel names — every AU country town has a Royal, a Commercial, a Terminus.
|
||||
export const PUB_NAME = [
|
||||
'The Royal', 'The Royal Hotel', 'The Commercial', 'The Terminus', 'The Railway Hotel',
|
||||
'The Criterion', 'The Grand', 'The Exchange Hotel', 'The Federal', 'The Bridge Hotel',
|
||||
'The Victoria', 'The Star Hotel', 'The Imperial', 'The Great Southern',
|
||||
];
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
export const DISTRICT_KINDS = ['mainstreet', 'arcade', 'backstreets', 'warehouse', 'residential', 'market'];
|
||||
export const EDGE_KINDS = ['main', 'side', 'lane', 'arcade'];
|
||||
export const USE_KINDS = ['shop', 'anchor', 'house', 'yard', 'stall', 'infill'];
|
||||
export const SHOP_TYPE_IDS = ['record', 'opshop', 'toy', 'book', 'video', 'pawn', 'milkbar', 'dept', 'stall'];
|
||||
export const SHOP_TYPE_IDS = ['record', 'opshop', 'toy', 'book', 'video', 'pawn', 'milkbar', 'dept', 'stall', 'pub'];
|
||||
|
||||
// ── the registry ──────────────────────────────────────────────────────────────────
|
||||
// Per type:
|
||||
@ -121,6 +121,17 @@ export const SHOP_TYPES = {
|
||||
weights: { market: 1 },
|
||||
footprint: { fmin: 3, fmax: 5, dmin: 3, dmax: 5 },
|
||||
},
|
||||
pub: {
|
||||
label: 'Pub',
|
||||
facades: ['facade-djsim-pub.jpg', 'facade-federation.jpg'], // corner hotel; both exist in web/assets/gen/
|
||||
sign: { bg: '#3a2416', fg: '#f2d98a', accent: '#c0392b', style: 'gilt' },
|
||||
interior: 'band_room', // Lane C pub archetype (stage + PA + bar)
|
||||
fittings: ['bar_counter', 'stage', 'pa_stack', 'pub_tables', 'stools'],
|
||||
storeys: [1, 2],
|
||||
hours: { open: 17, close: 23 }, // a NIGHT venue: open into NIGHT so gigs can run (gig layer sets these)
|
||||
weights: {}, // NEVER auto-placed — the ?gigs=1 layer converts one shop into the venue
|
||||
footprint: { fmin: 16, fmax: 24, dmin: 18, dmax: 26 },
|
||||
},
|
||||
};
|
||||
|
||||
// ── pure helpers (no randomness — plan.js supplies the prng stream) ─────────────────
|
||||
|
||||
34
web/map.html
34
web/map.html
@ -91,12 +91,14 @@ import { generatePlanFor, chunkIndex, CHUNK } from './js/citygen/index.js';
|
||||
const _q = new URLSearchParams(location.search);
|
||||
const PLANSRC = _q.get('plansrc') === 'osm' ? 'osm' : 'synthetic';
|
||||
const TOWN = _q.get('town') || undefined;
|
||||
const GIGS = _q.get('gigs') === '1'; // v3 gig layer: venue + nightly gigs + posters (?gigs=1)
|
||||
let customBands = []; // John's OPTIONAL band drop-in (loaded async at boot when ?gigs=1)
|
||||
import { SHOP_TYPES, SHOP_TYPE_IDS } from './js/core/registry.js';
|
||||
|
||||
// ── palette ─────────────────────────────────────────────────────────────────────────
|
||||
const TYPE_COLOR = {
|
||||
record:'#ff5470', opshop:'#5fbf74', toy:'#ffcf3f', book:'#c98b3b', video:'#ffe14d',
|
||||
pawn:'#c0c0c0', milkbar:'#ff6b6b', dept:'#e07b9a', stall:'#c8a24a',
|
||||
pawn:'#c0c0c0', milkbar:'#ff6b6b', dept:'#e07b9a', stall:'#c8a24a', pub:'#e8b73a',
|
||||
};
|
||||
const USE_COLOR = {
|
||||
shop:'#6b5a3a', anchor:'#8a4a63', house:'#3f5a4a', yard:'#2e3a2a', stall:'#7a6a3a', infill:'#4a4436',
|
||||
@ -153,6 +155,7 @@ function draw(){
|
||||
drawStreets();
|
||||
if ($('tLots').checked) drawLots();
|
||||
if ($('tShops').checked) drawShops();
|
||||
if (GIGS) drawGigs();
|
||||
if ($('tNodes').checked) drawNodes();
|
||||
if (hover) drawHoverHi();
|
||||
drawCompass();
|
||||
@ -229,6 +232,19 @@ function drawShops(){
|
||||
}
|
||||
}
|
||||
}
|
||||
// v3 gig overlay (?gigs=1): poster markers + a ring/label on the venue.
|
||||
function drawGigs(){
|
||||
if(!plan.gigs || !plan.gigs.length) return;
|
||||
ctx.fillStyle='#ffd23f';
|
||||
for(const p of (plan.posters||[])){ const [sx,sy]=w2s(p.x,p.z); // little poster pins
|
||||
ctx.beginPath(); ctx.moveTo(sx,sy-4); ctx.lineTo(sx+3,sy+3); ctx.lineTo(sx-3,sy+3); ctx.closePath(); ctx.fill(); }
|
||||
const venue=plan.shops.find(s=>s.venue); if(!venue) return;
|
||||
const l=plan.lots.find(x=>x.id===venue.lot); if(!l) return;
|
||||
const [sx,sy]=w2s(l.x,l.z);
|
||||
ctx.beginPath(); ctx.arc(sx,sy,10,0,7); ctx.strokeStyle='#ffd23f'; ctx.lineWidth=2; ctx.stroke();
|
||||
ctx.font='bold 10px ui-monospace,monospace'; ctx.fillStyle='#ffd23f'; ctx.textAlign='center';
|
||||
ctx.fillText('♪ '+venue.sign, sx, sy-15);
|
||||
}
|
||||
function drawNodes(){
|
||||
ctx.fillStyle=getcss('--accent');
|
||||
for(const n of plan.streets.nodes){ const [sx,sy]=w2s(n.x,n.z);
|
||||
@ -296,7 +312,7 @@ const pad=h=>String(h).padStart(2,'0')+':00';
|
||||
// ── panel ─────────────────────────────────────────────────────────────────────────
|
||||
function regen(seed){
|
||||
const s = (seed>>>0);
|
||||
plan = generatePlanFor(s, PLANSRC, { town: TOWN }); idx = chunkIndex(plan); _sbl = null; // rebuild the cache for the new plan
|
||||
plan = generatePlanFor(s, PLANSRC, { town: TOWN, gigs: GIGS, customBands }); idx = chunkIndex(plan); _sbl = null; // rebuild the cache for the new plan
|
||||
$('seed').value = s;
|
||||
$('name').innerHTML = `<b style="color:var(--accent);font-size:14px">${esc(plan.name)}</b>`;
|
||||
const nShop=plan.shops.length, byType={};
|
||||
@ -304,6 +320,13 @@ function regen(seed){
|
||||
$('stats').innerHTML =
|
||||
`<b>${plan.streets.edges.length}</b> streets · <b>${plan.blocks.length}</b> blocks<br>`+
|
||||
`<b>${plan.lots.length}</b> lots · <b>${nShop}</b> shops · <b>${Object.keys(idx.chunks).length}</b> chunks`;
|
||||
if (GIGS && plan.gigs && plan.gigs.length){
|
||||
const venue=plan.shops.find(s=>s.venue), g0=plan.gigs[0];
|
||||
$('stats').innerHTML += `<br><b style="color:#ffd23f">♪ ${esc(venue?.name||'venue')}</b><br>`+
|
||||
`tonight: <b>${esc(g0.bandName)}</b> · ${g0.cover?('$'+g0.cover+' cover'):'FREE'}<br>`+
|
||||
`<span class="muted">${plan.gigs.length} nights · ${(plan.posters||[]).length} posters`+
|
||||
`${customBands.length?` · ${customBands.length} custom bands`:''}</span>`;
|
||||
}
|
||||
fitView(); draw(); buildLegend(byType);
|
||||
}
|
||||
function buildLegend(byType){
|
||||
@ -335,7 +358,12 @@ addEventListener('resize', resize);
|
||||
// rAF ensures the flex layout has sized the canvas before fitView reads its dimensions.
|
||||
const q=new URLSearchParams(location.search);
|
||||
const bootSeed=parseInt(q.get('seed')||$('seed').value,10)||20261990;
|
||||
requestAnimationFrame(()=>{ resize(); regen(bootSeed); });
|
||||
requestAnimationFrame(async ()=>{
|
||||
resize();
|
||||
// OPTIONAL band drop-in: load John's names for the debug view (graceful if the file is absent).
|
||||
if (GIGS){ try { const r=await fetch('./assets/custom_bands.json'); if(r.ok){ const j=await r.json(); if(Array.isArray(j.bands)) customBands=j.bands; } } catch(e){} }
|
||||
regen(bootSeed);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user