#!/usr/bin/env python3 """PROCITY Lane D — R42 §42.4 acceptance shots: THE NEW ROSTER, ON THE STREET AND IN THE PUB. tools/.venv/bin/python tools/qa/r42_shots.py [--seed N] [--outdir DIR] 0 green · 1 red Two frames, and both of them name the bodies in shot — because "the luchador is gone" is a claim about the CAST, and a JPEG on its own cannot carry it. Every figure is resolved back to the ped GLB it is wearing (`fleet.all[pedIndex].pedName`), so the sidecar is a cast list, not a caption. 1. r42_street_crowd.jpg the retail heart at midday. Camera pose chosen the R41 way — the bench station with the most patronage doors around it, stood off on the carriageway and occlusion-raycast before it is accepted — then held until the crowd offers the frame. 2. r42_pub_crowd.jpg THE BEFORE/AFTER. Shot on the same recipe Lane F used for docs/shots/laneF_r41/pub_furnished.jpg (?gigs=1, segment 5, the best pub, Lane C's own room.spawn line, back 5.2 m, eye 1.62 m) so the pair is comparable frame for frame. That R41 frame is the one with the purple EL CHUPACABRA luchador in the gig crowd; this is the same room after the recast. Human-sized line (R10) on both, measured off the posed skeleton, written into the sidecar .txt. Fresh headless contexts, own no-store server. """ import sys, os, json, time, socket, subprocess, pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent.parent PORT = int(os.environ.get('PROCITY_R42_SHOT_PORT', '8993')) HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 OUTDIR = ROOT / 'docs' / 'shots' / 'laneD' 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]) LO, HI = 1.4, 2.0 RETIRED = ['comical_luchador_01', 'comical_boy_01', 'man_elder_01', 'man_soldier_ww2_01', 'dj_phrtt_01'] rc = 0 def FAIL(m): global rc; rc = 1; print(f" \033[31m✗ {m}\033[0m") def OK(m): print(f" \033[32m✓\033[0m {m}") NOSTORE = r''' import sys, http.server, functools class H(http.server.SimpleHTTPRequestHandler): def end_headers(self): self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate') super().end_headers() def log_message(self, *a): pass http.server.HTTPServer(('127.0.0.1', int(sys.argv[1])), functools.partial(H, directory=sys.argv[2])).serve_forever() ''' # ── street: the R41 §41.3 place/frame logic, unchanged (it is already measurement-driven) ────────── PLACE = r""" async () => { const P = window.PROCITY, C = P.citizens; const THREE = await import('three'); const mod = await import('./js/citizens/sim.js'); const V3 = P.scene.position.constructor; const ray = new THREE.Raycaster(); ray.far = 40; const doors = []; if (C.shopsByChunk) for (const list of C.shopsByChunk.values()) for (const d of list) doors.push(d); const clear = (px, pz, tx, tz) => { const d = Math.hypot(tx - px, tz - pz); ray.set(new V3(px, 1.6, pz), new V3((tx - px) / d, 0, (tz - pz) / d)); let h = ray.intersectObject(P.scene, true).filter(x => !x.object.isSkinnedMesh && !x.object.isSprite); if (h.length && h[0].distance < d - 0.8) return false; for (const [ax, az] of [[1,0],[-1,0],[0,1],[0,-1]]) { ray.set(new V3(px, 1.6, pz), new V3(ax, 0, az)); h = ray.intersectObject(P.scene, true).filter(x => !x.object.isSkinnedMesh); if (h.length && h[0].distance < 1.3) return false; } return true; }; const out = []; for (let i = 0; i < C.edges.length; i++) { const e = C.edges[i]; if ((e.width || 4) < 10) continue; const ux = e.ux, uz = e.uz; for (const st of mod.benchStationsFor(e)) { let nd = 0; for (const d of doors) if (Math.hypot(d.x - st.x, d.z - st.z) < 14) nd++; if (!nd) continue; const bd = Math.hypot(st.x - (e.A.x + ux * st.s), st.z - (e.A.z + uz * st.s)) || 1; const nx = (st.x - (e.A.x + ux * st.s)) / bd, nz = (st.z - (e.A.z + uz * st.s)) / bd; const back = Math.min(bd * 0.55, 9); for (const sgn of [1, -1]) { const px = st.x - nx * back - ux * 5 * sgn, pz = st.z - nz * back - uz * 5 * sgn; out.push({ nd, px, pz, yaw: Math.atan2(-(st.x - px), -(st.z - pz)), bx: st.x, bz: st.z }); } } } out.sort((p, q) => q.nd - p.nd); const keep = []; for (const c of out) { if (keep.length >= 6) break; if (clear(c.px, c.pz, c.bx, c.bz)) keep.push(c); } return { ok: keep.length > 0, cands: keep, stations: out.length / 2 | 0 }; } """ # Same visibility discipline as R41 ("in the frustum" is not "in the picture") plus the R42 addition: # every figure is resolved to the ped GLB it is wearing. STREET_MEASURE = r""" async () => { const P = window.PROCITY, C = P.citizens, V3 = P.scene.position.constructor; const THREE = await import('three'); const cam = P.camera; cam.updateMatrixWorld(true); const ray = new THREE.Raycaster(); ray.far = 30; const names = (P.fleet.all || []).map(r => r.pedName); const visible = (c) => { const dx = c.x - cam.position.x, dz = c.z - cam.position.z, d = Math.hypot(dx, dz); ray.set(new V3(cam.position.x, 1.35, cam.position.z), new V3(dx / d, 0, dz / d)); const h = ray.intersectObject(P.scene, true) .filter(x => !x.object.isSkinnedMesh && !x.object.isSprite && !x.object.isPoints); return !(h.length && h[0].distance < d - 0.5); }; const v = new V3(); const rows = []; const mid = []; for (const c of C.activeCitizens()) { const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z); v.set(c.x, 1.0, c.z).project(cam); const inShot = !(v.z > 1 || Math.abs(v.x) > 0.62 || Math.abs(v.y) > 0.55); if (c.tier === 'mid' && inShot && d <= 70) mid.push(names[c.pedIndex] || '?'); if (!c.actor || c.actorKind !== 'rig') continue; if (d < 3.0 || d > 22 || !inShot || !visible(c)) continue; let lo = 1e9, hi = -1e9; c.actor.inner.updateWorldMatrix(true, true); c.actor.inner.traverse(o => { if (o.isBone) { const w = new V3(); o.getWorldPosition(w); if (w.y < lo) lo = w.y; if (w.y > hi) hi = w.y; } }); const posedNow = c.loiter > 0; rows.push({ id: c.id, d: +d.toFixed(1), ped: names[c.pedIndex] || '?', state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit' : (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk'), clip: c.bench ? c.posture.sit : c.lean ? c.posture.lean : (posedNow && c.glance) ? '@look' : (posedNow ? c.posture.idle : c.posture.walk), h: +c.height.toFixed(3), stature: +(hi - lo).toFixed(3), footY: +lo.toFixed(3) }); } rows.sort((a, b) => a.d - b.d); const clips = new Set(rows.map(r => r.clip)), states = new Set(rows.map(r => r.state)); // [R42] the R41 score plus a CAST term: this round's frame is about who is in it, so a frame // showing five different bodies beats one showing the same body five times. const bodies = new Set(rows.map(r => r.ped)); return { rows, midPeds: mid, clips: clips.size, states: [...states], bodies: bodies.size, posed: rows.filter(r => r.state === 'bench-sit' || r.state === 'lean').length, score: clips.size * 100 + rows.length * 25 + bodies.size * 120 + rows.filter(r => r.state === 'bench-sit' || r.state === 'lean').length * 350, info: window.DBG.info() }; } """ # ── pub: Lane F's own §41.6 hero recipe, so the pair is comparable ───────────────────────────────── ENTER_PUB = r""" async () => { const P = window.PROCITY, D = window.DBG; const pubs = (P.plan.shops || []).filter(s => s.type === 'pub'); if (!pubs.length) return { ok: false, why: 'no pub in this town' }; let best = null; for (const s of pubs.slice(0, 6)) { D.enterShop(s.id); await new Promise(r => setTimeout(r, 2600)); const room = P.interiorMode.current; const crew = P.interiorMode.crew; const n = (room ? 1 : 0) && ((crew ? crew.members.length : 0) + (((P.interiorMode.keepers || {}).keepers || []).length)); if (room && (!best || n > best.figures)) { best = { shop: { id: s.id, name: s.name, type: s.type }, figures: n }; if (n >= 6) break; } D.exitShop(); await new Promise(r => setTimeout(r, 400)); } if (!best) return { ok: false, why: 'no pub interior would build' }; D.enterShop(best.shop.id); await new Promise(r => setTimeout(r, 2800)); return { ok: true, ...best, gig: P.gigs ? P.gigs.stateOf(best.shop.id) : null }; } """ PUB_FRAME = r""" async ({ back, eye }) => { const P = window.PROCITY, V3 = P.scene.position.constructor; const room = P.interiorMode.current; if (!room) return { ok: false, why: 'no room open' }; const sp = room.spawn || { x: 0, z: 0 }; const tx = 0, tz = -room.dims.D * 0.12, ty = 1.05; // F's 'centre' target, verbatim let dx = sp.x - tx, dz = sp.z - tz; const L = Math.hypot(dx, dz) || 1; dx /= L; dz /= L; const d = Math.min(Math.max(back, 1.9), Math.max(L, back)); const px = tx + dx * d, pz = tz + dz * d; P.camera.position.set(px, eye, pz); P.camera.lookAt(new V3(tx, ty, tz)); P.camera.updateMatrixWorld(true); P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera); return { ok: true, cam: [+px.toFixed(2), +pz.toFixed(2)], target: [+tx.toFixed(2), +tz.toFixed(2)], draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles, dims: room.dims }; } """ PUB_MEASURE = r""" () => { const P = window.PROCITY, V3 = P.scene.position.constructor; const names = (P.fleet.all || []).map(r => r.pedName); const cam = P.camera; cam.updateMatrixWorld(true); const v = new V3(); const rows = []; const add = (a, kind, role, pedIndex) => { if (!a || !a.inner) { rows.push({ state: kind, role, ped: 'placeholder', h: 0, stature: 0 }); return; } let lo = 1e9, hi = -1e9; a.inner.updateWorldMatrix(true, true); a.inner.traverse(o => { if (o.isBone) { const w = new V3(); o.getWorldPosition(w); if (w.y < lo) lo = w.y; if (w.y > hi) hi = w.y; } }); v.set(a.fig.position.x, 1.0, a.fig.position.z).project(cam); rows.push({ state: kind, role, ped: (pedIndex != null && names[pedIndex]) || '?', h: +(a.height || a.nominalHeight || 0).toFixed(3), stature: +(hi - lo).toFixed(3), clip: ((a.mixer && a.mixer._actions) || []).filter(x => x.getEffectiveWeight() > 0.5) .map(x => x.getClip().name)[0] || null, inFrame: !(v.z > 1 || Math.abs(v.x) > 0.92 || Math.abs(v.y) > 0.92) }); }; const crew = P.interiorMode.crew; if (crew) for (const m of crew.members) add(m.actor, m.part === 'band' ? 'band' : 'crowd', m.role, m.pedIndex); for (const k of ((P.interiorMode.keepers || {}).keepers || [])) add(k.actor, k.browse ? 'browser' : 'keeper', k.type, k.pedIndex); return { rows, info: window.DBG.info() }; } """ def port_up(p): with socket.socket() as s: s.settimeout(0.4); return s.connect_ex(('127.0.0.1', p)) == 0 def serve(): pr = subprocess.Popen([sys.executable, '-c', NOSTORE, str(PORT), str(ROOT / 'web')], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) for _ in range(80): if port_up(PORT): return pr time.sleep(0.1) pr.terminate(); raise SystemExit(f'could not serve on :{PORT}') def human_line(rows): """The R10 no-giants line, height-RELATIVE (D's R41 finding: a 1.32 m wall-leaner is not a defect, a 2.4 m anything is, and a body folded to hip height is the R16 failure this exists to catch).""" out, bad = [], [] for r in rows: if r.get('ped') == 'placeholder': continue h = r.get('h') or 0 ratio = (r['stature'] / h) if h else None who = r.get('id', r.get('state')) if r['stature'] > HI: bad.append(f'{who} GIANT {r["stature"]}m') elif h and r['stature'] < 0.55 * h: bad.append(f'{who} FOLDED {r["stature"]}m of {h}m') elif h and not (LO <= h <= HI): bad.append(f'{who} nominal {h}m out of [{LO},{HI}]') out.append(f"{r['state']}" + (f"[{r['ped']}]" if r.get('ped') else '') + f" {r['stature']}m" + (f" ({ratio:.0%} of its {h}m nominal)" if ratio else '')) verdict = ('ALL HUMAN-SIZED — no giant (>2.0 m), no fold (<55% of nominal)' if not bad else 'FAIL: ' + ', '.join(bad)) return (f"HUMAN-SIZED LINE (R10) — {len(out)} figure(s), stature (feet→crown) measured off the " f"posed skeleton: {' · '.join(out)}. {verdict}."), not bad def cast_line(peds, label): seen = {} for p in peds: seen[p] = seen.get(p, 0) + 1 banned = sorted(set(peds) & set(RETIRED)) return (f"CAST ({label}) — {len(peds)} figures wearing {len(seen)} distinct bodies: " + ', '.join(f'{k}×{v}' for k, v in sorted(seen.items())) + '. ' + ('PERIOD LAW: none of the five retired bodies is in this frame.' if not banned else f'RETIRED BODY IN FRAME: {banned}')), not banned def boot(pg, q=''): pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1' + (('&' + q) if q else '')) pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=45000) pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }") pg.wait_for_function('() => window.PROCITY.fleet && window.PROCITY.fleet.ready', timeout=25000) def main(): global rc OUTDIR.mkdir(parents=True, exist_ok=True) from playwright.sync_api import sync_playwright srv = serve() try: with sync_playwright() as p: # ── 1. the street ───────────────────────────────────────────────────────────────────── print('\033[1m1. r42_street_crowd — the recast town, on the footpath\033[0m') b = p.chromium.launch() pg = b.new_page(viewport={'width': 1280, 'height': 720}) errs = [] pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None) pg.on('pageerror', lambda e: errs.append(str(e))) boot(pg, 'weather=0&magpie=0&washing=0') pg.evaluate("() => { window.DBG.setSegment(2); window.DBG.shot('crossroads_busy'); }") pg.wait_for_timeout(45000) # let the crowd reach its posture steady state place = pg.evaluate(PLACE) print(f" place: {place.get('stations', 0)} bench stations near shop doors, " f"{len(place.get('cands', []))} clear camera poses") best = None for cand in place.get("cands", [])[:6]: pg.evaluate('([x,z,y]) => window.DBG.teleport(x,z,y)', [cand['px'], cand['pz'], cand['yaw']]) pg.wait_for_timeout(2500) for _ in range(14): m = pg.evaluate(STREET_MEASURE) if m['rows'] and (not best or m['score'] > best['score']): pg.screenshot(path=str(OUTDIR / 'r42_street_crowd.jpg'), type='jpeg', quality=92) best = dict(m, **{k: cand[k] for k in ('px', 'pz', 'yaw')}) print(f" ({cand['px']:.0f},{cand['pz']:.0f}) score {m['score']} " f"n={len(m['rows'])} clips={m['clips']} bodies={m['bodies']} " f"posed={m['posed']}") # good enough to stop hunting: two posed states, four clips, four distinct bodies if best and best['posed'] >= 2 and best['clips'] >= 4 and best['bodies'] >= 4: break pg.wait_for_timeout(2500) pg.evaluate('([x,z,y]) => window.DBG.teleport(x,z,y)', [cand['px'], cand['pz'], cand['yaw']]) if best and best['posed'] >= 2 and best['clips'] >= 4 and best['bodies'] >= 4: break if not best: FAIL('street: never framed anybody') else: line, ok = human_line(best['rows']) cast, cok = cast_line([r['ped'] for r in best['rows']] + best['midPeds'], 'near rigs + mid billboards in frame') states = {} for r in best['rows']: states[r['state']] = states.get(r['state'], 0) + 1 cap = (f"PROCITY R42 §42.4 — r42_street_crowd.jpg (THE RECAST ROSTER, ON THE STREET)\n" f"seed {SEED} · synthetic · MIDDAY · camera ({best['px']:.1f}, {best['pz']:.1f}) " f"yaw {best['yaw']:.3f}\n" f"{len(best['rows'])} near-tier rigs in frame · states {states} · " f"{best['clips']} distinct clips: {', '.join(sorted(set(r['clip'] for r in best['rows'])))}\n" f"draws {best['info']['drawCalls']} / budget {best['info']['budget']['draws']} · " f"tris {best['info']['tris']}\n\n{cast}\n\n{line}\n") (OUTDIR / 'r42_street_crowd.txt').write_text(cap) print(cap) if not ok: FAIL('street: human-sized line') if not cok: FAIL('street: a retired body is in frame') OK('r42_street_crowd.jpg + .txt') if errs: FAIL(f'street: {len(errs)} console error(s): {errs[:2]}') b.close() # ── 2. the pub (the before/after against laneF_r41/pub_furnished.jpg) ───────────────── print('\n\033[1m2. r42_pub_crowd — the same room as R41\'s pub_furnished, recast\033[0m') b = p.chromium.launch() pg = b.new_page(viewport={'width': 1280, 'height': 720}) errs = [] pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None) pg.on('pageerror', lambda e: errs.append(str(e))) boot(pg, 'gigs=1&weather=0&magpie=0&washing=0') pg.evaluate('() => window.DBG.setSegment(5)') pg.wait_for_timeout(1500) r = pg.evaluate(ENTER_PUB) if not r.get('ok'): FAIL(f"pub: {r.get('why')}") else: fr = pg.evaluate(PUB_FRAME, {'back': 5.2, 'eye': 1.62}) if not fr.get('ok'): FAIL(f"pub frame: {fr.get('why')}") else: m = pg.evaluate(PUB_MEASURE) pg.screenshot(path=str(OUTDIR / 'r42_pub_crowd.jpg'), type='jpeg', quality=92) inframe = [x for x in m['rows'] if x.get('inFrame')] line, ok = human_line(inframe) cast, cok = cast_line([x['ped'] for x in m['rows'] if x['ped'] != 'placeholder'], 'every figure in the room') cap = (f"PROCITY R42 §42.4 — r42_pub_crowd.jpg (THE BEFORE/AFTER)\n" f"BEFORE: docs/shots/laneF_r41/pub_furnished.jpg — same seed, same recipe, and the\n" f"purple EL CHUPACABRA luchador (comical_luchador_01) in the gig crowd.\n" f"seed {SEED} · {r['shop']['name']} (pub) · segment 5 · ?gigs=1 · gig state " f"{r.get('gig')}\n" f"camera {fr['cam']} → {fr['target']} (Lane C's room.spawn line, back 5.2 m, eye " f"1.62 m — Lane F's §41.6 hero recipe verbatim, so the pair is comparable)\n" f"{len(m['rows'])} figures in the room, {len(inframe)} in frame · " f"{fr['draws']} draws of the ≤350 interior law · {fr['tris']} tris · " f"room {fr['dims']['W']}x{fr['dims']['D']} m\n\n{cast}\n\n{line}\n") (OUTDIR / 'r42_pub_crowd.txt').write_text(cap) print(cap) if not ok: FAIL('pub: human-sized line') if not cok: FAIL('pub: a retired body is in frame') OK('r42_pub_crowd.jpg + .txt') if errs: FAIL(f'pub: {len(errs)} console error(s): {errs[:2]}') b.close() finally: srv.terminate() print('\n\033[32m● shots written\033[0m' if rc == 0 else '\n\033[31m● problems — see above\033[0m') return rc if __name__ == '__main__': sys.exit(main())