#!/usr/bin/env python3 """PROCITY Lane F — R41 §41.6 THE PROOF SHOTS: the round the game visibly changed. tools/.venv/bin/python tools/qa/r41_tour.py [--only NAME[,NAME…]] [--outdir DIR] R41 is the first round in a long time whose whole point is VISIBLE, so the tour is evidence, not decoration. Six frames, each chosen by MEASUREMENT (the camera is derived from where the thing actually is, then the frame is checked for what is actually in it), plus three before/after pairs where the control arm was cheap: street_postures a busy street with citizens in VISIBLY DIFFERENT postures — D's headline. The camera is not a taste call: it is the live crowd's own best cluster, found by polling until a bench-sitter, a leaner and ≥2 walkers project into one frame, then held. PAIR: the same seed/camera at ?clips=0 (R40's street). browse_interior a browsing interior — browsers examining/holding at Lane C's browse points, framed from Lane C's own room.spawn (the view walking in gives you). pub_furnished the furnished pub. It had a bar and almost nothing else before. PAIR: the same room built with Lane C's `noKit:true` — the exact pre-R41 room. record_dj_booth the record shop's rigged DJ booth: deck_1200_rigged + mixer_ttm54i_rigged + a static 1200. Camera aimed at the booth group by its own userData. opshop_wardrobe an op-shop rack wearing the 52 real 1990s Australian garment layers. PAIR: the same shop, same camera, on a default boot — the procedural canvas. credits_panel Lane B's new attribution surface (F2), on an OSM boot so the ODbL credit that the round exists to make sayable is on screen. R10 LAW: every frame containing a humanoid carries the human-sized line in a .txt sidecar — stature (feet→crown, off the POSED skeleton) for every figure in shot, as a ratio of that citizen's own nominal height. Fails on a giant (>2.0 m) or a fold (<55% of nominal), never on an absolute seated band (the library's leans legitimately take 20% off a standing crown — D's R41 finding). Fresh headless context per frame, own no-store server. Exit 0 = every frame captured and every human-sized line clean. """ import sys, os, json, time, socket, subprocess, pathlib ROOT = pathlib.Path(__file__).resolve().parent.parent.parent PORT = int(os.environ.get('PROCITY_R41_TOUR_PORT', '8982')) HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 OUTDIR = ROOT / 'docs' / 'shots' / 'laneF_r41' LO, HI = 1.4, 2.0 if '--outdir' in sys.argv: OUTDIR = pathlib.Path(sys.argv[sys.argv.index('--outdir') + 1]) ONLY = None if '--only' in sys.argv: ONLY = set(sys.argv[sys.argv.index('--only') + 1].split(',')) fails = [] def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}") def OK(m): print(f" \033[32m✓\033[0m {m}") def head(m): print(f"\n\033[1m{m}\033[0m") def note(m): print(f" \033[33m·\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() ''' # ── the street: find the crowd's own best cluster, and hold until the postures are in it ────────── # Postures run on 6-20 s dwells, so the honest way to shoot the busy moment is to stand still until # it happens. The camera stands out on the carriageway: the peds are at the back of a 3.5 m verge, so # there is no room to back off beside them (D measured this the hard way in §41.3). STREET_LOOK = r""" () => { const P = window.PROCITY, C = P.citizens, V3 = P.scene.position.constructor; const cam = P.camera; const people = []; for (const c of C.activeCitizens()) { if (!c.actor || c.actorKind !== 'rig') continue; const posedNow = c.loiter > 0; people.push({ id: c.id, x: c.x, z: c.z, state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit' : (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk') }); } // An ANCHOR is any rigged citizen who is not walking — bench-sit and lean are the round's headline, // but a citizen stopped at a window in their own seeded idle is the third of D's three new states and // is just as much the point. Only ~2-10 of the ~150 active citizens are near-tier RIGS at any instant // (the rest are impostors and carry no posture), so the anchor set is small and worth widening. const anchors = people.filter(p => p.state !== 'walk'); if (!anchors.length) return { ok: false, why: 'no non-walking rig on the street yet', n: people.length }; // Score every candidate stand-off around every anchor: 12 bearings x 3 ranges, keeping the pose // that puts the MOST DISTINCT posture states in frame (a frame of six walkers proves nothing). // Candidate LOOK-AT targets: every anchor, plus the midpoint of every pair of anchors in DIFFERENT // states — the frame the round actually wants (a sitter AND a leaner AND the walkers between them) // is a frame aimed between two of them, and no amount of orbiting a single anchor will find it. const targets = anchors.map(a => ({ x: a.x, z: a.z, state: a.state })); for (let i = 0; i < anchors.length; i++) for (let j = i + 1; j < anchors.length; j++) if (anchors[i].state !== anchors[j].state && Math.hypot(anchors[i].x - anchors[j].x, anchors[i].z - anchors[j].z) < 34) targets.push({ x: (anchors[i].x + anchors[j].x) / 2, z: (anchors[i].z + anchors[j].z) / 2, state: anchors[i].state + '+' + anchors[j].state }); let best = null; for (const a of targets) { for (let i = 0; i < 12; i++) { const th = i * Math.PI / 6; for (const r of [7, 9.5, 12, 15, 18]) { const px = a.x + Math.cos(th) * r, pz = a.z + Math.sin(th) * r; cam.position.set(px, 1.62, pz); cam.lookAt(new V3(a.x, 1.25, a.z)); cam.updateMatrixWorld(true); const v = new V3(); const seen = []; for (const p of people) { const d = Math.hypot(p.x - px, p.z - pz); if (d < 2.4 || d > 26) continue; v.set(p.x, 1.0, p.z).project(cam); if (v.z > 1 || Math.abs(v.x) > 0.9 || Math.abs(v.y) > 0.9) continue; seen.push(p); } const states = new Set(seen.map(s => s.state)); const score = states.size * 100 + seen.length * 10 - r * 4; // ties go to the CLOSER frame — a // busy street 18 m off is mostly pavement (measured: the first accepted frame was two thirds footpath) if (!best || score > best.score) best = { score, px, pz, ax: a.x, az: a.z, states: [...states], inFrame: seen.length, anchor: a.state }; } } } cam.position.set(best.px, 1.62, best.pz); cam.lookAt(new V3(best.ax, 1.25, best.az)); cam.updateMatrixWorld(true); // MEASURE IN THE SAME BEAT. Searching in one evaluate and measuring in the next lets the dwell // clock run between them, and a bench somebody has just got up from is the frame you actually // shoot (measured: the first cut of this harness did exactly that, twice). const v2 = new V3(); const rows = []; for (const c of C.activeCitizens()) { if (!c.actor || c.actorKind !== 'rig') continue; const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z); if (d < 2.2 || d > 26) continue; v2.set(c.x, 1.0, c.z).project(cam); if (v2.z > 1 || Math.abs(v2.x) > 0.92 || Math.abs(v2.y) > 0.92) 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; // THE CLIP IS READ OFF THE MIXER, not off `c.posture`. `posture` is the ASSIGNMENT — a pure // function of (citySeed, id) that exists whether or not the library ever loads (D's law: decided // once, never re-rolled; the base clip plays while a variant is in flight). Reading it reports // `walk_shopping_bag` on a ?clips=0 boot that has fetched nothing — exactly the note-about-the- // thing this round's control arm exists to avoid. Measured: the first cut of this caption did it. const playing = ((c.actor.mixer && c.actor.mixer._actions) || []) .filter(x => x.getEffectiveWeight() > 0.5).map(x => x.getClip().name); rows.push({ id: c.id, d: +d.toFixed(1), state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit' : (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk'), clip: playing[0] || null, assigned: 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) }); } rows.sort((a, b) => a.d - b.d); const states = [...new Set(rows.map(r => r.state))].sort(); const clips = [...new Set(rows.map(r => r.clip).filter(Boolean))].sort(); // Score on DISTINCT CLIPS as well as distinct states — which is the honest headline measure and D's // own (§41.3 census: 4 distinct clips across the whole crowd → 20). Two walkers on `@walk` and // `walk_shopping_bag` beside a sitter is three visibly different postures in one frame even though // the state machine calls two of them "walk". return { ok: true, ...best, rows, states, clips, inFrame: rows.length, score2: states.length * 100 + clips.length * 60 + rows.length * 10, info: window.DBG.info(), clipStats: C.clipStats() }; } """ # The R10 line: stature off the POSED skeleton for every rig actually in the picture. STREET_MEASURE = r""" () => { const P = window.PROCITY, C = P.citizens, V3 = P.scene.position.constructor; const cam = P.camera; cam.updateMatrixWorld(true); const v = new V3(); const rows = []; for (const c of C.activeCitizens()) { if (!c.actor || c.actorKind !== 'rig') continue; const d = Math.hypot(c.x - cam.position.x, c.z - cam.position.z); if (d < 2.2 || d > 26) continue; v.set(c.x, 1.0, c.z).project(cam); if (v.z > 1 || Math.abs(v.x) > 0.92 || Math.abs(v.y) > 0.92) 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; // THE CLIP IS READ OFF THE MIXER, not off `c.posture`. `posture` is the ASSIGNMENT — a pure // function of (citySeed, id) that exists whether or not the library ever loads (D's law: decided // once, never re-rolled; the base clip plays while a variant is in flight). Reading it reports // `walk_shopping_bag` on a ?clips=0 boot that has fetched nothing — exactly the note-about-the- // thing this round's control arm exists to avoid. Measured: the first cut of this caption did it. const playing = ((c.actor.mixer && c.actor.mixer._actions) || []) .filter(x => x.getEffectiveWeight() > 0.5).map(x => x.getClip().name); rows.push({ id: c.id, d: +d.toFixed(1), state: c.bench ? 'bench-sit' : c.lean ? 'lean' : (posedNow && c.sit) ? 'sit' : (posedNow && c.glance) ? 'glance' : (posedNow ? 'idle' : 'walk'), clip: playing[0] || null, assigned: 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) }); } rows.sort((a, b) => a.d - b.d); return { rows, info: window.DBG.info(), clips: C.clipStats() }; } """ # ── interiors: frame a room from Lane C's own spawn, or at a named target ───────────────────────── INT_FRAME = r""" async ({ target, 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 }; let tx, tz, ty = 1.15; if (target === 'people') { const figs = ((P.interiorMode.keepers || {}).keepers || []).map(k => k.actor.fig); if (!figs.length) return { ok: false, why: 'no figures in the room' }; tx = 0; tz = 0; for (const f of figs) { tx += f.position.x; tz += f.position.z; } tx /= figs.length; tz /= figs.length; } else if (target === 'centre') { tx = 0; tz = -room.dims.D * 0.12; ty = 1.05; } else { // a tagged group: djBooth, or a stock-bearing fitting let hit = null; room.group.traverse(o => { if (hit) return; const u = o.userData || {}; if (target === 'djBooth' && u.djBooth) hit = o; if (target === 'garment' && Array.isArray(u.buyItems) && u.buyItems.some(b => b && b.item && String(b.item.id).startsWith('wr_'))) hit = o; if (target === 'stockrack' && (u.isStock || u.buyMesh) && !hit) hit = o; }); if (!hit) return { ok: false, why: `no ${target} in this room` }; // A fitting group's ORIGIN sits on the floor, so aiming at it frames carpet and cuts the prop in // half. Aim at the bounding-box centre of what is actually there. const THREE = await import('three'); const c = new THREE.Box3().setFromObject(hit).getCenter(new V3()); tx = c.x; tz = c.z; ty = Math.max(0.95, c.y); } // stand off `back` metres from the target along the line from Lane C's spawn 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, shop: P.interiorMode.shop, dims: room.dims }; } """ INT_MEASURE = r""" () => { const P = window.PROCITY, V3 = P.scene.position.constructor; const rows = []; for (const k of ((P.interiorMode.keepers || {}).keepers || [])) { const a = k.actor; if (!a || !a.inner) continue; 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; } }); rows.push({ state: k.browse ? 'browser' : 'keeper', h: +(a.height || 0).toFixed(3), clip: ((a.mixer && a.mixer._actions) || []).filter(x => x.getEffectiveWeight() > 0.5) .map(x => x.getClip().name)[0] || null, stature: +(hi - lo).toFixed(3) }); } return { rows, info: window.DBG.info() }; } """ # Lane C's noKit control, built INSIDE the running game: the same shop, the same seed, the same # archetype, with every R41 spec dropped — the exact pre-R41 room, in the room's own scene. NOKIT_SWAP = r""" async () => { const P = window.PROCITY; const M = await import('./js/interiors/interiors.js'); const THREE = await import('three'); const shopRef = P.interiorMode.shop; const shop = (P.plan.shops || []).find(s => s.id === shopRef.id); const old = P.interiorMode.current; const before = { draws: null }; P.interiorMode.scene.remove(old.group); const manifest = await fetch('assets/manifest.json?x=' + Date.now()).then(r => r.ok ? r.json() : null).catch(() => null); const room = M.buildInterior(shop, THREE, { useGLB: true, manifest, noKit: true }); await room.glbReady; room.group.userData.kind = 'interior'; P.interiorMode.scene.add(room.group); window.__F_NOKIT = { room, old }; return { ok: true, shop: shopRef, pulls: room.pulls || null, fittings_before: (old.placement || []).length, fittings_after: (room.placement || []).length }; } """ NOKIT_FRAME = r""" ({ cam, target, eye }) => { const P = window.PROCITY, V3 = P.scene.position.constructor; P.camera.position.set(cam[0], eye, cam[1]); P.camera.lookAt(new V3(target[0], 1.05, target[1])); P.camera.updateMatrixWorld(true); P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera); return { draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles }; } """ def port_up(port): with socket.socket() as s: s.settimeout(0.4); return s.connect_ex(('127.0.0.1', port)) == 0 def serve(): p = 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 p time.sleep(0.1) p.terminate(); raise SystemExit(f'could not serve on :{PORT}') def human_line(rows): """The R10 no-giants line — height-RELATIVE, per D's R41 finding: a 1.32 m 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: h = r.get('h') or 0 ratio = (r['stature'] / h) if h else None if r['stature'] > HI: bad.append(f"{r.get('id', r['state'])} GIANT {r['stature']}m") elif h and r['stature'] < 0.55 * h: bad.append(f"{r.get('id', r['state'])} FOLDED {r['stature']}m of {h}m") elif h and not (LO <= h <= HI): bad.append(f"{r.get('id', r['state'])} nominal {h}m out of [{LO},{HI}]") out.append(f"{r['state']}" + (f"[{r['clip']}]" if r.get('clip') 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) in frame, stature (feet→crown) measured " f"off the posed skeleton: {' · '.join(out)}. {verdict}."), not bad def write_shot(pg, name, caption, rows=None, capture=True): OUTDIR.mkdir(parents=True, exist_ok=True) if capture: pg.screenshot(path=str(OUTDIR / f'{name}.jpg'), type='jpeg', quality=92) txt = caption if rows is not None: line, ok = human_line(rows) txt += '\n\n' + line if not ok: FAIL(f'{name}: {line}') (OUTDIR / f'{name}.txt').write_text(txt + '\n') OK(f'{name}.jpg + .txt') def new_page(p): 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))) return b, pg, errs def boot(pg, query): pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query 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'; }") # ── 1. THE STREET ──────────────────────────────────────────────────────────────────────────────── def shot_street(p): head('1. street_postures — the crowd stops being one person copy-pasted (+ the ?clips=0 pair)') pose = None for q, name in (('', 'street_postures'), ('clips=0', 'street_postures_clips0')): b, pg, errs = new_page(p) try: boot(pg, q + ('&' if q else '') + 'weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.setSegment(2)") pg.evaluate("() => window.DBG.shot('street_noon')") if not q: # HOLD until the crowd offers the frame, and CAPTURE THE INSTANT IT DOES. Postures run # on 6-20 s dwells, so a camera chosen from a poll twenty seconds ago is aimed at a # bench somebody has already got up from — measured, twice, before this loop was built. # The search + the measurement are one evaluate; the capture is the next statement; the # file on disk is always the best frame seen so far. # …and RESAMPLE THE TOWN, not just the clock: the sitters and leaners of one 30 m stretch # are the same dozen citizens, so holding one spot for a minute samples one bench. The # loop hops between the shell's own street bookmarks every 8 polls. # Measured first (probe over 5 bookmarks x 4 samples): the near-tier holds only 2-10 RIGS # at a time and `crossroads_busy` is where the rigs are (9-10 vs 1-3 elsewhere), and the # non-walk share of them builds over ~10 s of dwell. So: dwell long, hop rarely, start # where the people are. STOPS = ['crossroads_busy', 'street_noon', 'crossroads_busy', 'patronage_door', 'night_crowd'] best = None byAnchor = {} OUTDIR.mkdir(parents=True, exist_ok=True) for i in range(72): if i % 12 == 0: pg.evaluate(f"() => window.DBG.shot('{STOPS[(i // 12) % len(STOPS)]}')") pg.evaluate("() => window.DBG.setSegment(2)") pg.wait_for_timeout(900) r = pg.evaluate(STREET_LOOK) if not r.get('ok'): pg.wait_for_timeout(600); continue a = r['anchor'].split('+')[0] if not best or r['score2'] > best['score2']: best = r pg.wait_for_timeout(150) # one composer frame at the new pose pg.screenshot(path=str(OUTDIR / 'street_postures.jpg'), type='jpeg', quality=92) if a != 'walk' and (a not in byAnchor or r['score2'] > byAnchor[a]['score2']): byAnchor[a] = r pg.wait_for_timeout(150) pg.screenshot(path=str(OUTDIR / f'street_{a}.jpg'), type='jpeg', quality=92) if len(r['states']) >= 3 and len(r['clips']) >= 3 and r['inFrame'] >= 4: break pg.wait_for_timeout(450) if not best: FAIL('street_postures: never found a non-walking rig in 72 polls'); return pose = best m = dict(rows=best['rows'], info=best['info'], clips=best['clipStats']) note(f"camera ({best['px']:.1f},{best['pz']:.1f}) on the {best['anchor']} anchor · " f"{best['inFrame']} rigs in frame · states {best['states']} · clips {best['clips']}") # One frame can only hold what the near tier holds (2-10 rigs), so the round's three new # street states are also shot ONE PER ANCHOR — best frame per anchor kind, same boot. for a, r in sorted(byAnchor.items()): # …but never twice: if the per-anchor best IS the headline frame, it is the same # picture under a second name. if abs(r['px'] - best['px']) < 1e-6 and abs(r['pz'] - best['pz']) < 1e-6: for ext in ('.jpg', '.txt'): f = OUTDIR / f'street_{a}{ext}' if f.exists(): f.unlink() note(f'street_{a} is the headline frame itself — not written twice') continue line, ok2 = human_line(r['rows']) (OUTDIR / f'street_{a}.txt').write_text( f"PROCITY R41 §41.6 — street_{a} (supporting frame: the {a} state)\n" f"seed {SEED}, midday, camera ({r['px']:.2f}, {r['pz']:.2f}) → ({r['ax']:.2f}, {r['az']:.2f}).\n" f"{r['inFrame']} rigs in frame · states {r['states']} · clips {r['clips']} · " f"{r['info']['drawCalls']} draws of the ≤300 street law.\n\n{line}\n") if not ok2: FAIL(f'street_{a}: {line}') OK(f'street_{a}.jpg + .txt ({a}, {len(r["clips"])} clips in frame)') else: # the CONTROL arm: identical camera, R40's street (no motion library at all) pg.evaluate("""({px,pz,ax,az}) => { const P=window.PROCITY, V3=P.scene.position.constructor; P.player.teleport(px, pz, 0); P.camera.position.set(px, 1.62, pz); P.camera.lookAt(new V3(ax,1.25,az)); P.chunks.warmup(P.camera.position); P.camera.updateMatrixWorld(true); }""", pose) pg.wait_for_timeout(1500) m = pg.evaluate(STREET_MEASURE) states = sorted(set(r['state'] for r in m['rows'])) clips = sorted(set(r['clip'] for r in m['rows'] if r['clip'])) note(f"{name}: {len(m['rows'])} figures · states {states} · {len(clips)} distinct clips · " f"{m['info']['drawCalls']} draws / {m['info']['tris']} tris · bank {m['clips']}") cap = (f"PROCITY R41 §41.6 — {name}\n" f"seed {SEED}, midday (seg 2), camera ({pose['px']:.2f}, {pose['pz']:.2f}) looking at " f"({pose['ax']:.2f}, {pose['az']:.2f}) — the SAME pose on both arms.\n" f"{'DEFAULT BOOT (R41): the motion library is on.' if not q else 'CONTROL ?clips=0 (= R40): the motion library is OFF and nothing else changes.'}\n" f"{len(m['rows'])} rigs in frame · posture states {states} · {len(clips)} distinct clips " f"({', '.join(clips)}) · {m['info']['drawCalls']} draws of the ≤300 street law " f"({m['info']['tris']} tris) · clip bank {m['clips'].get('groups')} groups / " f"{m['clips'].get('clips')} clips / {m['clips'].get('bytes')} B.") write_shot(pg, name, cap, m['rows'], capture=bool(q)) if not q and (len(clips) < 3 or len(states) < 2): FAIL(f'street_postures: {len(states)} state(s) / {len(clips)} clip(s) in frame — ' f'the headline (visibly different postures) is not visible') if errs: FAIL(f'{name}: {len(errs)} console error(s): {errs[:2]}') finally: b.close() # ── 2-5. THE INTERIORS ─────────────────────────────────────────────────────────────────────────── def enter_best(pg, want_type=None, need=None, tries=10): """Enter a shop of `want_type` (or the most-occupied shop) and settle. `need` is a JS predicate name checked on the built room: 'browsers' | 'djBooth' | 'garment'.""" js = r""" async ({ want, need, tries }) => { const P = window.PROCITY, D = window.DBG, C = P.citizens; // Prefer the shop types whose floor is browsable (Lane C puts 3.00 browse points in every type, // but a record bin / clothes rail / bookshelf is what a browse clip READS as), then by how many // patronage occupants are actually inside — occupancy is what puts browsers on the floor. const PREF = ['record', 'opshop', 'book', 'video', 'stall', 'pawn']; const shops = (P.plan.shops || []).filter(s => !want || s.type === want) .map(s => ({ id: s.id, name: s.name, type: s.type, occ: (C.occupancyOf(s.id) || {}).count || 0 })) .sort((a, b) => (b.occ - a.occ) || ((PREF.indexOf(a.type) + 9) % 9) - ((PREF.indexOf(b.type) + 9) % 9)); const has = (room) => { if (!room) return false; if (need === 'browsers') return (((P.interiorMode.keepers || {}).keepers) || []).filter(k => k.browse).length > 0; if (need === 'djBooth') { let f = false; room.group.traverse(o => { if ((o.userData || {}).djBooth) f = true; }); return f; } if (need === 'garment') { let f = false; room.group.traverse(o => { const u = o.userData || {}; if (Array.isArray(u.buyItems) && u.buyItems.some(b => b && b.item && String(b.item.id).startsWith('wr_'))) f = true; }); return f; } return true; }; // Take the BEST candidate, not the first that works: the first shop with one browser and the // shop with three are both "ok", and only one of them is a photograph of a browsing interior. let best = null; const cands = shops.filter(s => need !== 'browsers' || s.occ > 0).slice(0, tries); for (const s of cands) { D.enterShop(s.id); await new Promise(r => setTimeout(r, 2600)); const room = P.interiorMode.current; const ks = ((P.interiorMode.keepers || {}).keepers) || []; if (room && has(room)) { const cand = { ok: true, shop: s, tried: shops.length, browsers: ks.filter(k => k.browse).length, keepers: ks.length, fittings: (room.placement || []).length }; if (!best || cand.browsers > best.browsers) best = cand; if (need !== 'browsers' || cand.browsers >= 3) return best; } D.exitShop(); await new Promise(r => setTimeout(r, 350)); } if (best) { D.enterShop(best.shop.id); await new Promise(r => setTimeout(r, 2600)); return best; } return { ok: false, why: `no ${want || 'shop'} satisfied "${need}"`, tried: shops.length }; } """ return pg.evaluate(js, dict(want=want_type, need=need, tries=tries)) def shot_interior(p, name, want_type, need, target, back, eye, seg, title, extra_q='', dwell=1200): head(f'{title}') b, pg, errs = new_page(p) try: boot(pg, f'gigs=1&weather=0&magpie=0&washing=0{extra_q}') pg.evaluate(f"() => window.DBG.setSegment({seg})") # Patronage is a wall-clock process (R8: peds duck in as they walk past), so a browser shot taken # 1.2 s after boot photographs an empty shop. Measured: 0 shops with occupants at 1.2 s. pg.wait_for_timeout(dwell) occ = pg.evaluate("() => (window.PROCITY.plan.shops||[]).filter(s => (window.PROCITY.citizens.occupancyOf(s.id)||{}).count > 0).length") note(f'after {dwell} ms of street time: {occ} shop(s) hold patronage occupants') r = enter_best(pg, want_type, need) if not r.get('ok'): FAIL(f"{name}: {r.get('why')}"); return None note(f"{r['shop']['name']} ({r['shop']['type']}, id {r['shop']['id']}) · {r['fittings']} fittings · " f"{r['keepers']} figures ({r['browsers']} browsing) · tried {r['tried']} candidates") fr = pg.evaluate(INT_FRAME, dict(target=target, back=back, eye=eye)) if not fr.get('ok'): FAIL(f"{name}: {fr.get('why')}"); return None m = pg.evaluate(INT_MEASURE) note(f"{fr['draws']} draws / {fr['tris']} tris · room {fr['dims']['W']}x{fr['dims']['D']} m · " f"camera {fr['cam']} → {fr['target']}") cap = (f"PROCITY R41 §41.6 — {name}\n" f"seed {SEED}, {r['shop']['name']} ({r['shop']['type']}), segment {seg}. " f"{r['fittings']} fittings placed · {r['keepers']} figures in the room " f"({r['browsers']} at Lane C browse points) · camera {fr['cam']} looking at {fr['target']} " f"(Lane C's own room.spawn line).\n" f"{fr['draws']} draws of the ≤350 interior law ({fr['tris']} tris).") write_shot(pg, name, cap, m['rows'] if m['rows'] else None) if errs: FAIL(f'{name}: {len(errs)} console error(s): {errs[:2]}') return dict(page=pg, browser=b, frame=fr, shop=r['shop']) finally: b.close() def shot_pub_pair(p): """Two things, deliberately separated. The HERO is the pub on gig night — which is what a player sees, and what the round is for. But a gig-night frame is NOT a fair before/after for §41.4, because the band and the crowd ride the room group and are disposed with it: a `noKit` rebuild takes the R41 fittings AND the eleven people out of the picture, and a pair like that credits the furniture with the crowd. So the A/B pair is shot separately, on a QUIET midday pub with the gig layer off, where the only thing that moves between the arms is the kit.""" head("4. pub_furnished (hero, gig night) + pub_kit_after/before (the honest A/B, quiet pub)") # ── the hero ────────────────────────────────────────────────────────────────────────────────── b, pg, errs = new_page(p) try: boot(pg, 'gigs=1&weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.setSegment(5)") pg.wait_for_timeout(1500) r = enter_best(pg, 'pub', None) if not r.get('ok'): FAIL(f"pub_furnished: {r.get('why')}") else: fr = pg.evaluate(INT_FRAME, dict(target='centre', back=5.2, eye=1.62)) m = pg.evaluate(INT_MEASURE) note(f"hero: {r['shop']['name']} · {r['fittings']} fittings · {fr['draws']} draws / {fr['tris']} tris " f"· {len(m['rows'])} figures") write_shot(pg, 'pub_furnished', f"PROCITY R41 §41.6 — pub_furnished (the hero: gig night)\n" f"seed {SEED}, {r['shop']['name']} (pub), segment 5, ?gigs=1. {r['fittings']} fittings " f"placed by §41.4 — the plywood bar, the DJ booth, the speaker stack, the jukebox, the " f"couch, the bistro table. The band and the crowd are the R13 gig layer, NOT R41 cargo: " f"the fittings claim is measured in the pub_kit pair, not here.\n" f"{fr['draws']} draws of the ≤350 interior law ({fr['tris']} tris). " f"Camera {fr['cam']} → {fr['target']}.", m['rows'] if m['rows'] else None) if errs: FAIL(f'pub_furnished: {len(errs)} console error(s): {errs[:2]}') finally: b.close() # ── the honest A/B: quiet pub, gig layer OFF, the kit is the only variable ──────────────────── b, pg, errs = new_page(p) try: # gigs stays ON — ?gigs=0 removes the venue TYPES from the plan entirely (measured: "no pub # satisfied", 0 pub lots), so the control for "no crowd" is the CLOCK, not the flag. And the # clock has to be SEGMENT 4: this town's one pub keeps 17:00-23:00 (measured), so midday is a # locked door and segment 5 is the gig. 18:30 is open, quiet, and crowd-free. boot(pg, 'gigs=1&weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.setSegment(4)") pg.wait_for_timeout(1500) r = enter_best(pg, 'pub', None) if not r.get('ok'): FAIL(f"pub_kit_after: {r.get('why')}"); return fr = pg.evaluate(INT_FRAME, dict(target='centre', back=5.2, eye=1.62)) if not fr.get('ok'): FAIL(f"pub_kit_after: {fr.get('why')}"); return m = pg.evaluate(INT_MEASURE) write_shot(pg, 'pub_kit_after', f"PROCITY R41 §41.6 — pub_kit_after (AFTER — the R41 kit)\n" f"seed {SEED}, {r['shop']['name']} (pub), 18:30 (segment 4) — open, gig not on, so no band and no crowd " f"on either arm and nothing but the furniture differs. {r['fittings']} fittings.\n" f"{fr['draws']} draws / {fr['tris']} tris. Camera {fr['cam']} → {fr['target']}, " f"identical on both arms.", m['rows'] if m['rows'] else None) sw = pg.evaluate(NOKIT_SWAP) if not sw.get('ok'): FAIL('pub_kit_before: the noKit rebuild failed'); return pg.wait_for_timeout(700) fr2 = pg.evaluate(NOKIT_FRAME, dict(cam=fr['cam'], target=fr['target'], eye=1.62)) note(f"noKit control: {sw['fittings_before']} → {sw['fittings_after']} fittings · " f"{fr['draws']} → {fr2['draws']} draws ({fr2['draws'] - fr['draws']:+d}) · " f"{fr['tris']} → {fr2['tris']} tris") write_shot(pg, 'pub_kit_before', f"PROCITY R41 §41.6 — pub_kit_before (BEFORE — the CONTROL)\n" f"The SAME shop, the SAME seed, the SAME camera, rebuilt with Lane C's `noKit: true`, which " f"drops exactly the R41 specs and reproduces the pre-R41 room.\n" f"fittings {sw['fittings_before']} → {sw['fittings_after']} · " f"draws {fr['draws']} → {fr2['draws']} ({fr2['draws'] - fr['draws']:+d}) · " f"tris {fr['tris']} → {fr2['tris']}. That difference is what §41.4 bought in this room, " f"measured — and nothing else in the frame moved.\n" f"NOTE: the keeper rides the room group and is disposed with it, so the control frame has " f"no shopkeeper. Every OTHER difference in the picture is the kit.") if fr2['draws'] >= fr['draws']: FAIL(f"pub pair: the noKit control is not cheaper ({fr2['draws']} vs {fr['draws']}) — the pair proves nothing") if sw['fittings_after'] >= sw['fittings_before']: FAIL(f"pub pair: noKit did not drop any fitting ({sw['fittings_before']} → {sw['fittings_after']})") if errs: FAIL(f'pub_kit pair: {len(errs)} console error(s): {errs[:2]}') finally: b.close() def shot_wardrobe_pair(p): head('5. opshop_wardrobe + opshop_wardrobe_off — the 52 garments, and the canvas that ships default') cam = target = None for q, name in (('&stock=real', 'opshop_wardrobe'), ('', 'opshop_wardrobe_off')): b, pg, errs = new_page(p) try: boot(pg, 'weather=0&magpie=0&washing=0' + q) pg.evaluate("() => window.DBG.setSegment(2)") pg.wait_for_timeout(1200) r = enter_best(pg, 'opshop', 'garment' if q else None) if not r.get('ok'): FAIL(f"{name}: {r.get('why')}"); return if q: fr = pg.evaluate(INT_FRAME, dict(target='garment', back=2.6, eye=1.55)) if not fr.get('ok'): FAIL(f"{name}: {fr.get('why')}"); return cam, target = fr['cam'], fr['target'] else: fr = pg.evaluate(NOKIT_FRAME, dict(cam=cam, target=target, eye=1.55)) fr = dict(fr, cam=cam, target=target, ok=True) si = pg.evaluate("() => window.DBG.stockInfo()") m = pg.evaluate(INT_MEASURE) ids = [i for i in (si.get('renderedIds') or []) if str(i).startswith('wr_')] titles = (si.get('renderedTitles') or [])[:6] note(f"{name}: {r['shop']['name']} · base {si.get('base')} · {len(ids)} garment ids · " f"{fr['draws']} draws / {fr['tris']} tris") cap = (f"PROCITY R41 §41.6 — {name}\n" f"seed {SEED}, {r['shop']['name']} (opshop), midday. Camera {fr['cam']} → {fr['target']}, " f"IDENTICAL on both arms.\n" + (f"?stock=real — Lane C's wardrobe pack, turned on by F's one line: base " f"{si.get('base')}, {si.get('packItems')} named 1990s Australian garment layers off " f"ONE 246 KB atlas, {len(ids)} of them on rendered meshes " f"({', '.join(titles)}…).\n" if q else "DEFAULT BOOT (the control, and what ships) — no pack, so Lane C's procedural garment " "canvas, byte-identically. Nothing breaks without the wardrobe.\n") + f"{fr['draws']} draws of the ≤350 interior law ({fr['tris']} tris).") write_shot(pg, name, cap, m['rows'] if m['rows'] else None) if q and not ids: FAIL(f'{name}: no wardrobe ids on rendered meshes — the shot proves nothing') if errs: FAIL(f'{name}: {len(errs)} console error(s): {errs[:2]}') finally: b.close() def shot_credits(p): head('6. credits_panel — Lane B\'s new attribution surface, on an OSM boot (the ODbL credit on screen)') b, pg, errs = new_page(p) try: boot(pg, 'plansrc=osm&town=katoomba&weather=0&magpie=0&washing=0') pg.evaluate("() => window.DBG.shot('street_noon')") pg.wait_for_timeout(600) link = pg.evaluate("() => { const el = document.getElementById('pc-creditlink'); return el ? el.textContent.trim() : null; }") pg.keyboard.press('F2') pg.wait_for_timeout(1200) st = pg.evaluate(r"""() => { const el = document.getElementById('pc-credits'); const t = el ? el.innerText : ''; // NB `offsetParent` is null for a position:fixed element even when it is on screen — // the panel is fixed/inset:0, so that test reports "closed" on a perfectly open panel. return { open: !!(el && getComputedStyle(el).display !== 'none'), entries: (window.PROCITY.credits && window.PROCITY.credits.count) || (t.match(/\n/g) || []).length, osm: /OpenStreetMap contributors/.test(t), odbl: /ODbL/.test(t), mixamo: /Mixamo/i.test(t), chars: t.length }; }""") note(f"HUD link: {link!r} · panel open {st['open']} · OSM string {st['osm']} · ODbL {st['odbl']} · " f"Mixamo row {st['mixamo']} · {st['chars']} chars of panel text") cap = (f"PROCITY R41 §41.6 — credits_panel\n" f"seed {SEED}, ?plansrc=osm&town=katoomba — an OSM boot, so the obligation is live and the HUD " f"link itself carries it: {link!r}.\n" f"F2 opens Lane B's §41.5 surface. On screen: the © OpenStreetMap contributors credit verbatim " f"({st['osm']}), the ODbL 1.0 licence name ({st['odbl']}), and the Mixamo row that records why " f"R41's 46 clips owe no attribution ({st['mixamo']}).\n" f"Zero draws by construction (DOM), zero boot fetches (credits.json is fetched on first open).") write_shot(pg, 'credits_panel', cap) if not st['open']: FAIL('credits_panel: the panel did not open on F2') if not st['osm']: FAIL('credits_panel: the OSM attribution string is not on screen') if errs: FAIL(f'credits_panel: {len(errs)} console error(s): {errs[:2]}') finally: b.close() def main(): srv = serve() try: from playwright.sync_api import sync_playwright with sync_playwright() as p: if not ONLY or 'street' in ONLY: shot_street(p) if not ONLY or 'browse' in ONLY: shot_interior(p, 'browse_interior', None, 'browsers', 'people', 3.0, 1.62, 2, "2. browse_interior — browsers examining and holding at Lane C's browse points", extra_q='&stock=real', dwell=25000) if not ONLY or 'record' in ONLY: shot_interior(p, 'record_dj_booth', 'record', 'djBooth', 'djBooth', 2.5, 1.45, 2, '3. record_dj_booth — the rigged 1200 + TTM-54i behind the counter') if not ONLY or 'pub' in ONLY: shot_pub_pair(p) if not ONLY or 'wardrobe' in ONLY: shot_wardrobe_pair(p) if not ONLY or 'credits' in ONLY: shot_credits(p) finally: srv.terminate() print('') if fails: print(f'\033[31m● FAIL\033[0m — {len(fails)} problem(s):') for f in fails: print(f' · {f}') return 1 print(f'\033[32m● PASS\033[0m — the tour is in {OUTDIR.relative_to(ROOT)}, every humanoid frame carrying its R10 line') return 0 if __name__ == '__main__': sys.exit(main())