Verified Lane D's R10 buildFigure span-normalize fix: qa.sh --strict GREEN 6/6 incl. the new no-giants scale gate (24 interior figs / 6 shops all crown 1.4-2.0m, under-ceiling). The 3.83m I first measured live was a STALE ES-module cache in a day-old browser tab, not a regression (served rigs.js + fresh-chromium gate both correct). Re-shot the v2 tour on the fixed tree: browsers.png now shows human-sized figures (browser at the rack + keeper at the counter). night_crowd.png re-framed to a readable clean-night "lit VIDEO REGAL amid CLOSED town" (pulled out of the rain group; reuses night_neon's tighter dist-12 pose). Honest finding: night_crowd has no visible street crowd and can't without new work. Lane D's night patronage parks ducked-in patrons hidden INSIDE the shop (they are the interior browsers), and this seed's video shop is a low-traffic corner (tested: 0 patronage / 0 visible at night). Flagged for D; a visible outdoor night queue is a v3 mechanic. ~20fps in night captures is a headless software-GL artifact (scene is light: 128 draws / 65k tris). Full-default soak PASS (heap 54MB stable, no perf drift, 0 errors). Tour README updated with stable v2 filenames. v2.0 annotated tag staged separately (push held for John — public release + the night_crowd caveat on his flagged money shot). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
105 lines
5.8 KiB
Python
105 lines
5.8 KiB
Python
#!/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'<figure><img src="{n}.png" loading="lazy"><figcaption>{n}</figcaption></figure>' for n in shot_names)
|
|
(OUT.parent / 'v2_contact.html').write_text(
|
|
'<!doctype html><meta charset="utf-8"><title>PROCITY v2.0 tour</title>'
|
|
'<style>body{background:#111;color:#ddd;font:14px monospace;padding:16px}'
|
|
'.g{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}figure{margin:0}img{width:100%}'
|
|
'figcaption{color:#8f8;padding:3px 0}</style><h2>PROCITY v2.0 — ' + str(len(shot_names)) + ' shots</h2>'
|
|
'<div class="g">' + cells + '</div>')
|
|
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()
|