F last: five landed lanes → one Friday night. Seed 20261990 → The Thornbury Hotel,
tonight SCREAMING UTES, $10 on the door. qa.sh --strict GREEN 6/6.
- gig_state.js (new, F-owned): quiet → doors (DUSK) → on (NIGHT) → done (DAWN) → quiet.
Rides lighting's procity:segment event (a poll only sees the segments it samples, and
missed the night roll entirely); `state` is a getter because B's audio reads it from its
own rAF while the player is inside a shop.
- ?gigs=1 seam: custom_bands.json fetched in the bootstrap (no fetch flag-off or ?noassets)
→ generatePlanFor(seed, src, {gigs:true, customBands}). 7 of John's 10 names fill the week.
- Cover charge on the R8 buy seam, zero new UI: paid debits once with a toast, re-entry the
same night is free, broke → polite knockback, free nights walk straight in.
- interior_mode: opts.gig → C's gigKey; D's GigCrew (band 3 + crowd 8, 3 dancing) at C's
stage/watch points; both disposed with the room (leak-free over 4 cycles).
- Cross-lane fix at the glue: C emits gigKey 'gig-pubrock', E shipped the bed as
'pubrock-live' — unresolved the pub plays to a silent room and nothing errors. Bridged
here; C/E/Fable owe CITY_SPEC a contract line. Details + 2 more findings in LANE_F_NOTES §12.
- smoke_gigs (16 gates): schedule determinism, band==3, crowd ≤ watchPoints, dance mix, bed
resolves, ≤350 draws (47 rig / 171 placeholder), no-giants by STATURE (the band stands on a
0.32m deck, so world-crown fails a correct band), cover ruling, ?noassets+?gigs.
Flags-off regression gains a v3 arm: no state machine, no plan.gigs, no pub, no venue geo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
6.4 KiB
JavaScript
99 lines
6.4 KiB
JavaScript
// 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); },
|
||
};
|
||
}
|