#!/usr/bin/env python3 """PROCITY Lane F — v3.0-beta THE DISTRICT acceptance shots (round 13). The two money shots the round asks for, both reproducible (never a hand-cropped screenshot nobody can re-take when the gig layer changes): 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] [--outdir DIR] 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 ROOT = pathlib.Path(__file__).resolve().parent.parent.parent PORT = 8130 HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 OUTDIR = ROOT / 'docs' / 'shots' / 'laneF' if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1]) if '--outdir' in sys.argv: OUTDIR = pathlib.Path(sys.argv[sys.argv.index('--outdir') + 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}') # ── Shot 1: INTERIOR. 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 interior shot deterministic in headless. INTERIOR_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, mem = crew ? crew.members : []; const band = mem.filter(m => m.part === 'band'); return { ok: true, venue: (P.plan.shops.find(s => s.id === P.gigs.venueShopId) || {}).name, 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 }; } """ 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() OUTDIR.mkdir(parents=True, exist_ok=True) interior_out = OUTDIR / 'r13_v3beta_gig_interior.png' street_out = OUTDIR / 'r13_v3beta_venue_night.png' 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') # Shot 1 — interior info = pg.evaluate(INTERIOR_JS) if not info.get('ok'): b.close(); sys.exit(f'\033[31m✗ could not stage the interior shot: {info}\033[0m') pg.screenshot(path=str(interior_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() finally: if srv: srv.terminate() if errs: print(f" ! {len(errs)} page error(s): {errs[0][:120]}") sys.exit(1) if __name__ == '__main__': main()