#!/usr/bin/env python3 """Eyes-on shots of the PA flyover ramps. Camera targets are QUERIED FROM THE LIVE GEOMETRY on every run, never hardcoded. The previous capture script pinned literal coordinates at the old defect sites; after the ramps were redesigned those coords pointed at empty air and the shots silently framed the wrong thing. Same class of bug as the hardcoded yFork=96 -- a constant that stops matching the model. Each shot names a part and an offset; the part's real position is looked up in Studio at capture time. If a named part is missing the shot is SKIPPED with a loud message rather than falling back to a stale guess. """ import shutil import sys import time sys.path.insert(0, "/tmp") import rbx # noqa: E402 OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots" # name, anchor part, camera offset from anchor, look-at offset from anchor, fov SHOTS = [ # The flyover itself, from the side -- the money shot: ramp over deck. ("F1_flyover_side", "PAEntryRampCross_100", (300, 150, 300), (0, -20, 0), 55), # Underneath the crossing, looking up: proves real clearance. ("F2_flyover_under", "PAEntryRampCross_100", (140, -70, 140), (0, 10, 0), 75), # The spiral descending into the PA apron. ("F3_spiral_into_pa", "PAEntryRampTrunk_200", (340, 220, 340), (-120, -60, -120), 60), # Exit ramp climbing out of the PA. ("F4_exit_climb", "PAExitRampTrunk_200", (-340, 200, 300), (60, -40, 40), 60), # Where the descent lands on the far carriageway. ("F5_landing_merge", "PAEntryRampNear_200", (220, 130, 220), (0, -15, 0), 55), # Wide establishing shot of the whole interchange. ("F6_wide", "PAEntryRampCross_100", (900, 620, 900), (-250, -120, -250), 60), ] POS_QUERY = """ local ws=game:GetService("Workspace"); local map=ws:FindFirstChild("Map") if not map then return "NOMAP" end local want={} for _,n in ipairs({%s}) do want[n]=true end local found={} for _,d in ipairs(map:GetDescendants()) do if d:IsA("BasePart") and want[d.Name] then found[#found+1]=("%%s=%%.1f,%%.1f,%%.1f"):format(d.Name,d.Position.X,d.Position.Y,d.Position.Z) end end return table.concat(found,"|") """ CAM = """ local cam = workspace.CurrentCamera cam.CameraType = Enum.CameraType.Scriptable cam.FieldOfView = {fov} cam.CFrame = CFrame.lookAt(Vector3.new({px},{py},{pz}), Vector3.new({tx},{ty},{tz})) return "ok" """ def fetch_anchors(names): """Look up each anchor part's real position in the live datamodel.""" lst = ",".join('"%s"' % n for n in sorted(set(names))) r = rbx.call( "execute_luau", {"datamodel_type": "Edit", "code": POS_QUERY % lst}, wait=40, timeout=400, ) txt = str(r) out = {} for chunk in txt.split("|"): if "=" not in chunk: continue nm, _, vals = chunk.partition("=") nm = nm.strip().strip("'\"[]{}() ") parts = vals.strip().strip("'\"[]{}() ").split(",") if len(parts) != 3: continue try: out[nm] = tuple(float(v) for v in parts) except ValueError: continue return out def main(): anchors = fetch_anchors([s[1] for s in SHOTS]) print(f"resolved {len(anchors)} anchors: {sorted(anchors)}") made, skipped = [], [] for name, anchor, cam_off, look_off, fov in SHOTS: if anchor not in anchors: print(f"{name}: SKIP -- anchor {anchor} not found in model") skipped.append(name) continue ax, ay, az = anchors[anchor] px, py, pz = ax + cam_off[0], ay + cam_off[1], az + cam_off[2] tx, ty, tz = ax + look_off[0], ay + look_off[1], az + look_off[2] r = rbx.call( "execute_luau", { "datamodel_type": "Edit", "code": CAM.format( fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz ), }, wait=12, timeout=300, ) if isinstance(r, dict) and r.get("isError"): print(f"{name}: CAM FAIL {str(r)[:120]}") skipped.append(name) continue time.sleep(1.5) cap = rbx.call( "screen_capture", {"capture_id": "ScreenCapture_1"}, wait=22, timeout=300 ) imgs = cap.get("images") if isinstance(cap, dict) else None if not imgs: print(f"{name}: CAPTURE FAIL {str(cap)[:140]}") skipped.append(name) continue dest = f"{OUT}/{name}.jpg" shutil.copy(imgs[0], dest) made.append(dest) print(f"{name}: OK ({anchor} @ {ax:.0f},{ay:.0f},{az:.0f}) -> {dest}") print(f"\nMADE {len(made)} SKIPPED {len(skipped)} {skipped if skipped else ''}") if __name__ == "__main__": main()