#!/usr/bin/env python3 """PROCITY Lane F — v2.0 tour: 12 beauty shots across flag-groups → docs/shots/v2_tour/ + contact sheet. Boots each flag-combo once, shoots its bookmarks (Lane B dbg.js poses) + interior shots. Un-letterboxed 1280x720 (dbg.js syncSize). Run: cd web && python3 -m http.server 8130 then tools/.venv/bin/python tools/v2_tour.py """ import sys, pathlib from playwright.sync_api import sync_playwright ROOT = pathlib.Path(__file__).resolve().parent.parent OUT = ROOT / 'docs' / 'shots' / 'v2_tour' OUT.mkdir(parents=True, exist_ok=True) HOST = 'http://127.0.0.1:8130' # flag-combo -> [street bookmarks] (each dbg.js bookmark sets its own pose + day segment) GROUPS = [ ('winmap=1&weather=rain&tram=1&stock=real&pop=200', ['rain_verandah', 'tram_stop', 'rain_street', 'patronage_door', 'shopfront_detail', 'street_noon', 'crossroads_busy']), # [Lane F R10] night_crowd on a CLEAN night boot (no rain → the neon reads brighter + fps recovers; # rain variants already exist as rain_verandah/rain_street). High pop for any passers-by. ('pop=300', ['night_crowd']), ('plansrc=osm&town=katoomba&pop=180', ['katoomba_main']), ] # interior shots: flags, then enter + optional dig INTERIORS = [ ('dig_real', 'dig=1&stock=real', 'record', True), # enter record shop + open the dig (real covers) ('browsers', 'stock=real&pop=200', 'occupied', False), # enter an occupied shop (browsers present) ] def boot(pg, q): pg.goto(f'{HOST}/?dbg=1&{q}') pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000) 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=12000) except Exception: pass def main(): shot_names, errs = [], [] with sync_playwright() as p: b = p.chromium.launch() pg = b.new_page(viewport={'width': 1280, 'height': 720}) pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None) for flags, marks in GROUPS: boot(pg, flags) for name in marks: try: # [Lane F R10] night_crowd's dist-16 bookmark reads dark + empty (the openLate video # shop sits at a dead-end corner and nothing routes patrons to it at night — a real # engine gap, noted for D). Reuse night_neon's tighter dist-12 pose so the stable # night_crowd.png filename carries the readable "one lit shop amid a CLOSED town" shot. pose = 'night_neon' if name == 'night_crowd' else name pg.evaluate("(n) => window.DBG.shot(n)", pose) pg.wait_for_timeout(1100) pg.evaluate("(n) => window.DBG.shot(n)", pose) # settle bloom/segment pg.wait_for_timeout(300) pg.screenshot(path=str(OUT / f'{name}.png')) shot_names.append(name); print('shot', name) except Exception as e: print('MISS', name, str(e)[:80]) for name, flags, kind, do_dig in INTERIORS: boot(pg, flags) ok = pg.evaluate("""(kind) => { const P=window.PROCITY, D=window.DBG; D.setSegment(2); let s=null; if(kind==='occupied'){ const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;} let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}} D.teleport(best.x,best.z,0); P.citizens.setPopulation(200); for(let i=0;i<400;i++) P.citizens.update(0.05); for(const sh of plan.shops){ const o=P.citizens.occupancyOf(sh.id); if(o&&o.count>0){s=sh;break;} } if(!s) s=plan.shops.find(x=>x.type==='opshop'&&P.isOpen(x)); } else { s=(P.plan.shops||[]).find(x=>x.type===kind && P.isOpen(x)); } if(!s) return false; D.enterShop(s.id); return true; }""", kind) if not ok: print('MISS', name, '(no shop)'); continue if do_dig: # aim at a bin + open the dig for the real-cover shot pg.evaluate("""() => { const P=window.PROCITY; const bins=[]; P.interiorMode.current.group.traverse(o=>{if(o.userData&&o.userData.kind==='bin')bins.push(o)}); if(bins.length){ const bp=new P.THREE.Vector3(); bins[0].getWorldPosition(bp); P.camera.position.set(bp.x,1.6,bp.z+1.4); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld(); window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'})); } }""") pg.wait_for_timeout(900) pg.screenshot(path=str(OUT / f'{name}.png')) shot_names.append(name); print('shot', name) b.close() # contact sheet cells = ''.join(f'
{n}
' for n in shot_names) (OUT.parent / 'v2_contact.html').write_text( 'PROCITY v2.0 tour' '

PROCITY v2.0 — ' + str(len(shot_names)) + ' shots

' '
' + cells + '
') print(f'\n{len(shot_names)} shots → docs/shots/v2_tour/ · contact: docs/shots/v2_contact.html') if errs: print(f'⚠ {len(errs)} console error(s); first: {errs[0][:120]}') if __name__ == '__main__': main()