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>
101 lines
4.4 KiB
Python
101 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""PROCITY Lane F — v3.0-alpha acceptance shot (round 12).
|
|
|
|
The money shot the round asks for: gig ON, band on Lane C's deck, mixed standing/dancing crowd at the
|
|
watch points, everything human-sized next to the door/bar/ceiling. Reproducible so the shot can be
|
|
re-taken when the gig layer changes (and so it is never a hand-cropped screenshot nobody can redo).
|
|
|
|
Run: tools/.venv/bin/python tools/qa/gig_shot.py [--seed N] [--out PATH]
|
|
Needs the Playwright venv + Lane B's ?dbg=1 hook. Writes a full-frame 1280x720 PNG.
|
|
"""
|
|
import sys, time, socket, subprocess, pathlib
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
|
PORT = 8130
|
|
HOST = f'http://127.0.0.1:{PORT}'
|
|
SEED = 20261990
|
|
OUT = ROOT / 'docs' / 'shots' / 'laneF' / 'r12_v3alpha_gig.png'
|
|
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])
|
|
|
|
|
|
def port_up(port):
|
|
with socket.socket() as s:
|
|
s.settimeout(0.4)
|
|
return s.connect_ex(('127.0.0.1', port)) == 0
|
|
|
|
|
|
def ensure_server():
|
|
if port_up(PORT):
|
|
return None
|
|
proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)], cwd=str(ROOT / 'web'),
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
for _ in range(50):
|
|
if port_up(PORT): return proc
|
|
time.sleep(0.1)
|
|
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
|
|
# render (rather than trusting rAF) keeps the shot deterministic in headless.
|
|
POSE_JS = r"""
|
|
async () => {
|
|
const P = window.PROCITY, D = window.DBG, THREE = P.THREE;
|
|
D.setSegment(5); // NIGHT — the gig is on
|
|
D.enterShop(P.gigs.venueShopId);
|
|
await new Promise(r => setTimeout(r, 900)); // room build + crew spawn + rig clips settle
|
|
const room = P.interiorMode.current;
|
|
if (!room) return { ok: false, why: 'not in the venue', state: P.gigs.state };
|
|
const st = room.stage;
|
|
P.camera.position.set(st.x + 0.2, 1.6, room.dims.D / 2 - 1.2);
|
|
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z));
|
|
P.renderer.info.reset();
|
|
P.renderer.render(P.interiorMode.scene, P.camera);
|
|
const crew = P.interiorMode.crew;
|
|
return { ok: true, venue: (P.plan.shops.find(s => s.venue) || {}).name, band: P.gigs.bandName,
|
|
state: P.gigs.state, cover: P.gigs.cover, crew: P.interiorMode.crewInfo,
|
|
dancers: crew ? crew.members.filter(m => m.part === 'crowd' && m.dance).length : 0,
|
|
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles };
|
|
}
|
|
"""
|
|
|
|
|
|
def main():
|
|
try:
|
|
from playwright.sync_api import sync_playwright
|
|
except ImportError:
|
|
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
|
|
srv = ensure_server()
|
|
try:
|
|
with sync_playwright() as p:
|
|
b = p.chromium.launch()
|
|
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
|
errs = []
|
|
pg.on('pageerror', lambda e: errs.append(str(e)))
|
|
pg.goto(f'{HOST}/index.html?seed={SEED}&gigs=1&dbg=1')
|
|
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=25000)
|
|
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
|
try:
|
|
pg.wait_for_function('() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)',
|
|
timeout=15000)
|
|
except Exception:
|
|
print(' ! fleet never readied — shooting the placeholder crew')
|
|
info = pg.evaluate(POSE_JS)
|
|
if not info.get('ok'):
|
|
b.close(); sys.exit(f'\033[31m✗ could not stage the shot: {info}\033[0m')
|
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
pg.screenshot(path=str(OUT))
|
|
b.close()
|
|
finally:
|
|
if srv: srv.terminate()
|
|
|
|
print(f"\033[32m● SHOT\033[0m {OUT.relative_to(ROOT)}")
|
|
print(f" {info['venue']} · {info['band']} · state={info['state']} · ${info['cover']} cover")
|
|
print(f" band {info['crew']['band']} · crowd {info['crew']['crowd']} ({info['dancers']} dancing) · "
|
|
f"{info['draws']} draws · {info['tris']:,} tris")
|
|
if errs: print(f" ! {len(errs)} page error(s): {errs[0][:120]}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|