The compat alias delegating to the primary venue is gone — every read is now keyed by venueShopId. B swept the cross-lane readers in R14; this migrates F's own tools + the spec, one atomic commit. - gig_state.js: delete the alpha-shape alias block + the now-unused primaryId/P; update() just ticks all latches - flags_check.py + gig_shot.py: .venueShopId->venueShopIds[0], .state->stateOf(id), .cover->coverOf(id), .bandName->bandNameOf(id) - CITY_SPEC \S v3: delete the two alias sentences, add the omitted nightOf(id) to the accessor list selfcheck 14264/14264 green (base 0x3fa36874, gig 0x4f4a549d); flags_off + smoke_gigs green, 0 warn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
6.9 KiB
Python
141 lines
6.9 KiB
Python
#!/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;
|
|
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — read the primary venue by id
|
|
D.setSegment(5); // NIGHT — the gig is on
|
|
D.enterShop(id);
|
|
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.stateOf(id) };
|
|
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 === id) || {}).name,
|
|
band: P.gigs.bandNameOf(id), state: P.gigs.stateOf(id), cover: P.gigs.coverOf(id), 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.venueShopIds[0];
|
|
return { ok: P.mode === 'street', venue: (P.plan.shops.find(s => s.id === id) || {}).name,
|
|
state: P.gigs.stateOf(id),
|
|
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()
|