PROCITY/web/js/world/gig_state.js

162 lines
10 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 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 24 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); },
};
}