// PROCITY Lane F — gig_state.js [integration glue, F-owned, round 12 / v3.0-alpha] // The Friday-night state machine: quiet → doors → on → done, driven 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.state → the muffled through-the-wall spill // • Lane B venue.js update(state) → marquee/sign glow on the frontage // • Lane D sim.js setGig(venueShopId, on) → the gig-night patronage surge // • Lane C buildInterior(shop, THREE, { gig }) → room.audio.gigKey (the live bed inside) // • Lane F the cover charge at the door + the smokes // // Alpha model (CITY_SPEC §"CityPlan v3 — gig layer"): tonight is plan.gigs[0]; startSeg=5 (NIGHT 22:00), // endSeg=5, doors at startSeg−1 (DUSK). One venue, one genre, seven nightly entries in the plan. // // WHY 'done' NEEDS A LATCH. lighting.js's segment→hour map tops out at NIGHT (22:00) and the pub closes at // 23, 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 and the crowd drains per Lane A's closing-time ruling), and clear the // latch on any other segment → back to 'quiet'. The latch clears on any non-DAWN segment rather than only // at MORNING because the player drives this 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). A night only *counts* as rolled when we leave 'done' FORWARDS — that's what separates a real // DAWN→MORNING roll (new night, cover due again) from a rewind back out of the gig (same night, paid). // // WHY IT LISTENS RATHER THAN POLLS. The latch has to observe every segment, and a poll only sees the // segments it happens to be sampled on: driving the clock NIGHT→DAWN→MORNING→NIGHT with nothing reading in // between (player inside the pub, 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. Caught live in // the R12 verify. lighting.js dispatches `procity:segment` on EVERY segment change (hand-driven or the // automatic day cycle), so that is the signal the 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; export function createGigState({ plan, lighting }) { const gigs = (plan && plan.gigs) || []; const gig = gigs[0] || null; // alpha: tonight is gigs[0] (CITY_SPEC v3) 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 — // MORNING/MIDDAY/ARVO for the alpha schedule. Landing here after a gig means THE DAY HAS TURNED, which // is what rolls the night (and makes the next cover due). See the roll note below. 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 // 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 means "a new night" (cover due again) // or "we rewound out of it" (same night, stays paid) is decided by WHERE we land, not by having // watched 'done' go by: keying the roll on leaving 'done' meant a clock jump straight from NIGHT to // MORNING never rolled the night at all, and a broke punter walked in on last night's stamp. The // smoke caught it; stepping ([/]) always passes through DAWN, so only jumps exposed it. 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; } // Every segment change, from [ / ] or the automatic cycle — this is what guarantees no segment is missed. const onSegment = (e) => { if (gig && e && e.detail) observe(e.detail.seg); }; if (typeof window !== 'undefined') window.addEventListener('procity:segment', onSegment); function tick() { if (!gig || !lighting || !lighting.getClock) return (state = 'quiet'); return observe(lighting.getClock().seg); } tick(); // settle on the boot segment return { get state() { return tick(); }, update: tick, // shell calls this once per street frame (same thing) get on() { return tick() === 'on'; }, get open() { const s = tick(); return s === 'doors' || s === 'on'; }, // the door is taking punters get gig() { return gig; }, get venueShopId() { return gig ? gig.venueShopId : null; }, get bandName() { return gig ? gig.bandName : null; }, get cover() { return gig ? gig.cover | 0 : 0; }, get night() { return night; }, // cover stamp — runtime only (like the wallet itself): 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; }, dispose() { if (typeof window !== 'undefined') window.removeEventListener('procity:segment', onSegment); }, }; }