Wires A–E + D's district together and fires it. Seed 20261990 → "Boolarra Heads": 3 venues, 2 playing tonight (The Exchange Hotel pub gig-pubrock + an RSL gig-covers, one dark), Screaming Utes $10 on the door. - gig_state.js → PER-VENUE latches off the shared procity:segment spine (one per venueShopId); all three hard-won R12 laws kept. New runtime surface window.PROCITY.gigs.byVenue + onOf/openOf/gigOf/coverOf/ bandNameOf/paidOf(id) + markPaidOf/venueShopIds; the R12 single-venue alias kept for one round. - interior_mode.js: crowd-walla rides the interior tone layer under the live band bed while `on`; an applause sting fires once on the on->done edge. DELETE the GIG_BED bridge — debt #1, and it was a LIVE BUG: E renamed pubrock-live->gig-pubrock, so the bridge (gig-pubrock->pubrock-live) pointed at a dead key => the band would have mimed in silence. room.audio.gigKey (already the manifest key) feeds directly. - index.html: per-venue cover charge (a night at two venues is two covers), concurrent per-venue setGig surges, and the outdoor VenueQueue lifecycle (D shipped the class + left the street-side seam to F: spawn at doors off B's queueZoneFor, drive each frame, dispose by done), exposed as queueCountOf(id). - interior_scale_check.py: human-size assert now measures STATURE (crown-foot, platform-independent); world-crown ceiling-clip assert kept as its own line (debt #6). - flags_check.py smoke_gigs -> THE DISTRICT: band->4, per-venue loop (every gigKey resolves across 3 genres, crowd <= its own watchPoints, no-giants by stature), street budget at the busiest venue block, the queue assert; plus the R12 determinism/cover/noassets/flags-off arms. - CITY_SPEC: pin the window.PROCITY.gigs runtime surface. gig_shot.py -> r13 interior + venue_night shots. qa.sh --strict GREEN 6/6, 0 warn. Interior 63 draws/35k tris; street @ venue block 121 draws/51k tris; band+crowd stature 1.61-1.81 m under a 4 m ceiling; flags-off fingerprint 0x3fa36874 unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
9.6 KiB
JavaScript
156 lines
9.6 KiB
JavaScript
// PROCITY Lane F — gig_state.js [integration glue, F-owned, round 13 / v3.0-beta THE DISTRICT]
|
||
// The Friday-night state machine, now PER-VENUE. A town has 2–4 venues (pub / band_room / rsl); each runs
|
||
// its own latch quiet → doors → on → done off Lane B's day clock and Lane A's plan.gigs. F owns it because
|
||
// every other lane consumes it and none of them own the clock:
|
||
// • Lane B audio.js reads window.PROCITY.gigs.byVenue[id] → the muffled through-the-wall spill per venue
|
||
// • Lane B venue.js update(byVenue) → marquee/sign glow on each frontage
|
||
// • Lane D sim.js setGig(venueShopId, on) → the gig-night patronage surge, per venue
|
||
// • Lane C buildInterior(shop, THREE, { gig }) → room.audio.gigKey (the live bed inside)
|
||
// • Lane F the cover charge at each door + the smokes
|
||
//
|
||
// Model (CITY_SPEC §"CityPlan v3 — gig layer"): plan.gigs is the whole week × every venue. TONIGHT for a
|
||
// venue is its night-0 entry (the same night-0 convention A's posters and B's frontage key off); a venue
|
||
// dark tonight has no latch gig → reads 'quiet' all session, which is true to the seeded schedule. Each
|
||
// venue's night rolls the same way (clock turns → next cover due), so a night at two venues is two covers.
|
||
//
|
||
// ── THREE HARD-WON LAWS, unchanged from the alpha (they each cost a live-verify to learn): ──────────────
|
||
// WHY 'done' NEEDS A LATCH. lighting.js's segment→hour map tops out at NIGHT (22:00) and a venue closes at
|
||
// closing time, so the gig owns the WHOLE night segment — "after the gig" can only be the far side of the
|
||
// wrap, DAWN. So: latch while the clock is inside the on-window, hold 'done' for exactly the segment after
|
||
// it (DAWN = closing time — the band packs up, the crowd drains per Lane A's closing ruling), and clear on
|
||
// any other segment → back to 'quiet'. The latch clears on any non-DAWN segment (not only at MORNING)
|
||
// because the player drives the clock by hand ([ and ]): rewinding NIGHT → DUSK → ARVO must read
|
||
// doors → quiet, not leave 'done' stuck on a midday town (the R12 live verify caught it doing exactly that).
|
||
//
|
||
// WHY THE NIGHT ROLLS ON WHERE THE CLOCK LANDS, not on watching 'done' go by. First cut incremented the
|
||
// night when LEAVING 'done' forwards — so a clock JUMP straight from NIGHT to MORNING (never touching DAWN)
|
||
// never rolled the night, and a broke punter walked in on last night's paid stamp. Now: the latch clears
|
||
// wherever it clears, and landing in the DAYTIME (MORNING…ARVO) is what rolls the night; landing on the
|
||
// doors means we rewound out of the gig — same night, stays paid. DBG/shots/soak all JUMP, so only this
|
||
// rule survives them.
|
||
//
|
||
// WHY IT LISTENS RATHER THAN POLLS. Each latch must observe EVERY segment; a poll only sees the segments it
|
||
// is sampled on. Driving the clock NIGHT→DAWN→MORNING→NIGHT with nothing reading in between (player inside a
|
||
// venue, audio muted, so neither the street loop nor the audio rAF is reading) skipped the roll entirely —
|
||
// the night never advanced and the cover was never due again. lighting.js dispatches `procity:segment` on
|
||
// EVERY segment change (hand-driven or the auto day cycle), so that is the signal every latch rides. Reads
|
||
// stay safe on their own: state is a getter that re-observes the current segment, and observing the same
|
||
// segment twice is idempotent.
|
||
|
||
const SEGS = 6;
|
||
|
||
// One venue's latch — the alpha machine, its hard-won internals verbatim. `gig` is this venue's tonight
|
||
// (night-0) entry, or null when the venue is dark tonight (→ always 'quiet').
|
||
function createVenueLatch(gig, lighting) {
|
||
const startSeg = gig && gig.startSeg != null ? gig.startSeg : 5;
|
||
const endSeg = gig && gig.endSeg != null ? gig.endSeg : 5;
|
||
const doorsSeg = (startSeg - 1 + SEGS) % SEGS; // DUSK — the doors open
|
||
const doneSeg = (endSeg + 1) % SEGS; // DAWN — closing time; the only segment 'done' lives in
|
||
// The daytime segments: everything from the morning after the gig up to (not including) the doors.
|
||
// Landing here after a gig means THE DAY HAS TURNED, which rolls the night (next cover due).
|
||
const DAYTIME = (() => {
|
||
const set = new Set();
|
||
for (let s = (doneSeg + 1) % SEGS; s !== doorsSeg; s = (s + 1) % SEGS) set.add(s);
|
||
return set;
|
||
})();
|
||
|
||
let state = 'quiet';
|
||
let played = false; // latch: tonight's gig has run (set while inside the on-window)
|
||
let night = 0; // rolls when the day turns after a gig — keys the cover stamp (runtime only)
|
||
let paidNight = -1; // the night whose cover is paid; re-entry the same night is free
|
||
|
||
// The whole machine, for ONE segment. Idempotent per segment, so it is safe to call from both the
|
||
// procity:segment event and any read.
|
||
function observe(seg) {
|
||
const on = seg >= startSeg && seg <= endSeg;
|
||
if (on) played = true;
|
||
else if (played && seg !== doneSeg) {
|
||
// The latch is clearing: the gig is behind us. Whether that's "a new night" (cover due again) or
|
||
// "we rewound out of it" (same night, stays paid) is decided by WHERE we land, not by watching
|
||
// 'done' go by (see the roll law in the header).
|
||
played = false;
|
||
if (DAYTIME.has(seg)) night++; // landed in the daytime ⇒ the day turned ⇒ next cover is due
|
||
} // (landed on the doors ⇒ rewound out of the gig ⇒ same night)
|
||
state = on ? 'on' : seg === doorsSeg ? 'doors' : played ? 'done' : 'quiet';
|
||
return state;
|
||
}
|
||
|
||
function tick() {
|
||
if (!gig || !lighting || !lighting.getClock) return (state = 'quiet');
|
||
return observe(lighting.getClock().seg);
|
||
}
|
||
|
||
return {
|
||
gig, observe, tick,
|
||
get state() { return tick(); },
|
||
get on() { return tick() === 'on'; },
|
||
get open() { const s = tick(); return s === 'doors' || s === 'on'; }, // the door is taking punters
|
||
get cover() { return gig ? gig.cover | 0 : 0; },
|
||
get bandName() { return gig ? gig.bandName : null; },
|
||
get night() { tick(); return night; },
|
||
// cover stamp — runtime only (like the wallet): paid once, free to pop out and back in until the night
|
||
// rolls. NOT persisted, NOT written into the plan (goldens never move).
|
||
get paid() { tick(); return paidNight === night; },
|
||
markPaid() { tick(); paidNight = night; },
|
||
};
|
||
}
|
||
|
||
export function createGigState({ plan, lighting }) {
|
||
const gigs = (plan && plan.gigs) || [];
|
||
// The district's venues, in first-seen (gigId) order. Primary = gigs[0].venueShopId — the alpha's
|
||
// "tonight", kept as the compat alias so B's mid-round single-venue callers still light the right pub.
|
||
const venueIds = [...new Set(gigs.map((g) => g.venueShopId))];
|
||
const primaryId = gigs.length ? gigs[0].venueShopId : null;
|
||
|
||
// one latch per venue, keyed by its night-0 gig (null → dark tonight → 'quiet')
|
||
const latch = new Map();
|
||
for (const id of venueIds) {
|
||
const tonight = gigs.find((g) => g.venueShopId === id && g.night === 0) || null;
|
||
latch.set(id, createVenueLatch(tonight, lighting));
|
||
}
|
||
|
||
// ONE listener drives every latch — the listens-not-polls law, now fanned out across the district.
|
||
const onSegment = (e) => { if (e && e.detail) for (const l of latch.values()) l.observe(e.detail.seg); };
|
||
if (typeof window !== 'undefined') window.addEventListener('procity:segment', onSegment);
|
||
|
||
function tickAll() { for (const l of latch.values()) l.tick(); }
|
||
tickAll(); // settle every latch on the boot segment
|
||
|
||
const L = (id) => latch.get(id) || null;
|
||
const P = () => L(primaryId);
|
||
|
||
return {
|
||
// ── per-venue API (R13 district) ──────────────────────────────────────────────────────────────
|
||
get venueShopIds() { return venueIds.slice(); },
|
||
stateOf(id) { const l = L(id); return l ? l.state : 'quiet'; },
|
||
onOf(id) { const l = L(id); return l ? l.on : false; },
|
||
openOf(id) { const l = L(id); return l ? l.open : false; },
|
||
gigOf(id) { const l = L(id); return l ? l.gig : null; },
|
||
coverOf(id) { const l = L(id); return l ? l.cover : 0; },
|
||
bandNameOf(id) { const l = L(id); return l ? l.bandName : null; },
|
||
nightOf(id) { const l = L(id); return l ? l.night : 0; },
|
||
paidOf(id) { const l = L(id); return l ? l.paid : false; },
|
||
markPaidOf(id) { const l = L(id); if (l) l.markPaid(); },
|
||
// the map B's venue.update() and audio.js consume: a fresh { [venueShopId]: 'quiet'|'doors'|'on'|'done' }.
|
||
// Called per street frame over a handful of venues — cheap.
|
||
get byVenue() { const o = {}; for (const [id, l] of latch) o[id] = l.tick(); return o; },
|
||
// shell calls this once per street frame — ticks every latch (so B/D read fresh state) and returns the
|
||
// primary state for the alpha-compat caller.
|
||
update() { tickAll(); return P() ? P().state : 'quiet'; },
|
||
|
||
// ── alpha-shape alias → the PRIMARY venue (kept for one round; B reads it mid-round) ─────────────
|
||
get state() { const p = P(); return p ? p.state : 'quiet'; },
|
||
get on() { const p = P(); return p ? p.on : false; },
|
||
get open() { const p = P(); return p ? p.open : false; },
|
||
get gig() { const p = P(); return p ? p.gig : null; },
|
||
get venueShopId() { return primaryId; },
|
||
get bandName() { const p = P(); return p ? p.bandName : null; },
|
||
get cover() { const p = P(); return p ? p.cover : 0; },
|
||
get night() { const p = P(); return p ? p.night : 0; },
|
||
get paid() { const p = P(); return p ? p.paid : false; },
|
||
markPaid() { const p = P(); if (p) p.markPaid(); },
|
||
|
||
dispose() { if (typeof window !== 'undefined') window.removeEventListener('procity:segment', onSegment); },
|
||
};
|
||
}
|