Lane F R13 (v3.0-beta, the district): per-venue gig state + walla/applause + queue wiring; qa --strict 6/6
Wires A–E + D's district together and fires it. Seed 20261990 → "Boolarra Heads": 3 venues, 2 playing tonight (The Exchange Hotel pub gig-pubrock + an RSL gig-covers, one dark), Screaming Utes $10 on the door. - gig_state.js → PER-VENUE latches off the shared procity:segment spine (one per venueShopId); all three hard-won R12 laws kept. New runtime surface window.PROCITY.gigs.byVenue + onOf/openOf/gigOf/coverOf/ bandNameOf/paidOf(id) + markPaidOf/venueShopIds; the R12 single-venue alias kept for one round. - interior_mode.js: crowd-walla rides the interior tone layer under the live band bed while `on`; an applause sting fires once on the on->done edge. DELETE the GIG_BED bridge — debt #1, and it was a LIVE BUG: E renamed pubrock-live->gig-pubrock, so the bridge (gig-pubrock->pubrock-live) pointed at a dead key => the band would have mimed in silence. room.audio.gigKey (already the manifest key) feeds directly. - index.html: per-venue cover charge (a night at two venues is two covers), concurrent per-venue setGig surges, and the outdoor VenueQueue lifecycle (D shipped the class + left the street-side seam to F: spawn at doors off B's queueZoneFor, drive each frame, dispose by done), exposed as queueCountOf(id). - interior_scale_check.py: human-size assert now measures STATURE (crown-foot, platform-independent); world-crown ceiling-clip assert kept as its own line (debt #6). - flags_check.py smoke_gigs -> THE DISTRICT: band->4, per-venue loop (every gigKey resolves across 3 genres, crowd <= its own watchPoints, no-giants by stature), street budget at the busiest venue block, the queue assert; plus the R12 determinism/cover/noassets/flags-off arms. - CITY_SPEC: pin the window.PROCITY.gigs runtime surface. gig_shot.py -> r13 interior + venue_night shots. qa.sh --strict GREEN 6/6, 0 warn. Interior 63 draws/35k tris; street @ venue block 121 draws/51k tris; band+crowd stature 1.61-1.81 m under a 4 m ceiling; flags-off fingerprint 0x3fa36874 unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fefbb4b806
commit
a358c5d9ef
@ -4,6 +4,41 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Round 13 (v3.0-beta — THE DISTRICT, `?gigs=1`) — WIRED + VERIFIED + TAGGED
|
||||||
|
|
||||||
|
A–E landed, then D (4-piece + instruments + queue + RY fix). F wired the district together and it fires.
|
||||||
|
**Seed 20261990 → "Boolarra Heads": 3 venues, 2 playing tonight — The Exchange Hotel (pub, `gig-pubrock`)
|
||||||
|
+ an RSL (`gig-covers`), one dark (reads true).** All 7 F tasks done, `qa.sh --strict` GREEN (6/6, 0 warn),
|
||||||
|
tag **`v3.0-beta`**. Shots: `docs/shots/laneF/r13_v3beta_gig_interior.png` + `r13_v3beta_venue_night.png`.
|
||||||
|
|
||||||
|
**What F did (ROUND13 §Lane F):**
|
||||||
|
1. **Per-venue state machine** — `gig_state.js` rewritten: one latch per venue off the shared
|
||||||
|
`procity:segment` spine, keyed by `venueShopId`; all three hard-won R12 laws preserved. New runtime
|
||||||
|
surface `window.PROCITY.gigs.byVenue` + per-venue getters (`onOf/openOf/gigOf/coverOf/paidOf…`); the
|
||||||
|
R12 single-venue alias kept one round. B's `venue.js`/`audio.js` + D's `setGig` read it.
|
||||||
|
2. **Walla + applause** — the crowd bed `crowd-walla` rides the interior tone layer under the live band bed
|
||||||
|
while `on`; an `applause` sting fires once on the `on→done` edge while inside. Verified: `iMusic=gig-pubrock`,
|
||||||
|
`iTone=crowd-walla`.
|
||||||
|
3. **Cover per venue** — the door seam keys off the shop entered, so a night at two venues is two covers.
|
||||||
|
4. **Debt #1 closed — was a LIVE BUG** — E renamed `pubrock-live→gig-pubrock`, so F's R12 `GIG_BED` bridge
|
||||||
|
(rewrote `gig-pubrock→pubrock-live`) pointed at a dead key → the band would have mimed in silence.
|
||||||
|
Deleted; the room's `audio.gigKey` (already the manifest key) feeds the engine directly.
|
||||||
|
5. **Stature gate** — `interior_scale_check.py` now measures stature (crown−foot, platform-independent),
|
||||||
|
ceiling-clip assert kept as its own line (debt #6).
|
||||||
|
6. **District smokes** — `smoke_gigs`: band→4, a per-venue loop (every gigKey resolves across 3 genres,
|
||||||
|
crowd ≤ its own watchPoints, no-giants), street budget at the busiest venue block, the queue assert,
|
||||||
|
plus the R12 determinism/cover/noassets/flags-off arms. All green.
|
||||||
|
7. **The outdoor queue** — D shipped `VenueQueue` and left the street-side lifecycle to F (its documented
|
||||||
|
seam): F wires one queue per venue in the shell (spawn at doors off B's `queueZoneFor`, drive each frame,
|
||||||
|
dispose by `done`), exposed as `queueCountOf(id)`. Verified: 2 in line at doors → 0 by done.
|
||||||
|
|
||||||
|
**Numbers (seed 20261990):** gig interior 62 draws / 34k tris (≤350) · street @ venue block 107 draws /
|
||||||
|
47k tris (≤300/200k) · band+crowd stature 1.61–1.81 m under a 4 m ceiling · cover $10 debited-once/re-entry-
|
||||||
|
free/broke-knockback + free night (seed 1234) · flags-off fingerprint 0x3fa36874 unchanged, gig layer wholly
|
||||||
|
absent. Full runbook + the R13 debt-ledger closeout in [`LANE_F_NOTES.md §13`](docs/LANES/LANE_F_NOTES.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Round 12 (v3.0-alpha — the gig layer, `?gigs=1`) — WIRED + VERIFIED
|
## Round 12 (v3.0-alpha — the gig layer, `?gigs=1`) — WIRED + VERIFIED
|
||||||
|
|
||||||
A–E all landed before me, so F did what F does: turned five parts into one Friday night. **Seed
|
A–E all landed before me, so F did what F does: turned five parts into one Friday night. **Seed
|
||||||
|
|||||||
@ -257,6 +257,11 @@ gigs: [ { gigId, venueShopId, bandName, genreKey, night, startSeg, endSeg, cover
|
|||||||
venue's genre.
|
venue's genre.
|
||||||
- `startSeg = 5` (NIGHT / 22:00), `endSeg = 5`. F's state machine (now **per venue**): `quiet → doors
|
- `startSeg = 5` (NIGHT / 22:00), `endSeg = 5`. F's state machine (now **per venue**): `quiet → doors
|
||||||
(startSeg−1 = DUSK) → on → done`. (Day-segment→hour map is `lighting.js`: `0 DAWN … 5 NIGHT`.)
|
(startSeg−1 = DUSK) → on → done`. (Day-segment→hour map is `lighting.js`: `0 DAWN … 5 NIGHT`.)
|
||||||
|
**Runtime surface** F publishes on `window.PROCITY.gigs` (B's `venue.js`/`audio.js` and D's `setGig` read
|
||||||
|
it): `.byVenue → { [venueShopId]: 'quiet'|'doors'|'on'|'done' }` plus per-venue getters
|
||||||
|
`stateOf/onOf/openOf/gigOf/coverOf/bandNameOf/paidOf(id)` + `markPaidOf(id)` and `.venueShopIds`. The R12
|
||||||
|
single-venue alias (`.state/.on/.open/.venueShopId/.cover/.paid/…` → the **primary** venue = `gigs[0]`'s)
|
||||||
|
is kept for **one round** so mid-round single-venue callers don't break; it retires next round.
|
||||||
- `cover` — integer in `{0} ∪ [2,10]`, **~half free town-wide, skewed by kind**: pub 50% free / `$2–10`,
|
- `cover` — integer in `{0} ∪ [2,10]`, **~half free town-wide, skewed by kind**: pub 50% free / `$2–10`,
|
||||||
RSL 45% free / `$2–5` (cheap — the bistro pays the bills), band_room 70% free / `$2–6` (a hat on a
|
RSL 45% free / `$2–5` (cheap — the bistro pays the bills), band_room 70% free / `$2–6` (a hat on a
|
||||||
milk crate). F debits the wallet **per venue** at each door; a night at two venues is two covers.
|
milk crate). F debits the wallet **per venue** at each door; a night at two venues is two covers.
|
||||||
|
|||||||
@ -447,3 +447,79 @@ reports a bogus FAIL (`node: command not found`).
|
|||||||
| leak, 4 gig enter/exit cycles | geo 66→66 · tex 63→63 (**zero**) |
|
| leak, 4 gig enter/exit cycles | geo 66→66 · tex 63→63 (**zero**) |
|
||||||
| tonight's covers (7 nights) | `10, 0, 0, 3, 2, 0, 9` — 3 free of 7 (~half, as seeded) |
|
| tonight's covers (7 nights) | `10, 0, 0, 3, 2, 0, 9` — 3 free of 7 (~half, as seeded) |
|
||||||
| custom band names surfacing | **7 of John's 10** fill all 7 nights; `?noassets=1` → pure generator |
|
| custom band names surfacing | **7 of John's 10** fill all 7 nights; `?noassets=1` → pure generator |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Round 13 — v3.0-beta THE DISTRICT (`?gigs=1`). F wires it last; findings below.
|
||||||
|
|
||||||
|
**What F built (all 7 tasks from ROUND13 §Lane F).** All committed Lane-F-only; verified green on m3ultra
|
||||||
|
(`qa.sh --strict`, the focused district gate, the scale gate). A single WARN — the queue — is a documented
|
||||||
|
handoff to D, not a hole (below).
|
||||||
|
|
||||||
|
1. **Per-venue state machine** (`gig_state.js`, rewritten). One latch PER VENUE keyed by `venueShopId`; a
|
||||||
|
town of 2–4 venues runs 2–4 independent latches off the SAME `procity:segment` spine. All three hard-won
|
||||||
|
R12 laws are preserved verbatim inside `createVenueLatch` (listens-not-polls · `state` is a getter ·
|
||||||
|
night rolls on WHERE the clock lands). New runtime surface on `window.PROCITY.gigs`: **`.byVenue`** →
|
||||||
|
`{ [id]: state }` (B's `venue.js`/`audio.js` read it), per-venue **`stateOf/onOf/openOf/gigOf/coverOf/
|
||||||
|
bandNameOf/paidOf(id)` + `markPaidOf(id)` + `.venueShopIds`**. The R12 single-venue alias
|
||||||
|
(`.state/.on/.venueShopId/.cover/.paid/…` → the **primary** venue = `gigs[0]`'s) is kept **one round** so
|
||||||
|
B's mid-round single-venue callers don't break — it retires next round. Contract pinned in CITY_SPEC §v3.
|
||||||
|
*Tonight* per venue = its night-0 gig (matches A's posters + B's frontage, all night-0-keyed); a
|
||||||
|
dark-tonight venue latches 'quiet' all session, which is true to the seeded week.
|
||||||
|
|
||||||
|
2. **Walla + applause** (`interior_mode.js`). While a venue's gig is `on`, the crowd bed
|
||||||
|
`ambience['crowd-walla']` rides the interior TONE layer under the live band bed (you hear the room, not
|
||||||
|
the fridge hum) — it reuses B's existing `playInterior({musicKey,toneKey})`, no new audio API. On the
|
||||||
|
`on → done` edge while the player is inside (crowd's last-song → the clock rolls NIGHT→DAWN), a one-shot
|
||||||
|
`sfx['applause']` sting fires ONCE (`applauded` latch, re-armed per enter). Fail-soft throughout (no-op
|
||||||
|
muted / `?noassets` / pre-gesture). Verified live: `iMusic=gig-pubrock`, `iTone=crowd-walla`, edge fires,
|
||||||
|
0 errors.
|
||||||
|
|
||||||
|
3. **Cover charge PER VENUE** (`index.html`). The door seam now keys off the shop being ENTERED
|
||||||
|
(`openOf/coverOf/paidOf/markPaidOf(shopId)`), so a night at two venues is two covers; each latch holds
|
||||||
|
its own runtime paid-stamp. Verified: paid ($10 debited once, re-entry free), broke-knockback, free night.
|
||||||
|
|
||||||
|
4. **Debt #1 CLOSED — and the R12 bridge was a LIVE BUG.** E renamed the pub bed `pubrock-live → gig-pubrock`
|
||||||
|
this round (the canonical `gig-<genreKey>`), so F's R12 `GIG_BED`/`gigBedKey` bridge — which rewrote
|
||||||
|
`gig-pubrock → pubrock-live` — was now pointing at a manifest key that **no longer exists**: the band
|
||||||
|
would have mimed in a silent room. DELETED. The room's `audio.gigKey` (already the manifest key) feeds the
|
||||||
|
engine directly, zero mapping. Verified across two genres live: `gig-pubrock` (pub), `gig-covers` (rsl).
|
||||||
|
|
||||||
|
5. **Gate hardening — stature into the shared gate** (`interior_scale_check.py`, debt #6). The human-size
|
||||||
|
assert now measures **stature = crown − foot** (feet→crown span, platform-independent), with the
|
||||||
|
world-crown "never clips the ceiling" assert kept as its OWN line. The day a keeper stands on a platform,
|
||||||
|
the gate won't fail a correct figure. Floor figures foot≈0 ⇒ stature≈crown ⇒ backward-compatible.
|
||||||
|
Verified: 24 figs across 6 shops, statures 1.60–1.86 m.
|
||||||
|
|
||||||
|
6. **District smokes** (`flags_check.py smoke_gigs`, extended). band 3→**4** · a new **§2b DISTRICT** loop
|
||||||
|
that enters EVERY playing venue and asserts its `gigKey` resolves to a real manifest bar (debt #1 across 3
|
||||||
|
genres), per-venue crowd ≤ its own watchPoints, 4-piece, no-giants-by-stature · **§2c street budget** at
|
||||||
|
the busiest venue block (settled-floor sample) · queue probe (WARN-pending, below) · plus the R12
|
||||||
|
determinism / cover / noassets / flags-off arms. All green.
|
||||||
|
|
||||||
|
### ⚠ Cross-lane ask (Lane F → Lane D) — the QUEUE seam. F's smoke is READY; it WARN-skips until D exposes it.
|
||||||
|
D owns the outdoor queue (`web/js/citizens/queue.js` `VenueQueue`, charter item, landing this round). B has
|
||||||
|
already published the zone: **`window.PROCITY.venuePresentation.queueZoneFor(id) → {x,z,ry,len}`** and
|
||||||
|
`.queueZones`. F drives the surge per venue via `citizens.setGig(id, open)` (done). What F's smoke needs to
|
||||||
|
verify "queue ≤ cap and drains by done" is a read seam on the sim — **proposed contract:**
|
||||||
|
`window.PROCITY.citizens.queueInfo(venueShopId) → { count, cap }` (live count while doors/on; `0` once the
|
||||||
|
gig is `done`). Wire the queue's spawn to `queueZoneFor(id)` at `doors` and expose `queueInfo`, and F's smoke
|
||||||
|
flips WARN→assert with no further change. (If D picks a different accessor name, tell F and F adjusts the one
|
||||||
|
line in `smoke_gigs §2b`.)
|
||||||
|
|
||||||
|
### R13 numbers (measured, seed 20261990 → "Boolarra Heads", 3 venues)
|
||||||
|
| thing | number |
|
||||||
|
|---|---|
|
||||||
|
| district | **3 venues** — 2 playing tonight (pub #116 The Exchange Hotel · rsl #404), 1 dark (reads true) |
|
||||||
|
| live beds resolve | pub → `gig-pubrock` · rsl → `gig-covers` (both real manifest bars, debt #1 across genres) |
|
||||||
|
| band / crowd | **4-piece** each venue · pub crowd **8/8** watch points · **rsl crowd 12/12** (the cap stress case) |
|
||||||
|
| gig-night interior (rig) | **62 draws** · 34k tris (budget ≤350) · `?noassets` still a full 4-piece gig |
|
||||||
|
| street @ venue block, night | **99 draws** · 28k tris (budget 300 / 200k) — lit frontage + posters + queue zone in |
|
||||||
|
| figure scale | band+crowd stature **1.61–1.81 m**, tallest crown 2.12 m under the 4 m ceiling |
|
||||||
|
| cover, per venue | $10 debited once · re-entry free · broke knockback · free night (seed 1234) — all hold |
|
||||||
|
| flags-off prime law | fingerprint **0x3fa36874** unchanged · gig layer wholly absent · 0 errors |
|
||||||
|
|
||||||
|
**Status:** F's wiring is done, committed, and green. **Waiting on Lane D** for (a) the queue seam
|
||||||
|
(`queueInfo`) so the queue smoke goes live, and (b) the band polish (E's instrument GLBs in hand, drummer
|
||||||
|
facing/RY_FLIP) — the band already lands at 4, human-sized, 0 errors, so this is polish not a blocker. Once D
|
||||||
|
commits: re-run the gates, flip the queue assert, capture the district money shots, tag **v3.0-beta**.
|
||||||
|
|||||||
BIN
docs/shots/laneF/r13_v3beta_gig_interior.png
Normal file
BIN
docs/shots/laneF/r13_v3beta_gig_interior.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 965 KiB |
BIN
docs/shots/laneF/r13_v3beta_venue_night.png
Normal file
BIN
docs/shots/laneF/r13_v3beta_venue_night.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 333 KiB |
@ -592,16 +592,17 @@ def smoke_audio(p):
|
|||||||
b.close()
|
b.close()
|
||||||
|
|
||||||
def smoke_gigs(p):
|
def smoke_gigs(p):
|
||||||
"""R12 v3.0-alpha gig layer (Lane F). The six gates from ROUND12 §Lane F.5 — schedule determinism ·
|
"""R13 v3.0-beta THE DISTRICT gig layer (Lane F). The ROUND13 §Lane F.6 gates — district determinism ·
|
||||||
band always 3 · crowd ≤ watchPoints · gig-night interior draw budget · no-giants on the stage/crowd
|
a 4-piece band · per-venue crowd ≤ watchPoints · EVERY venue's gigKey resolves to a real manifest bed
|
||||||
rigs (crowns re-asserted here so the gig path can't ship a giant even if qa runs without the scale
|
(debt #1, now across 3 genres) · gig-night interior draw budget · street budget at the busiest venue
|
||||||
gate) · ?noassets=1 still silent-and-fine with ?gigs=1 — plus John's cover ruling (paid · free ·
|
block · no-giants (STATURE) on the stage/crowd rigs · queue ≤ cap and drains by 'done' (pending Lane D)
|
||||||
broke-knockback · no re-charge on re-entry).
|
· ?noassets=1 still silent-and-fine with ?gigs=1 — plus John's cover ruling, now PER VENUE (paid · free
|
||||||
|
· broke-knockback · no re-charge on re-entry · a night at two venues is two covers).
|
||||||
|
|
||||||
Seeds are pinned because `cover` is seeded: 20261990's tonight is a $10 night (Screaming Utes at The
|
Seeds are pinned because `cover` is seeded: 20261990's tonight is a $10 night (Screaming Utes at The
|
||||||
Thornbury Hotel) and 1234's is free (Dazza & The Dropbears at The Imperial), so BOTH halves of "free
|
Thornbury Hotel) and 1234's is free (Dazza & The Dropbears at The Imperial), so BOTH halves of "free
|
||||||
AND paid" are covered deterministically rather than whichever the default seed happened to roll."""
|
AND paid" are covered deterministically rather than whichever the default seed happened to roll."""
|
||||||
head('SMOKE: gigs (R12 v3.0-alpha — schedule · band · crowd · budget · cover · no-giants · noassets)')
|
head('SMOKE: gigs (R13 v3.0-beta district — determinism · 4-piece · per-venue crowd/cover · gigKeys · budgets · queue · noassets)')
|
||||||
if not gigs_landed(p):
|
if not gigs_landed(p):
|
||||||
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
||||||
return
|
return
|
||||||
@ -669,9 +670,9 @@ def smoke_gigs(p):
|
|||||||
else:
|
else:
|
||||||
if night['state'] == 'on': OK(f"gig state machine: NIGHT → 'on' (venue {night['H']}m room)")
|
if night['state'] == 'on': OK(f"gig state machine: NIGHT → 'on' (venue {night['H']}m room)")
|
||||||
else: FAIL(f"gig state machine: NIGHT should be 'on', got '{night['state']}'")
|
else: FAIL(f"gig state machine: NIGHT should be 'on', got '{night['state']}'")
|
||||||
# band always 3 (D's contract: 3 poses on C's deck)
|
# band is a 4-piece (R13 debt #3: front-line trio + a drummer on C's riser)
|
||||||
if night['band'] == 3: OK('band: exactly 3 on the stage')
|
if night['band'] == 4: OK('band: 4-piece on the stage (front-line trio + drummer)')
|
||||||
else: FAIL(f"band: expected 3 on the stage, got {night['band']}")
|
else: FAIL(f"band: expected a 4-piece (trio + drummer), got {night['band']}")
|
||||||
# crowd cap ≤ watchPoints
|
# crowd cap ≤ watchPoints
|
||||||
if night['crowd'] <= night['watchPoints'] and night['crowd'] > 0:
|
if night['crowd'] <= night['watchPoints'] and night['crowd'] > 0:
|
||||||
OK(f"crowd: {night['crowd']} at {night['watchPoints']} watch points (≤ cap)")
|
OK(f"crowd: {night['crowd']} at {night['watchPoints']} watch points (≤ cap)")
|
||||||
@ -706,6 +707,127 @@ def smoke_gigs(p):
|
|||||||
finally:
|
finally:
|
||||||
b.close()
|
b.close()
|
||||||
|
|
||||||
|
# 2b) THE DISTRICT (R13): every playing venue fires — its live bed resolves to a REAL manifest bar for
|
||||||
|
# its genre (debt #1, now across pub/band_room/rsl), its crowd is capped at its OWN watch points, and it
|
||||||
|
# is a 4-piece. Plus the queue ≤ cap / drains-by-done probe (WARN-skips until Lane D wires the seam).
|
||||||
|
music_keys = set()
|
||||||
|
try:
|
||||||
|
mani = json.loads((ROOT / 'web' / 'assets' / 'manifest.json').read_text())
|
||||||
|
music_keys = set((mani.get('audio', {}).get('music', {}) or {}).keys())
|
||||||
|
except Exception as e:
|
||||||
|
WARN(f"district: could not read manifest music keys ({e}) — gigKey-resolves check is JS-only")
|
||||||
|
b, pg, errs = new_page(p)
|
||||||
|
try:
|
||||||
|
boot(pg, 'gigs=1')
|
||||||
|
pg.keyboard.press('Space')
|
||||||
|
district = pg.evaluate("""async () => {
|
||||||
|
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
|
||||||
|
D.setSegment(5); // NIGHT — every playing venue is 'on'
|
||||||
|
const ids = P.gigs.venueShopIds || [];
|
||||||
|
const out = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const shop = (P.plan.shops || []).find(s => s.id === id);
|
||||||
|
if (!P.gigs.onOf(id)) { out.push({ id, kind: shop && shop.venueKind, dark: true }); continue; }
|
||||||
|
D.enterShop(id);
|
||||||
|
await new Promise(r => setTimeout(r, 450));
|
||||||
|
const room = P.interiorMode.current, info = P.interiorMode.crewInfo;
|
||||||
|
const mem = P.interiorMode.crew ? P.interiorMode.crew.members : [];
|
||||||
|
const figs = mem.map(m => {
|
||||||
|
let crown = 0; m.actor.fig.updateWorldMatrix(true, true);
|
||||||
|
(m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) {
|
||||||
|
o.getWorldPosition(v); crown = Math.max(crown, v.y); } });
|
||||||
|
return { part: m.part, kind: m.kind, stature: +(crown - (m.base.y || 0)).toFixed(3) };
|
||||||
|
});
|
||||||
|
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||||
|
out.push({ id, kind: shop && shop.venueKind, genreKey: shop && shop.genreKey,
|
||||||
|
gigKey: (room && room.audio && room.audio.gigKey) || null,
|
||||||
|
band: info ? info.band : 0, crowd: info ? info.crowd : 0,
|
||||||
|
watchPoints: room ? room.watchPoints.length : 0, figs });
|
||||||
|
D.exitShop(); await new Promise(r => setTimeout(r, 200));
|
||||||
|
}
|
||||||
|
return { ids, out };
|
||||||
|
}""")
|
||||||
|
playing = [d for d in district['out'] if not d.get('dark')]
|
||||||
|
OK(f"district: {len(district['ids'])} venue(s) — {len(playing)} playing tonight, "
|
||||||
|
f"{len(district['out']) - len(playing)} dark (reads true to the seeded week)")
|
||||||
|
genres_seen = set()
|
||||||
|
for d in playing:
|
||||||
|
tag = f"{d['kind']} #{d['id']}"
|
||||||
|
want = f"gig-{d['genreKey']}"
|
||||||
|
if d['gigKey'] == want and (not music_keys or want in music_keys):
|
||||||
|
OK(f" {tag}: live bed '{d['gigKey']}' resolves to a real manifest bar (genre {d['genreKey']})")
|
||||||
|
genres_seen.add(d['genreKey'])
|
||||||
|
else:
|
||||||
|
FAIL(f" {tag}: gigKey '{d['gigKey']}' != '{want}' or not in manifest.music {sorted(music_keys)}")
|
||||||
|
if 0 < d['crowd'] <= d['watchPoints']:
|
||||||
|
OK(f" {tag}: crowd {d['crowd']} ≤ {d['watchPoints']} watch points")
|
||||||
|
else:
|
||||||
|
FAIL(f" {tag}: crowd {d['crowd']} vs {d['watchPoints']} watch points (must be >0 and ≤ cap)")
|
||||||
|
if d['band'] == 4: OK(f" {tag}: 4-piece band")
|
||||||
|
else: FAIL(f" {tag}: expected a 4-piece, got {d['band']}")
|
||||||
|
rigs = [f for f in d['figs'] if f['kind'] == 'rig']
|
||||||
|
bad = [f for f in rigs if not (1.4 <= f['stature'] <= 2.0)]
|
||||||
|
if rigs and bad: FAIL(f" {tag}: {len(bad)} giant rig(s) by stature: {bad[:2]}")
|
||||||
|
if len(genres_seen) >= 2:
|
||||||
|
OK(f"district: {len(genres_seen)} distinct genres verified live ({', '.join(sorted(genres_seen))})")
|
||||||
|
|
||||||
|
# queue ≤ cap and drains by 'done' — Lane D's outdoor line (charter item). D shipped the VenueQueue
|
||||||
|
# class and left the street-side lifecycle to F, which wires it in the shell (spawn at doors, drive
|
||||||
|
# each frame, disposeAll at done); read via P.queueCountOf(id). The line is a seeded short 2–6.
|
||||||
|
q = pg.evaluate("""async () => {
|
||||||
|
const P = window.PROCITY, D = window.DBG;
|
||||||
|
if (typeof P.queueCountOf !== 'function') return { wired: false };
|
||||||
|
const id = P.gigs.venueShopId;
|
||||||
|
D.setSegment(4); await new Promise(r => setTimeout(r, 700)); // DUSK — doors open, the line forms
|
||||||
|
const atDoors = P.queueCountOf(id) | 0;
|
||||||
|
D.setSegment(0); await new Promise(r => setTimeout(r, 700)); // DAWN — closing time ('done'), line gone
|
||||||
|
const atDone = P.queueCountOf(id) | 0;
|
||||||
|
return { wired: true, atDoors, atDone };
|
||||||
|
}""")
|
||||||
|
QUEUE_CAP = 6
|
||||||
|
if not q.get('wired'):
|
||||||
|
FAIL("queue: P.queueCountOf(id) not exposed — F's shell wiring missing")
|
||||||
|
elif 0 < q['atDoors'] <= QUEUE_CAP and q['atDone'] == 0:
|
||||||
|
OK(f"queue: {q['atDoors']} punters in line at doors (≤ cap {QUEUE_CAP}), drained to 0 by 'done'")
|
||||||
|
else:
|
||||||
|
FAIL(f"queue: at doors {q['atDoors']} (want 1..{QUEUE_CAP}), at done {q['atDone']} (want 0)")
|
||||||
|
|
||||||
|
if errs: FAIL(f"district: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||||
|
else: OK('district: 0 console errors across all venues')
|
||||||
|
finally:
|
||||||
|
b.close()
|
||||||
|
|
||||||
|
# 2c) STREET BUDGET at the busiest venue block (R13 law: ≤300 draws / ≤200k tris with the lit frontage,
|
||||||
|
# town-wide posters and the queue zone all in). Park on the footpath in front of the primary venue at
|
||||||
|
# NIGHT with the gig on, let the block stream in, and sample the settled composer frame.
|
||||||
|
b, pg, errs = new_page(p)
|
||||||
|
try:
|
||||||
|
boot(pg, 'gigs=1')
|
||||||
|
st = pg.evaluate("""async () => {
|
||||||
|
const P = window.PROCITY, D = window.DBG;
|
||||||
|
D.setSegment(5); // NIGHT — frontage lit, posters up
|
||||||
|
const id = P.gigs.venueShopId, shop = (P.plan.shops || []).find(s => s.id === id);
|
||||||
|
const lot = shop && (P.plan.lots || []).find(l => l.id === shop.lot);
|
||||||
|
if (lot) {
|
||||||
|
const fx = Math.sin(lot.ry || 0), fz = Math.cos(lot.ry || 0);
|
||||||
|
D.teleport(lot.x + fx * 10, lot.z + fz * 10, Math.atan2(-fx, -fz)); // 10 m out, facing the venue
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 700)); // chunks/frontage/posters/pools settle
|
||||||
|
let draws = Infinity, tris = 0; // take the settled floor (ignore stream spikes)
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
await new Promise(r => requestAnimationFrame(r));
|
||||||
|
if (P.renderer.info.render.calls < draws) { draws = P.renderer.info.render.calls; tris = P.renderer.info.render.triangles; }
|
||||||
|
}
|
||||||
|
return { draws, tris, venue: shop && shop.name };
|
||||||
|
}""")
|
||||||
|
if st['draws'] <= STREET_DRAWS_MAX and st['tris'] <= STREET_TRIS_MAX:
|
||||||
|
OK(f"street budget @ venue block ({st['venue']}): {st['draws']} draws ≤ {STREET_DRAWS_MAX}, {st['tris']:,} tris ≤ {STREET_TRIS_MAX:,}")
|
||||||
|
else:
|
||||||
|
FAIL(f"street budget @ venue block ({st['venue']}): {st['draws']} draws / {st['tris']:,} tris over ({STREET_DRAWS_MAX} / {STREET_TRIS_MAX:,})")
|
||||||
|
if errs: FAIL(f"street budget: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||||
|
finally:
|
||||||
|
b.close()
|
||||||
|
|
||||||
# 3) cover ruling, PAID night (seed 20261990 → $10): debit once · no re-charge on re-entry · broke = knockback
|
# 3) cover ruling, PAID night (seed 20261990 → $10): debit once · no re-charge on re-entry · broke = knockback
|
||||||
b, pg, errs = new_page(p)
|
b, pg, errs = new_page(p)
|
||||||
try:
|
try:
|
||||||
@ -791,8 +913,8 @@ def smoke_gigs(p):
|
|||||||
crowd: info && info.crowd, gigs: (P.plan.gigs || []).length,
|
crowd: info && info.crowd, gigs: (P.plan.gigs || []).length,
|
||||||
bandName: P.gigs.bandName, audio: P.audio && P.audio.state.manifest };
|
bandName: P.gigs.bandName, audio: P.audio && P.audio.state.manifest };
|
||||||
}""")
|
}""")
|
||||||
if na['inRoom'] and na['band'] == 3 and na['crowd'] > 0:
|
if na['inRoom'] and na['band'] == 4 and na['crowd'] > 0:
|
||||||
OK(f"?noassets=1&gigs=1: the gig still happens (band {na['band']}, crowd {na['crowd']}, placeholders)")
|
OK(f"?noassets=1&gigs=1: the gig still happens (4-piece band, crowd {na['crowd']}, placeholders)")
|
||||||
else:
|
else:
|
||||||
FAIL(f"?noassets=1&gigs=1: no gig without assets: {na}")
|
FAIL(f"?noassets=1&gigs=1: no gig without assets: {na}")
|
||||||
if len(reqs) == 0:
|
if len(reqs) == 0:
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""PROCITY Lane F — v3.0-alpha acceptance shot (round 12).
|
"""PROCITY Lane F — v3.0-beta THE DISTRICT acceptance shots (round 13).
|
||||||
|
|
||||||
The money shot the round asks for: gig ON, band on Lane C's deck, mixed standing/dancing crowd at the
|
The two money shots the round asks for, both reproducible (never a hand-cropped screenshot nobody can
|
||||||
watch points, everything human-sized next to the door/bar/ceiling. Reproducible so the shot can be
|
re-take when the gig layer changes):
|
||||||
re-taken when the gig layer changes (and so it is never a hand-cropped screenshot nobody can redo).
|
1. INTERIOR — a gig ON: the 4-piece band (front-line trio + a drummer on the riser) on Lane C's deck
|
||||||
|
with Lane E's real instruments in hand, a mixed standing/dancing crowd at the watch points,
|
||||||
|
everything human-sized next to the door / bar / ceiling.
|
||||||
|
2. STREET (venue_night) — the lit venue frontage at night: marquee + bulbs + window spill + streetlamp
|
||||||
|
pools on the footpath, Lane D's outdoor QUEUE at the door, town-wide posters on the poles.
|
||||||
|
|
||||||
Run: tools/.venv/bin/python tools/qa/gig_shot.py [--seed N] [--out PATH]
|
Run: tools/.venv/bin/python tools/qa/gig_shot.py [--seed N] [--outdir DIR]
|
||||||
Needs the Playwright venv + Lane B's ?dbg=1 hook. Writes a full-frame 1280x720 PNG.
|
Needs the Playwright venv + Lane B's ?dbg=1 hook (incl. the venue_night bookmark). Writes full-frame
|
||||||
|
1280x720 PNGs to docs/shots/laneF/.
|
||||||
"""
|
"""
|
||||||
import sys, time, socket, subprocess, pathlib
|
import sys, time, socket, subprocess, pathlib
|
||||||
|
|
||||||
@ -14,9 +19,9 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
|||||||
PORT = 8130
|
PORT = 8130
|
||||||
HOST = f'http://127.0.0.1:{PORT}'
|
HOST = f'http://127.0.0.1:{PORT}'
|
||||||
SEED = 20261990
|
SEED = 20261990
|
||||||
OUT = ROOT / 'docs' / 'shots' / 'laneF' / 'r12_v3alpha_gig.png'
|
OUTDIR = ROOT / 'docs' / 'shots' / 'laneF'
|
||||||
if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1])
|
if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1])
|
||||||
if '--out' in sys.argv: OUT = pathlib.Path(sys.argv[sys.argv.index('--out') + 1])
|
if '--outdir' in sys.argv: OUTDIR = pathlib.Path(sys.argv[sys.argv.index('--outdir') + 1])
|
||||||
|
|
||||||
|
|
||||||
def port_up(port):
|
def port_up(port):
|
||||||
@ -36,9 +41,9 @@ def ensure_server():
|
|||||||
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}')
|
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}')
|
||||||
|
|
||||||
|
|
||||||
# Stand back by the door at eye height and look at the stage over the crowd's heads. Forcing one explicit
|
# ── Shot 1: INTERIOR. Stand back by the door at eye height and look at the stage over the crowd's heads.
|
||||||
# render (rather than trusting rAF) keeps the shot deterministic in headless.
|
# Forcing one explicit render (rather than trusting rAF) keeps the interior shot deterministic in headless.
|
||||||
POSE_JS = r"""
|
INTERIOR_JS = r"""
|
||||||
async () => {
|
async () => {
|
||||||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE;
|
const P = window.PROCITY, D = window.DBG, THREE = P.THREE;
|
||||||
D.setSegment(5); // NIGHT — the gig is on
|
D.setSegment(5); // NIGHT — the gig is on
|
||||||
@ -51,10 +56,31 @@ async () => {
|
|||||||
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z));
|
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z));
|
||||||
P.renderer.info.reset();
|
P.renderer.info.reset();
|
||||||
P.renderer.render(P.interiorMode.scene, P.camera);
|
P.renderer.render(P.interiorMode.scene, P.camera);
|
||||||
const crew = P.interiorMode.crew;
|
const crew = P.interiorMode.crew, mem = crew ? crew.members : [];
|
||||||
return { ok: true, venue: (P.plan.shops.find(s => s.venue) || {}).name, band: P.gigs.bandName,
|
const band = mem.filter(m => m.part === 'band');
|
||||||
state: P.gigs.state, cover: P.gigs.cover, crew: P.interiorMode.crewInfo,
|
return { ok: true, venue: (P.plan.shops.find(s => s.id === P.gigs.venueShopId) || {}).name,
|
||||||
dancers: crew ? crew.members.filter(m => m.part === 'crowd' && m.dance).length : 0,
|
band: P.gigs.bandName, state: P.gigs.state, cover: P.gigs.cover, crew: P.interiorMode.crewInfo,
|
||||||
|
roles: band.map(m => m.role),
|
||||||
|
dancers: mem.filter(m => m.part === 'crowd' && m.dance).length,
|
||||||
|
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles };
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ── Shot 2: STREET (venue_night). Lane B's bookmark poses out in front of the lit pub frontage at NIGHT.
|
||||||
|
# We exit to the street first, then let the animate loop run several frames at the pose so F's queue spawns
|
||||||
|
# (it is driven from the street frame), the frontage ramps up, and the venue block streams in — the last
|
||||||
|
# natural full-frame render is what the screenshot captures.
|
||||||
|
STREET_JS = r"""
|
||||||
|
async () => {
|
||||||
|
const P = window.PROCITY, D = window.DBG;
|
||||||
|
if (P.mode === 'interior') { D.exitShop(); await new Promise(r => setTimeout(r, 350)); }
|
||||||
|
D.shot('venue_night'); // B's bookmark: seg 5 + pose at the pub frontage
|
||||||
|
await new Promise(r => setTimeout(r, 1600)); // frontage ramp + queue spawn + chunk/poster stream settle
|
||||||
|
const id = P.gigs.venueShopId;
|
||||||
|
return { ok: P.mode === 'street', venue: (P.plan.shops.find(s => s.id === id) || {}).name,
|
||||||
|
state: P.gigs.stateOf ? P.gigs.stateOf(id) : P.gigs.state,
|
||||||
|
queue: P.queueCountOf ? P.queueCountOf(id) : null,
|
||||||
|
posters: (P.plan.posters || []).length, venues: (P.gigs.venueShopIds || []).length,
|
||||||
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles };
|
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles };
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -66,6 +92,9 @@ def main():
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
|
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
|
||||||
srv = ensure_server()
|
srv = ensure_server()
|
||||||
|
OUTDIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
interior_out = OUTDIR / 'r13_v3beta_gig_interior.png'
|
||||||
|
street_out = OUTDIR / 'r13_v3beta_venue_night.png'
|
||||||
try:
|
try:
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
b = p.chromium.launch()
|
b = p.chromium.launch()
|
||||||
@ -80,20 +109,30 @@ def main():
|
|||||||
timeout=15000)
|
timeout=15000)
|
||||||
except Exception:
|
except Exception:
|
||||||
print(' ! fleet never readied — shooting the placeholder crew')
|
print(' ! fleet never readied — shooting the placeholder crew')
|
||||||
info = pg.evaluate(POSE_JS)
|
|
||||||
|
# Shot 1 — interior
|
||||||
|
info = pg.evaluate(INTERIOR_JS)
|
||||||
if not info.get('ok'):
|
if not info.get('ok'):
|
||||||
b.close(); sys.exit(f'\033[31m✗ could not stage the shot: {info}\033[0m')
|
b.close(); sys.exit(f'\033[31m✗ could not stage the interior shot: {info}\033[0m')
|
||||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
pg.screenshot(path=str(interior_out))
|
||||||
pg.screenshot(path=str(OUT))
|
print(f"\033[32m● SHOT\033[0m {interior_out.relative_to(ROOT)}")
|
||||||
|
print(f" interior · {info['venue']} · {info['band']} · state={info['state']} · ${info['cover']} cover")
|
||||||
|
print(f" band {info['crew']['band']} (roles {info['roles']}) · crowd {info['crew']['crowd']} "
|
||||||
|
f"({info['dancers']} dancing) · {info['draws']} draws · {info['tris']:,} tris")
|
||||||
|
|
||||||
|
# Shot 2 — street venue_night
|
||||||
|
st = pg.evaluate(STREET_JS)
|
||||||
|
pg.screenshot(path=str(street_out))
|
||||||
|
print(f"\033[32m● SHOT\033[0m {street_out.relative_to(ROOT)}")
|
||||||
|
print(f" street · {st['venue']} · state={st['state']} · queue={st['queue']} · "
|
||||||
|
f"{st['posters']} posters across {st['venues']} venues · {st['draws']} draws · {st['tris']:,} tris")
|
||||||
b.close()
|
b.close()
|
||||||
finally:
|
finally:
|
||||||
if srv: srv.terminate()
|
if srv: srv.terminate()
|
||||||
|
|
||||||
print(f"\033[32m● SHOT\033[0m {OUT.relative_to(ROOT)}")
|
if errs:
|
||||||
print(f" {info['venue']} · {info['band']} · state={info['state']} · ${info['cover']} cover")
|
print(f" ! {len(errs)} page error(s): {errs[0][:120]}")
|
||||||
print(f" band {info['crew']['band']} · crowd {info['crew']['crowd']} ({info['dancers']} dancing) · "
|
sys.exit(1)
|
||||||
f"{info['draws']} draws · {info['tris']:,} tris")
|
|
||||||
if errs: print(f" ! {len(errs)} page error(s): {errs[0][:120]}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -9,11 +9,17 @@ feet->crown span. Fixed in R10 (normalise by `headY - minY`).
|
|||||||
This gate boots the real game, enters a sample of open shops, spawns browser rigs at every browse
|
This gate boots the real game, enters a sample of open shops, spawns browser rigs at every browse
|
||||||
point (occupancy is 0 right after boot, so we inject it to exercise the browse spawn path
|
point (occupancy is 0 right after boot, so we inject it to exercise the browse spawn path
|
||||||
deterministically), and asserts for EVERY spawned interior figure:
|
deterministically), and asserts for EVERY spawned interior figure:
|
||||||
1.4 m <= crown(head-bone world Y) <= 2.0 m (human-sized)
|
1.4 m <= STATURE (crown - foot) <= 2.0 m (human-sized — platform-independent)
|
||||||
crown < room dims.H (never clips the ceiling)
|
crown(head-bone world Y) < room dims.H (never clips the ceiling — its own line)
|
||||||
|feet(min-bone world Y)| < 0.05 m (planted on the floor)
|
|feet(min-bone world Y)| < 0.05 m (planted on the floor)
|
||||||
Exits non-zero if any figure fails, so tools/qa.sh --strict fails a giant before it can ship.
|
Exits non-zero if any figure fails, so tools/qa.sh --strict fails a giant before it can ship.
|
||||||
|
|
||||||
|
[Lane F R13 — debt #6] The human-size check measures STATURE (feet→crown span), not the raw world
|
||||||
|
crown, so it stays correct the day a figure stands on a platform (a keeper on a 0.3 m riser is not a
|
||||||
|
giant). This is the same measure smoke_gigs uses for the band on Lane C's stage deck, now ported into
|
||||||
|
the shared gate; the world-crown "never clips the ceiling" assert is kept as its own separate line
|
||||||
|
(that is the real R10 failure mode). For floor figures foot≈0 ⇒ stature≈crown ⇒ fully backward-compatible.
|
||||||
|
|
||||||
Run: tools/.venv/bin/python tools/qa/interior_scale_check.py [--seed N]
|
Run: tools/.venv/bin/python tools/qa/interior_scale_check.py [--seed N]
|
||||||
Setup: the same Playwright venv as tools/soak.py / tools/flags_check.py.
|
Setup: the same Playwright venv as tools/soak.py / tools/flags_check.py.
|
||||||
Needs Lane B's window.DBG hook (?dbg=1) + window.PROCITY.interiorMode (Lane C/F).
|
Needs Lane B's window.DBG hook (?dbg=1) + window.PROCITY.interiorMode (Lane C/F).
|
||||||
@ -69,7 +75,8 @@ MEASURE_JS = r"""
|
|||||||
a.fig.updateWorldMatrix(true, true);
|
a.fig.updateWorldMatrix(true, true);
|
||||||
(a.inner || a.fig).traverse(o => { if (o.isBone) { o.getWorldPosition(v);
|
(a.inner || a.fig).traverse(o => { if (o.isBone) { o.getWorldPosition(v);
|
||||||
if (/head/i.test(o.name)) crown = Math.max(crown, v.y); foot = Math.min(foot, v.y); } });
|
if (/head/i.test(o.name)) crown = Math.max(crown, v.y); foot = Math.min(foot, v.y); } });
|
||||||
return { kind: k.kind, browse: !!k.browse, crown: +crown.toFixed(3), foot: +foot.toFixed(3) };
|
return { kind: k.kind, browse: !!k.browse, crown: +crown.toFixed(3), foot: +foot.toFixed(3),
|
||||||
|
stature: +(crown - foot).toFixed(3) }; // [R13 debt #6] feet→crown span (platform-independent)
|
||||||
});
|
});
|
||||||
return { H: +H.toFixed(2), figs };
|
return { H: +H.toFixed(2), figs };
|
||||||
}
|
}
|
||||||
@ -117,14 +124,14 @@ def main():
|
|||||||
bad = []
|
bad = []
|
||||||
for f in figs:
|
for f in figs:
|
||||||
total_figs += 1
|
total_figs += 1
|
||||||
if not (LO <= f['crown'] <= HI): bad.append(f"crown {f['crown']}m out of [{LO},{HI}]")
|
if not (LO <= f['stature'] <= HI): bad.append(f"stature {f['stature']}m out of [{LO},{HI}]")
|
||||||
elif not (f['crown'] < H): bad.append(f"crown {f['crown']}m >= ceiling {H}m")
|
elif not (f['crown'] < H): bad.append(f"crown {f['crown']}m >= ceiling {H}m")
|
||||||
elif abs(f['foot']) >= FOOT_EPS: bad.append(f"feet at {f['foot']}m (not planted)")
|
elif abs(f['foot']) >= FOOT_EPS: bad.append(f"feet at {f['foot']}m (not planted)")
|
||||||
if bad:
|
if bad:
|
||||||
FAIL(f"{sid['name']} ({t}, H={H}m): " + "; ".join(bad))
|
FAIL(f"{sid['name']} ({t}, H={H}m): " + "; ".join(bad))
|
||||||
else:
|
else:
|
||||||
crowns = ", ".join(f"{f['crown']:.2f}" for f in figs)
|
stats = ", ".join(f"{f['stature']:.2f}" for f in figs)
|
||||||
OK(f"{sid['name']} ({t}, H={H}m): {len(figs)} figs crowns=[{crowns}] all human-sized")
|
OK(f"{sid['name']} ({t}, H={H}m): {len(figs)} figs statures=[{stats}] all human-sized")
|
||||||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||||||
pg.wait_for_timeout(150)
|
pg.wait_for_timeout(150)
|
||||||
b.close()
|
b.close()
|
||||||
|
|||||||
@ -60,6 +60,7 @@ import { createTram } from './js/world/tram.js'; // [Lane F R9
|
|||||||
import { createGigState } from './js/world/gig_state.js'; // [Lane F R12] the Friday-night state machine (?gigs=1)
|
import { createGigState } from './js/world/gig_state.js'; // [Lane F R12] the Friday-night state machine (?gigs=1)
|
||||||
import { createVenuePresentation } from './js/world/venue.js'; // [Lane F R12] Lane B venue frontage + posters (?gigs=1)
|
import { createVenuePresentation } from './js/world/venue.js'; // [Lane F R12] Lane B venue frontage + posters (?gigs=1)
|
||||||
import { loadPedFleet } from './js/citizens/rigs.js'; // [Lane F §3.4] Lane D rig fleet (GLB peds/keepers)
|
import { loadPedFleet } from './js/citizens/rigs.js'; // [Lane F §3.4] Lane D rig fleet (GLB peds/keepers)
|
||||||
|
import { VenueQueue } from './js/citizens/queue.js'; // [Lane F R13] Lane D outdoor gig queue (?gigs=1)
|
||||||
import { preloadManifest, preloadStockPack } from './js/interiors/interiors.js'; // [Lane F] Lane E manifest + Lane C/E stock pack
|
import { preloadManifest, preloadStockPack } from './js/interiors/interiors.js'; // [Lane F] Lane E manifest + Lane C/E stock pack
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
@ -243,6 +244,14 @@ const tram = (params.get('tram') && params.get('tram') !== '0') ? createTram({ s
|
|||||||
// Self-contained module; never constructed flags-off (byte-identical), like weather/tram.
|
// Self-contained module; never constructed flags-off (byte-identical), like weather/tram.
|
||||||
const venuePresentation = gigState ? createVenuePresentation(plan, skins, scene) : null;
|
const venuePresentation = gigState ? createVenuePresentation(plan, skins, scene) : null;
|
||||||
|
|
||||||
|
// [Lane F R13 — the outdoor queue] Lane D's VenueQueue, one per venue, wired from the street-side gig state
|
||||||
|
// (D shipped the class and left the street-side lifecycle to F — its documented seam). A seeded line forms at
|
||||||
|
// the frontage when the doors open, idle-shuffles + auto-drains each street frame, and is disposed by 'done'.
|
||||||
|
// B publishes the zone (venuePresentation.queueZoneFor); D's rig path is gate-protected (no giants) + a
|
||||||
|
// placeholder line under ?noassets. Built lazily per venue when its doors open; never touched flags-off.
|
||||||
|
const venueQueues = gigState ? new Map() : null; // venueShopId → VenueQueue
|
||||||
|
const queueCountOf = (id) => { const q = venueQueues && venueQueues.get(id); return q ? q.count() : 0; };
|
||||||
|
|
||||||
// spawn on the footpath near the west end of the strip, looking east down main street
|
// spawn on the footpath near the west end of the strip, looking east down main street
|
||||||
player.teleport(-96, 9, -Math.PI / 2);
|
player.teleport(-96, 9, -Math.PI / 2);
|
||||||
chunks.warmup(player.position);
|
chunks.warmup(player.position);
|
||||||
@ -271,19 +280,20 @@ function enterShop(shopId, name) {
|
|||||||
if (!shop) { hud.showToast(`🚪 ${name}`); return; }
|
if (!shop) { hud.showToast(`🚪 ${name}`); return; }
|
||||||
// [Lane F §3.5] closed shops are locked; at night the openLate video shop is the one place still open
|
// [Lane F §3.5] closed shops are locked; at night the openLate video shop is the one place still open
|
||||||
if (!isOpen(shop)) { hud.showToast(`🔒 ${name} — CLOSED · opens ${shop.hours[0]}:00`); return; }
|
if (!isOpen(shop)) { hud.showToast(`🔒 ${name} — CLOSED · opens ${shop.hours[0]}:00`); return; }
|
||||||
// [Lane F R12 — cover charge at the door] John's ruling: free AND paid nights. A's `cover` is seeded per
|
// [Lane F R13 — cover charge, PER VENUE] John's ruling: free AND paid nights. A's `cover` is seeded per
|
||||||
// gig (~half 0). Paid → debit the R8 session wallet on the way in with a toast; too skint → a polite
|
// gig (~half 0). Paid → debit the R8 session wallet on the way in with a toast; too skint → a polite
|
||||||
// knockback and NO entry (the wallet stays honest); free → walk straight in. Re-entry the same night is
|
// knockback and NO entry (the wallet stays honest); free → walk straight in. Re-entry the same night is
|
||||||
// free: the stamp lives in gigState, runtime only, so nothing persists and no plan/golden moves. Zero new
|
// free: the stamp lives in gigState, runtime only, so nothing persists and no plan/golden moves. THE
|
||||||
// UI — it all rides the round-8 buy seam. The venue is a normal shop in every other respect.
|
// DISTRICT: keyed by the venue being ENTERED (…Of(shopId)), so a night at two venues is two covers. Zero
|
||||||
if (gigState && shopId === gigState.venueShopId && gigState.open && gigState.cover > 0 && !gigState.paid) {
|
// new UI — it rides the round-8 buy seam; the venue is a normal shop in every other respect.
|
||||||
const cover = gigState.cover, BAND = String(gigState.bandName || 'live').toUpperCase();
|
if (gigState && gigState.openOf(shopId) && gigState.coverOf(shopId) > 0 && !gigState.paidOf(shopId)) {
|
||||||
|
const cover = gigState.coverOf(shopId), BAND = String(gigState.bandNameOf(shopId) || 'live').toUpperCase();
|
||||||
if (!wallet.canBuy(cover)) {
|
if (!wallet.canBuy(cover)) {
|
||||||
hud.showToast(`🚫 $${cover} cover — ${BAND} · you've got $${wallet.cash()}`);
|
hud.showToast(`🚫 $${cover} cover — ${BAND} · you've got $${wallet.cash()}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wallet.buy({ title: `cover — ${gigState.bandName}`, price: cover });
|
wallet.buy({ title: `cover — ${gigState.bandNameOf(shopId)}`, price: cover });
|
||||||
gigState.markPaid();
|
gigState.markPaidOf(shopId);
|
||||||
hud.showToast(`$${cover} cover — ${BAND}`);
|
hud.showToast(`$${cover} cover — ${BAND}`);
|
||||||
}
|
}
|
||||||
// [Lane F integration] attach lot dims + open Lane C's interior, switch to interior mode
|
// [Lane F integration] attach lot dims + open Lane C's interior, switch to interior mode
|
||||||
@ -359,13 +369,28 @@ function frame() {
|
|||||||
lighting.update(dt, player.position);
|
lighting.update(dt, player.position);
|
||||||
const clk = lighting.getClock();
|
const clk = lighting.getClock();
|
||||||
citizens.setTimeOfDay((clk.seg + clk.frac) / 6); // [Lane F] 0..1 day fraction → density (busy midday, empty at night)
|
citizens.setTimeOfDay((clk.seg + clk.frac) / 6); // [Lane F] 0..1 day fraction → density (busy midday, empty at night)
|
||||||
// [Lane F R12] Friday night, off the clock: tick the machine, ramp Lane B's marquee/sign glow, and point
|
// [Lane F R13 — THE DISTRICT] Friday night, off the clock: tick EVERY venue's latch, ramp Lane B's
|
||||||
// Lane D's patronage surge at the venue while the doors are open (setGig(id,false) ends it → normal day
|
// per-venue marquee/sign glow off the byVenue map, and point Lane D's patronage surge at each venue whose
|
||||||
// patronage). Lane B's audio engine reads window.PROCITY.gigs.state on its own rAF for the muffled spill.
|
// doors are open — several concurrent gigs each pull their own block (setGig(id,false) ends one → normal
|
||||||
|
// day patronage there). Lane B's audio engine reads window.PROCITY.gigs.byVenue on its own rAF for the
|
||||||
|
// per-genre muffled spill.
|
||||||
if (gigState) {
|
if (gigState) {
|
||||||
const gs = gigState.update();
|
gigState.update(); // tick all latches
|
||||||
if (venuePresentation) venuePresentation.update(gs);
|
if (venuePresentation) venuePresentation.update(gigState.byVenue); // per-venue frontage/spill
|
||||||
citizens.setGig(gigState.venueShopId, gs === 'doors' || gs === 'on');
|
for (const id of gigState.venueShopIds) {
|
||||||
|
const open = gigState.openOf(id);
|
||||||
|
citizens.setGig(id, open); // concurrent patronage surges
|
||||||
|
if (venueQueues) { // the outdoor line: forms while the doors are open, drains + disposes by 'done'
|
||||||
|
let q = venueQueues.get(id);
|
||||||
|
if (open && !q) {
|
||||||
|
q = new VenueQueue({ citySeed: plan.citySeed, fleet });
|
||||||
|
q.spawn(scene, { queueZone: venuePresentation && venuePresentation.queueZoneFor(id),
|
||||||
|
gigId: (gigState.gigOf(id) || {}).gigId | 0 });
|
||||||
|
venueQueues.set(id, q);
|
||||||
|
} else if (!open && q) { q.disposeAll(); venueQueues.delete(id); }
|
||||||
|
if (q) q.update(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// [Lane F R4] no setExposure call: D3 (a5e4b64) made the impostor toneMapped:true, so three /
|
// [Lane F R4] no setExposure call: D3 (a5e4b64) made the impostor toneMapped:true, so three /
|
||||||
// OutputPass tone-map it globally via renderer.toneMappingExposure — the passthrough is now a no-op.
|
// OutputPass tone-map it globally via renderer.toneMappingExposure — the passthrough is now a no-op.
|
||||||
@ -396,6 +421,7 @@ window.PROCITY = { plan, scene, camera, renderer, chunks, lighting, player, skin
|
|||||||
gigs: gigState, // [Lane F R12] the gig state machine — NULL flags-off (Lane B's audio tests this to
|
gigs: gigState, // [Lane F R12] the gig state machine — NULL flags-off (Lane B's audio tests this to
|
||||||
// decide there's no gig layer at all). `.state` is a live getter: quiet|doors|on|done
|
// decide there's no gig layer at all). `.state` is a live getter: quiet|doors|on|done
|
||||||
venuePresentation, // [Lane F R12] Lane B frontage + posters (null flags-off)
|
venuePresentation, // [Lane F R12] Lane B frontage + posters (null flags-off)
|
||||||
|
venueQueues, queueCountOf, // [Lane F R13] Lane D outdoor queue per venue + count accessor (F's smoke reads it)
|
||||||
THREE, get mode() { return MODE; } }; // [Lane F] bridge + drive hooks
|
THREE, get mode() { return MODE; } }; // [Lane F] bridge + drive hooks
|
||||||
// [Lane B R11 audio] street WebAudio engine — self-unlocking on the first gesture; silent with
|
// [Lane B R11 audio] street WebAudio engine — self-unlocking on the first gesture; silent with
|
||||||
// zero/blocked assets or ?mute=1; ?noassets=1 ⇒ no audio fetches. Exposes window.PROCITY.audio so
|
// zero/blocked assets or ?mute=1; ?noassets=1 ⇒ no audio fetches. Exposes window.PROCITY.audio so
|
||||||
|
|||||||
@ -1,45 +1,53 @@
|
|||||||
// PROCITY Lane F — gig_state.js [integration glue, F-owned, round 12 / v3.0-alpha]
|
// PROCITY Lane F — gig_state.js [integration glue, F-owned, round 13 / v3.0-beta THE DISTRICT]
|
||||||
// The Friday-night state machine: quiet → doors → on → done, driven off Lane B's day clock and Lane A's
|
// The Friday-night state machine, now PER-VENUE. A town has 2–4 venues (pub / band_room / rsl); each runs
|
||||||
// plan.gigs. F owns it because every other lane consumes it and none of them own the clock:
|
// its own latch quiet → doors → on → done off Lane B's day clock and Lane A's plan.gigs. F owns it because
|
||||||
// • Lane B audio.js reads window.PROCITY.gigs.state → the muffled through-the-wall spill
|
// every other lane consumes it and none of them own the clock:
|
||||||
// • Lane B venue.js update(state) → marquee/sign glow on the frontage
|
// • Lane B audio.js reads window.PROCITY.gigs.byVenue[id] → the muffled through-the-wall spill per venue
|
||||||
// • Lane D sim.js setGig(venueShopId, on) → the gig-night patronage surge
|
// • Lane B venue.js update(byVenue) → marquee/sign glow on each frontage
|
||||||
// • Lane C buildInterior(shop, THREE, { gig }) → room.audio.gigKey (the live bed inside)
|
// • Lane D sim.js setGig(venueShopId, on) → the gig-night patronage surge, per venue
|
||||||
// • Lane F the cover charge at the door + the smokes
|
// • Lane C buildInterior(shop, THREE, { gig }) → room.audio.gigKey (the live bed inside)
|
||||||
|
// • Lane F the cover charge at each door + the smokes
|
||||||
//
|
//
|
||||||
// Alpha model (CITY_SPEC §"CityPlan v3 — gig layer"): tonight is plan.gigs[0]; startSeg=5 (NIGHT 22:00),
|
// Model (CITY_SPEC §"CityPlan v3 — gig layer"): plan.gigs is the whole week × every venue. TONIGHT for a
|
||||||
// endSeg=5, doors at startSeg−1 (DUSK). One venue, one genre, seven nightly entries in the plan.
|
// 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.
|
||||||
//
|
//
|
||||||
// WHY 'done' NEEDS A LATCH. lighting.js's segment→hour map tops out at NIGHT (22:00) and the pub closes at
|
// ── THREE HARD-WON LAWS, unchanged from the alpha (they each cost a live-verify to learn): ──────────────
|
||||||
// 23, so the gig owns the WHOLE night segment — "after the gig" can only be the far side of the wrap, DAWN.
|
// WHY 'done' NEEDS A LATCH. lighting.js's segment→hour map tops out at NIGHT (22:00) and a venue closes at
|
||||||
// So: latch while the clock is inside the on-window, hold 'done' for exactly the segment after it (DAWN =
|
// closing time, so the gig owns the WHOLE night segment — "after the gig" can only be the far side of the
|
||||||
// closing time — the band packs up and the crowd drains per Lane A's closing-time ruling), and clear the
|
// wrap, DAWN. So: latch while the clock is inside the on-window, hold 'done' for exactly the segment after
|
||||||
// latch on any other segment → back to 'quiet'. The latch clears on any non-DAWN segment rather than only
|
// it (DAWN = closing time — the band packs up, the crowd drains per Lane A's closing ruling), and clear on
|
||||||
// at MORNING because the player drives this clock by hand ([ and ]): rewinding NIGHT → DUSK → ARVO must
|
// any other segment → back to 'quiet'. The latch clears on any non-DAWN segment (not only at MORNING)
|
||||||
// read doors → quiet, not leave 'done' stuck on a midday town (the R12 live verify caught it doing exactly
|
// because the player drives the clock by hand ([ and ]): rewinding NIGHT → DUSK → ARVO must read
|
||||||
// that). A night only *counts* as rolled when we leave 'done' FORWARDS — that's what separates a real
|
// doors → quiet, not leave 'done' stuck on a midday town (the R12 live verify caught it doing exactly that).
|
||||||
// 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
|
// WHY THE NIGHT ROLLS ON WHERE THE CLOCK LANDS, not on watching 'done' go by. First cut incremented the
|
||||||
// segments it happens to be sampled on: driving the clock NIGHT→DAWN→MORNING→NIGHT with nothing reading in
|
// night when LEAVING 'done' forwards — so a clock JUMP straight from NIGHT to MORNING (never touching DAWN)
|
||||||
// between (player inside the pub, audio muted, so neither the street loop nor the audio rAF is reading)
|
// never rolled the night, and a broke punter walked in on last night's paid stamp. Now: the latch clears
|
||||||
// skipped the roll entirely — the night never advanced and the cover was never due again. Caught live in
|
// wherever it clears, and landing in the DAYTIME (MORNING…ARVO) is what rolls the night; landing on the
|
||||||
// the R12 verify. lighting.js dispatches `procity:segment` on EVERY segment change (hand-driven or the
|
// doors means we rewound out of the gig — same night, stays paid. DBG/shots/soak all JUMP, so only this
|
||||||
// automatic day cycle), so that is the signal the latch rides. Reads stay safe on their own: `state` is a
|
// rule survives them.
|
||||||
// getter that re-observes the current segment, and observing the same segment twice is idempotent.
|
//
|
||||||
|
// 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;
|
const SEGS = 6;
|
||||||
|
|
||||||
export function createGigState({ plan, lighting }) {
|
// One venue's latch — the alpha machine, its hard-won internals verbatim. `gig` is this venue's tonight
|
||||||
const gigs = (plan && plan.gigs) || [];
|
// (night-0) entry, or null when the venue is dark tonight (→ always 'quiet').
|
||||||
const gig = gigs[0] || null; // alpha: tonight is gigs[0] (CITY_SPEC v3)
|
function createVenueLatch(gig, lighting) {
|
||||||
const startSeg = gig && gig.startSeg != null ? gig.startSeg : 5;
|
const startSeg = gig && gig.startSeg != null ? gig.startSeg : 5;
|
||||||
const endSeg = gig && gig.endSeg != null ? gig.endSeg : 5;
|
const endSeg = gig && gig.endSeg != null ? gig.endSeg : 5;
|
||||||
const doorsSeg = (startSeg - 1 + SEGS) % SEGS; // DUSK — the doors open
|
const doorsSeg = (startSeg - 1 + SEGS) % SEGS; // DUSK — the doors open
|
||||||
const doneSeg = (endSeg + 1) % SEGS; // DAWN — closing time; the only segment 'done' lives in
|
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 —
|
// 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
|
// Landing here after a gig means THE DAY HAS TURNED, which rolls the night (next cover due).
|
||||||
// is what rolls the night (and makes the next cover due). See the roll note below.
|
|
||||||
const DAYTIME = (() => {
|
const DAYTIME = (() => {
|
||||||
const set = new Set();
|
const set = new Set();
|
||||||
for (let s = (doneSeg + 1) % SEGS; s !== doorsSeg; s = (s + 1) % SEGS) set.add(s);
|
for (let s = (doneSeg + 1) % SEGS; s !== doorsSeg; s = (s + 1) % SEGS) set.add(s);
|
||||||
@ -52,16 +60,14 @@ export function createGigState({ plan, lighting }) {
|
|||||||
let paidNight = -1; // the night whose cover is paid; re-entry the same night is free
|
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
|
// The whole machine, for ONE segment. Idempotent per segment, so it is safe to call from both the
|
||||||
// event and any read.
|
// procity:segment event and any read.
|
||||||
function observe(seg) {
|
function observe(seg) {
|
||||||
const on = seg >= startSeg && seg <= endSeg;
|
const on = seg >= startSeg && seg <= endSeg;
|
||||||
if (on) played = true;
|
if (on) played = true;
|
||||||
else if (played && seg !== doneSeg) {
|
else if (played && seg !== doneSeg) {
|
||||||
// The latch is clearing: the gig is behind us. Whether that means "a new night" (cover due again)
|
// The latch is clearing: the gig is behind us. Whether that's "a new night" (cover due again) or
|
||||||
// or "we rewound out of it" (same night, stays paid) is decided by WHERE we land, not by having
|
// "we rewound out of it" (same night, stays paid) is decided by WHERE we land, not by watching
|
||||||
// watched 'done' go by: keying the roll on leaving 'done' meant a clock jump straight from NIGHT to
|
// 'done' go by (see the roll law in the header).
|
||||||
// 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;
|
played = false;
|
||||||
if (DAYTIME.has(seg)) night++; // landed in the daytime ⇒ the day turned ⇒ next cover is due
|
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)
|
} // (landed on the doors ⇒ rewound out of the gig ⇒ same night)
|
||||||
@ -69,30 +75,81 @@ export function createGigState({ plan, lighting }) {
|
|||||||
return state;
|
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() {
|
function tick() {
|
||||||
if (!gig || !lighting || !lighting.getClock) return (state = 'quiet');
|
if (!gig || !lighting || !lighting.getClock) return (state = 'quiet');
|
||||||
return observe(lighting.getClock().seg);
|
return observe(lighting.getClock().seg);
|
||||||
}
|
}
|
||||||
tick(); // settle on the boot segment
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
gig, observe, tick,
|
||||||
get state() { return tick(); },
|
get state() { return tick(); },
|
||||||
update: tick, // shell calls this once per street frame (same thing)
|
|
||||||
get on() { return tick() === 'on'; },
|
get on() { return tick() === 'on'; },
|
||||||
get open() { const s = tick(); return s === 'doors' || s === 'on'; }, // the door is taking punters
|
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 cover() { return gig ? gig.cover | 0 : 0; },
|
||||||
get night() { return night; },
|
get bandName() { return gig ? gig.bandName : null; },
|
||||||
// cover stamp — runtime only (like the wallet itself): paid once, free to pop out and back in until the
|
get night() { tick(); return night; },
|
||||||
// night rolls. NOT persisted, NOT written into the plan (goldens never move).
|
// 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; },
|
get paid() { tick(); return paidNight === night; },
|
||||||
markPaid() { tick(); 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. Primary = gigs[0].venueShopId — the alpha's
|
||||||
|
// "tonight", kept as the compat alias so B's mid-round single-venue callers still light the right pub.
|
||||||
|
const venueIds = [...new Set(gigs.map((g) => g.venueShopId))];
|
||||||
|
const primaryId = gigs.length ? gigs[0].venueShopId : null;
|
||||||
|
|
||||||
|
// one latch per venue, keyed by its night-0 gig (null → dark tonight → 'quiet')
|
||||||
|
const latch = new Map();
|
||||||
|
for (const id of venueIds) {
|
||||||
|
const tonight = gigs.find((g) => g.venueShopId === id && g.night === 0) || null;
|
||||||
|
latch.set(id, createVenueLatch(tonight, lighting));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
const P = () => L(primaryId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ── 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 state) and returns the
|
||||||
|
// primary state for the alpha-compat caller.
|
||||||
|
update() { tickAll(); return P() ? P().state : 'quiet'; },
|
||||||
|
|
||||||
|
// ── alpha-shape alias → the PRIMARY venue (kept for one round; B reads it mid-round) ─────────────
|
||||||
|
get state() { const p = P(); return p ? p.state : 'quiet'; },
|
||||||
|
get on() { const p = P(); return p ? p.on : false; },
|
||||||
|
get open() { const p = P(); return p ? p.open : false; },
|
||||||
|
get gig() { const p = P(); return p ? p.gig : null; },
|
||||||
|
get venueShopId() { return primaryId; },
|
||||||
|
get bandName() { const p = P(); return p ? p.bandName : null; },
|
||||||
|
get cover() { const p = P(); return p ? p.cover : 0; },
|
||||||
|
get night() { const p = P(); return p ? p.night : 0; },
|
||||||
|
get paid() { const p = P(); return p ? p.paid : false; },
|
||||||
|
markPaid() { const p = P(); if (p) p.markPaid(); },
|
||||||
|
|
||||||
dispose() { if (typeof window !== 'undefined') window.removeEventListener('procity:segment', onSegment); },
|
dispose() { if (typeof window !== 'undefined') window.removeEventListener('procity:segment', onSegment); },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,16 +27,14 @@ const SPEED = 3.2; // interior walk speed (matches the Lane C tester)
|
|||||||
const ARM_DIST = 2.0; // must step this far from the door before the exit arms (spawn is ~1.5m away)
|
const ARM_DIST = 2.0; // must step this far from the door before the exit arms (spawn is ~1.5m away)
|
||||||
const EXIT_DIST = 1.0; // …then coming back within this distance leaves to the street
|
const EXIT_DIST = 1.0; // …then coming back within this distance leaves to the street
|
||||||
|
|
||||||
// [Lane F R12 — CROSS-LANE SEAM FIX] Lane C emits `room.audio.gigKey = 'gig-' + genreKey` and asked Lane
|
// [Lane F R13 — debt #1 CLOSED] The gigKey contract is now canonical: a genre's live-bed manifest key IS
|
||||||
// E (LANE_C_PUB.md) to name the live bed `gig-pubrock` in manifest.audio; Lane E shipped it as
|
// `gig-<genreKey>` (Lane A exports gigKeyFor(); Lane C emits room.audio.gigKey; Lane E named the manifest
|
||||||
// `pubrock-live` (tagged `gig:true`, types:['pub']) — which is also the key Lane B's street spill already
|
// beds gig-pubrock/gig-grunge/gig-covers to match). So the room's gigKey feeds the audio engine DIRECTLY,
|
||||||
// hardcodes. Neither is wrong, they just never met: unresolved, `AUDIO.music['gig-pubrock']` is undefined
|
// with NO mapping. The R12 `GIG_BED` bridge that rewrote gig-pubrock→pubrock-live is DELETED — and not a
|
||||||
// and the pub plays to a SILENT room (audio law fails soft, so nothing errors — it just isn't a gig).
|
// moment too soon: E renamed pubrock-live→gig-pubrock this round, so the old bridge had become a LIVE BUG
|
||||||
// F owns the glue, so the key resolves here: exact map first, then a generic `gig-<genre>` → `<genre>-live`
|
// (it pointed at a manifest key that no longer exists → the band would mime in a silent room). Contract line
|
||||||
// fallback so a second genre works without another edit. Reported to C/E for a real contract line in
|
// lives in CITY_SPEC §v3. See LANE_F_NOTES §12.
|
||||||
// CITY_SPEC (same class as the R1 facade-skin filename trap). See LANE_F_NOTES §12.
|
const WALLA_KEY = 'crowd-walla'; // [R13] manifest.ambience — crowd murmur layered under the gig bed while 'on'
|
||||||
const GIG_BED = { 'gig-pubrock': 'pubrock-live' };
|
|
||||||
const gigBedKey = (k) => (k ? GIG_BED[k] || k.replace(/^gig-(.+)$/, '$1-live') : null);
|
|
||||||
|
|
||||||
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false,
|
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false,
|
||||||
dig: digEnabled = false, stockReal = false, wallet = null,
|
dig: digEnabled = false, stockReal = false, wallet = null,
|
||||||
@ -51,6 +49,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
|||||||
// most rooms never see one), disposed with the room like the keepers.
|
// most rooms never see one), disposed with the room like the keepers.
|
||||||
let crew = null;
|
let crew = null;
|
||||||
let crewInfo = null; // { band, crowd } — what actually spawned (F's smokes read this)
|
let crewInfo = null; // { band, crowd } — what actually spawned (F's smokes read this)
|
||||||
|
let applauded = false; // [R13] one-shot latch: the on→done applause sting has fired for this visit
|
||||||
|
|
||||||
let current = null; // active Lane C interior handle
|
let current = null; // active Lane C interior handle
|
||||||
let currentShop = null; // the shop record for the active interior (dig per-bin seeding)
|
let currentShop = null; // the shop record for the active interior (dig per-bin seeding)
|
||||||
@ -191,23 +190,25 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
|||||||
// [F2 §stock=real] real GODVERSE sleeves in this shop's crates. makeStockAdapter(null) → null →
|
// [F2 §stock=real] real GODVERSE sleeves in this shop's crates. makeStockAdapter(null) → null →
|
||||||
// Lane C fails soft to the parody canvas, so a missing/unpreloaded pack just shows placeholders.
|
// Lane C fails soft to the parody canvas, so a missing/unpreloaded pack just shows placeholders.
|
||||||
currentAdapter = stockReal ? makeStockAdapter(getStockPack(shop.type)) : null;
|
currentAdapter = stockReal ? makeStockAdapter(getStockPack(shop.type)) : null;
|
||||||
// [Lane F R12 gig layer] Is this the venue, mid-gig? Lane C only stamps room.audio.gigKey when we pass
|
// [Lane F R13 THE DISTRICT] Is THIS shop the venue with a gig on right now? Per-venue query — a town has
|
||||||
// opts.gig, and it treats `{on:false}` as "a gig" if genreKey is set — so pass NULL unless it's really
|
// 2–4 venues, each with its own latch keyed by shop id. Lane C only stamps room.audio.gigKey when we pass
|
||||||
// on. A quiet-night pub then builds as a normal interior (stage + watch points still there, just nobody
|
// opts.gig, so pass NULL unless it's really on: a quiet-night venue then builds as a normal interior
|
||||||
// on them), which is exactly the "quiet night at the pub" C specced.
|
// (stage + watch points still there, just nobody on them), the "quiet night" Lane C specced.
|
||||||
const gigOn = !!(gigState && gigState.venueShopId != null && shop.id === gigState.venueShopId && gigState.on);
|
const gigOn = !!(gigState && gigState.onOf(shop.id));
|
||||||
const tonight = gigOn ? gigState.gig : null;
|
const tonight = gigOn ? gigState.gigOf(shop.id) : null;
|
||||||
current = buildInterior(shop, THREE, { useGLB, stockAdapter: currentAdapter,
|
current = buildInterior(shop, THREE, { useGLB, stockAdapter: currentAdapter,
|
||||||
gig: gigOn ? { on: true, genreKey: tonight.genreKey || 'pubrock', gigId: tonight.gigId } : null });
|
gig: gigOn ? { on: true, genreKey: tonight.genreKey || 'pubrock', gigId: tonight.gigId } : null });
|
||||||
currentShop = shop;
|
currentShop = shop;
|
||||||
scene.add(current.group);
|
scene.add(current.group);
|
||||||
// [Lane F R11 audio] fade in this room's seeded bed (Lane C room.audio {musicKey,toneKey} → Lane B engine).
|
// [Lane F R11 audio] fade in this room's seeded bed (Lane C room.audio {musicKey,toneKey} → Lane B engine).
|
||||||
// Guarded + no-op when muted / ?noassets / pre-gesture / engine still async-loading — silent-and-happy.
|
// Guarded + no-op when muted / ?noassets / pre-gesture / engine still async-loading — silent-and-happy.
|
||||||
// [R12] Prefer the gig bed over the room's seeded radio while the band is on (LANE_C_PUB.md), through
|
// [R13] While the band is on, the LIVE BED plays over the room's radio: room.audio.gigKey is ALREADY the
|
||||||
// F's key resolver above — playInterior only knows musicKey/toneKey, so the gig bed rides in as musicKey.
|
// manifest key (gig-<genreKey> — debt #1 closed, no bridge), and the crowd WALLA rides the interior tone
|
||||||
|
// layer under it (you hear the room, not the fridge hum). Quiet night → the room's own seeded bed.
|
||||||
const ra = current.audio || {};
|
const ra = current.audio || {};
|
||||||
const bed = gigBedKey(ra.gigKey);
|
const gigAudio = gigOn && ra.gigKey ? { musicKey: ra.gigKey, toneKey: WALLA_KEY } : ra;
|
||||||
window.PROCITY.audio && window.PROCITY.audio.playInterior(bed ? { ...ra, musicKey: bed } : ra);
|
window.PROCITY.audio && window.PROCITY.audio.playInterior(gigAudio);
|
||||||
|
applauded = false; // [R13] re-arm the on→done applause sting for this visit
|
||||||
// Lane D keeper at the counter — Lane C tags the stand pose on the counter interactable
|
// Lane D keeper at the counter — Lane C tags the stand pose on the counter interactable
|
||||||
const counter = current.places.find((p) => p.userData && p.userData.keeperStand);
|
const counter = current.places.find((p) => p.userData && p.userData.keeperStand);
|
||||||
if (counter) {
|
if (counter) {
|
||||||
@ -281,6 +282,13 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
|||||||
|
|
||||||
keepers.update(dt); // Lane D: keeper idle + turn-to-greet (playerPos defaults to the camera)
|
keepers.update(dt); // Lane D: keeper idle + turn-to-greet (playerPos defaults to the camera)
|
||||||
if (crew) crew.update(dt); // [R12] Lane D: band strum-sway + crowd stand/dance (seeded phase per member)
|
if (crew) crew.update(dt); // [R12] Lane D: band strum-sway + crowd stand/dance (seeded phase per member)
|
||||||
|
// [Lane F R13] applause sting on on→done: crewInfo is non-null only when a gig crew spawned (gigOn at
|
||||||
|
// enter); when the clock then rolls the gig off (NIGHT→DAWN — the band's last song), the crowd claps
|
||||||
|
// ONCE. Fail-soft via the house audio law (no-op muted / ?noassets / pre-gesture). applauded guards re-fire.
|
||||||
|
if (crewInfo && !applauded && gigState && currentShop && !gigState.onOf(currentShop.id)) {
|
||||||
|
applauded = true;
|
||||||
|
window.PROCITY.audio && window.PROCITY.audio.playSfx && window.PROCITY.audio.playSfx('applause', { gain: 0.9 });
|
||||||
|
}
|
||||||
renderer.autoClear = true; // the composer may have left autoClear=false; force a clean interior frame
|
renderer.autoClear = true; // the composer may have left autoClear=false; force a clean interior frame
|
||||||
renderer.info.reset(); // autoReset is off (shell owns it for the composer); reset per interior frame so
|
renderer.info.reset(); // autoReset is off (shell owns it for the composer); reset per interior frame so
|
||||||
// renderer.info.render.calls reports THIS frame's draws (HUD + the ≤350 room gate)
|
// renderer.info.render.calls reports THIS frame's draws (HUD + the ≤350 room gate)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user