#!/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()