#!/usr/bin/env python3 """PROCITY v5.0 RELEASE TOUR — THE LIVE CRATE (Lane F, R27 ledger #5) The crate walk, in the order you'd actually do it: Musgrave Road → Monster Robot Party's frontage (B's stocked-shopfront mark) → inside, riffling ITS OWN records → a mint shop, captioned honestly → the `?classic=1` control that still boots the exact v2 town. Posing: the street frames go through F's R21 cluster primitive (reused from tour_shots.py). Per-EDGE shop counts are ~1 on a real graph, so "the busiest edge" poses you at bare bitumen — the Fitzroy trap, twice learned. The interior frames pose themselves: the room builds you a metre inside its own door. Honesty rules baked in, not bolted on: · the mint frame says MINT in its caption. A minted crate is plausible, not real, and a tour is exactly where that distinction gets quietly lost. · the real frame's caption carries the shop's actual name and address, because it IS one. · nothing here boots ?live=1. A tour frame must be reproducible by anyone who clones this repo, and tier 2 needs a POS none of them have. The live tier is proven by the gate, which owns its own server; the tour shows tier 1, which is what ships in the box. """ import json, subprocess, sys, time, pathlib ROOT = pathlib.Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / 'tools' / 'qa')) PORT = 8131 HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 OUTDIR = ROOT / 'docs' / 'shots' / 'release_v5' SHOP = 3962749 # name, query, segment, mode, caption V5_TOUR = [ ('musgrave_road', f'plansrc=osm&town=redhill_godverse&stock=real&seed={SEED}', 3, f'frontage:{SHOP}', "the frontage — Monster Robot Party on Musgrave Road, Red Hill, Brisbane. It is on this road in the " "game because it is on this road"), ('the_real_crate', f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&seed={SEED}', 3, f'shop:{SHOP}', "THE REAL CRATE — Monster Robot Party (147 Musgrave Rd): 120 records off its own till, real covers, " "real titles, real prices. Sell one in the shop and it leaves this crate"), ('a_mint_crate', f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&seed={SEED}', 3, 'mint', "a MINT crate — plausible, NOT real: a seeded sample standing in for a shop whose till we don't hold. " "Same tier, same offline guarantee, different provenance — and the atlas says so in its own index"), ('classic_covenant', f'classic=1&seed={SEED}', 2, 'street', "?classic=1 — the exact v2 town, byte-identical forever (0x3fa36874). Five epochs, one covenant"), ] SHOT_JS = r""" async ({ mode, seg, shop }) => { const P = window.PROCITY, D = window.DBG, T = P.THREE; D.setSegment(seg); const plan = P.plan, shops = plan.shops || []; if (mode.startsWith('shop:') || mode === 'mint') { let target; if (mode === 'mint') { const man = await (await fetch('assets/stock_godverse/index.json')).json(); const mint = new Set((man.shops||[]).filter(s => s.sourcing === 'mint').map(s => s.godverseShopId)); const byId = new Map((man.shops||[]).map(s => [s.godverseShopId, s])); target = shops.find(s => mint.has(s.godverseShopId) && (byId.get(s.godverseShopId).types||[]).includes(s.type)); } else { target = shops.find(s => s.godverseShopId === shop); } if (!target) return { err: 'no target shop for ' + mode }; D.enterShop('g:' + target.godverseShopId); await new Promise(r => setTimeout(r, 1500)); const si = D.stockInfo(); // pose: stand back from the spawn looking into the room, so the crates are in frame const im = P.interiorMode, sp = im.current.spawn; P.camera.position.set(sp.x, 1.6, sp.z + 0.2); P.camera.rotation.set(0, sp.ry || 0, 0); P.camera.updateMatrixWorld(); // Beauty shot: hide EVERY overlay, not just the street HUD. R21 taught this on the street frames // and F only fixed it there — the first v5 interior frame came back with "press Esc to leave" // across the top of the money shot. Same lesson, second surface. // Hide EVERY DOM overlay rather than guessing ids — the interior banner is created at runtime, so a // hardcoded selector list is exactly how the HUD sneaks back into the next money shot. for (const el of document.body.children) if (el.tagName !== 'CANVAS') el.style.display = 'none'; for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r)); const info = D.info(); return { name: si.name, gid: si.godverseShopId, items: si.packItems, base: si.base, shops: shops.length, edges: (plan.streets.edges||[]).length, draws: info.drawCalls, tris: info.tris }; } // ── street ── // R21's cluster primitive answers "where is this town's heart". For `frontage:` the question is // "where is THIS shop", and they are not the same question: Red Hill's densest cluster is 3 shops/60m // and does not contain Monster Robot, so the cluster pose produced a beautiful photograph of an empty // footpath — the Fitzroy trap a third time, wearing the fix for the first two. Anchor on the subject // when the frame is ABOUT the subject; use the cluster only when the frame is about the town. const nById = new Map(plan.streets.nodes.map(n => [n.id, n])); const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor'); let hub = -1, best = null; if (mode.startsWith('frontage:')) { const gid = +mode.split(':')[1]; const sh = shops.find(x => x.godverseShopId === gid); best = sh ? (plan.lots || []).find(l => l.id === sh.lot) : null; // shop.lot → lot.id if (best) { hub = lots.filter(o => Math.hypot(o.x - best.x, o.z - best.z) < 60).length; } if (!best) return { err: 'no lot for godverse ' + gid }; } else { for (const l of lots) { let c = 0; for (const o of lots) if (Math.hypot(o.x - l.x, o.z - l.z) < 60) c++; if (c > hub) { hub = c; best = l; } } } if (best) { const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0]; const a = nById.get(e.a), b2 = nById.get(e.b); const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L; let t = (best.x - a.x) * ux + (best.z - a.z) * uz; t = Math.max(6, Math.min(L - 6, t)); if (mode.startsWith('frontage:')) { // A frontage frame FACES the shop. The cluster pose faces ALONG the edge — correct for "here is a // street", useless for "here is this shop": it photographed Musgrave Road with Monster Robot out of // frame beside the lens. Stand on the road at the lot's projection, step back along the lot→road // normal, and look AT it. const px = a.x + ux * t, pz = a.z + uz * t; // the lot's own point on the road let nx = px - best.x, nz = pz - best.z; // lot → road normal const NL = Math.hypot(nx, nz) || 1; nx /= NL; nz /= NL; const cx = best.x + nx * (NL + 7), cz = best.z + nz * (NL + 7); D.teleport(cx, cz, Math.atan2(best.x - cx, best.z - cz)); } else { const back = Math.min(26, t); D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz)); } } else { D.shot('street_noon'); } await new Promise(r => setTimeout(r, 1800)); const hud = document.getElementById('pc-hud'); if (hud) hud.style.display = 'none'; for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r)); const info = D.info(); return { name: plan.name, shops: shops.length, edges: (plan.streets.edges || []).length, hub, draws: info.drawCalls, tris: info.tris }; } """ def ensure_server(): import socket s = socket.socket() try: s.connect(('127.0.0.1', PORT)); s.close(); return None except Exception: pass srv = subprocess.Popen([sys.executable, '-m', 'http.server', str(PORT)], cwd=str(ROOT / 'web'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(1.2) return srv def main(): try: from playwright.sync_api import sync_playwright except ImportError: sys.exit('playwright not installed') print(f"\033[1mv5.0 RELEASE TOUR — THE LIVE CRATE\033[0m ({len(V5_TOUR)} frames → docs/shots/release_v5/)") OUTDIR.mkdir(parents=True, exist_ok=True) srv = ensure_server() shots, errs_total = [], 0 try: with sync_playwright() as p: b = p.chromium.launch() for name, query, seg, mode, cap in V5_TOUR: pg = b.new_page(viewport={'width': 1280, 'height': 720}) errs = [] pg.on('pageerror', lambda e: errs.append(str(e))) pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None) pg.goto(f'{HOST}/index.html?{query}&dbg=1') pg.wait_for_function('window.PROCITY && window.PROCITY.plan && window.DBG && window.DBG.ready', timeout=25000) pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }") try: pg.wait_for_function('() => !window.PROCITY.fleet || window.PROCITY.fleet.ready', timeout=15000) except Exception: pass r = pg.evaluate(SHOT_JS, {'mode': mode, 'seg': seg, 'shop': SHOP}) if r.get('err'): print(f" \033[31m✗\033[0m {name}: {r['err']}"); errs_total += 1; pg.close(); continue pg.screenshot(path=str(OUTDIR / f'{name}.png')) extra = (f" · {r['items']} records from {r['base']}" if r.get('items') else (f" · retail heart: {r['hub']} shops/60m" if r.get('hub') else '')) caption = (f"{cap} · {r['name']}{extra} · {r['draws']} draws · {r['tris']:,} tris") shots.append({'name': name, 'file': f'{name}.png', 'caption': caption}) print(f" \033[32m●\033[0m {name}: {caption}") errs_total += len(errs) if errs: print(f" \033[31m{len(errs)} page error(s)\033[0m: {errs[0][:110]}") pg.close() b.close() finally: if srv: srv.terminate() html = ["PROCITY v5.0 — THE LIVE CRATE", "", "

PROCITY v5.0 — THE LIVE CRATE

", "

One real record shop, on its real street, holding its real stock — and a server that can die " "mid-session without the game noticing.

"] for s in shots: html.append(f"

{s['name']}

{s['caption']}

") (OUTDIR / 'contact.html').write_text('\n'.join(html)) (OUTDIR / 'shots.json').write_text(json.dumps(shots, indent=1)) print(f"\n → {OUTDIR}/contact.html ({len(shots)} frames, {errs_total} page error(s))") return 0 if (len(shots) == len(V5_TOUR) and errs_total == 0) else 1 if __name__ == '__main__': sys.exit(main())