162 lines
10 KiB
JavaScript
162 lines
10 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. Every read is keyed by venueShopId — the R12
|
||
// single-venue alias retired in R15 (B swept its cross-lane readers in R14; F migrated its own tools).
|
||
const venueIds = [...new Set(gigs.map((g) => g.venueShopId))];
|
||
|
||
// one latch per venue, keyed by its night-N gig (null → dark tonight → 'quiet'). N defaults to 0 —
|
||
// the pre-v7 "tonight" every consumer was built against. [Lane F R30 — SLEEP=TOMORROW] the game layer
|
||
// re-keys N to (day − 1) % 7 via setWeekNight(), walking the EXISTING seeded week; no game ⇒ N stays 0
|
||
// and this file behaves byte-identically to R29.
|
||
let weekNight = 0;
|
||
const latch = new Map();
|
||
function buildLatches() {
|
||
latch.clear();
|
||
for (const id of venueIds) {
|
||
const tonight = gigs.find((g) => g.venueShopId === id && g.night === weekNight) || null;
|
||
latch.set(id, createVenueLatch(tonight, lighting));
|
||
}
|
||
}
|
||
buildLatches();
|
||
|
||
// 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;
|
||
|
||
return {
|
||
// ── the week (R30, SLEEP=TOMORROW) ────────────────────────────────────────────────────────────
|
||
// setWeekNight(n): re-key every venue's latch to its night-n gig. Fresh latches ⇒ paid stamps and
|
||
// played state reset — correct: a new night means the cover is due again (John's R12 ruling). The
|
||
// three hard-won latch laws live inside createVenueLatch, untouched. Idempotent per n (no-op when
|
||
// the night hasn't changed, so a boot-time call on night 0 changes nothing).
|
||
get weekNight() { return weekNight; },
|
||
setWeekNight(n) {
|
||
n = ((n % 7) + 7) % 7;
|
||
if (n === weekNight) return;
|
||
weekNight = n;
|
||
buildLatches();
|
||
tickAll(); // settle every fresh latch on the current segment
|
||
},
|
||
// ── 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 per-venue state.
|
||
update() { tickAll(); },
|
||
|
||
dispose() { if (typeof window !== 'undefined') window.removeEventListener('procity:segment', onSegment); },
|
||
};
|
||
}
|