PROCITY/web/js/world/gig_state.js
jing 2498fdc892 Lane F R12: wire the v3.0-alpha gig layer (?gigs=1) — state machine, cover charge, band+crowd, gig audio
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>
2026-07-15 21:09:56 +10:00

99 lines
6.4 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 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 startSeg1 (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); },
};
}