#!/usr/bin/env python3 """Capture the two spots the user circled in their screenshot. Their view looks along the mainline with the ramp descending on the left; the two marks sit at the suppression boundary (mainline samples ~525 and ~551), where the aux lane ends and the parapet transitions back to the deck edge. Anchor the camera to those SAMPLES, live, so the shot cannot drift. left oval -> sample 525 area: was a 4-sample hole with no barrier right circle -> sample 551 area: was a 26-stud protruding fence stub """ import re import shutil import sys import time sys.path.insert(0, "/tmp") import rbx # noqa: E402 OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots" ANCHOR_CODE = """ local map = workspace:FindFirstChild("Map") if not map then return "NOMAP" end local deck = {} for _, d in ipairs(map:GetDescendants()) do if d:IsA("BasePart") then local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$") if k then deck[tonumber(k)] = d end end end local out = {} for _, i in ipairs({520, 525, 538, 551, 556}) do local d = deck[i] if d then table.insert(out, ("%.1f %.1f %.1f"):format(d.Position.X, d.Position.Y, d.Position.Z)) end end if #out < 5 then return "NOSAMP" end return table.concat(out, " | ") """ CAM_TPL = """ 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 "cam ok" """ def grab(name, px, py, pz, tx, ty, tz, fov=55): code = CAM_TPL.format(fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz) r = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": code}) if isinstance(r, dict) and r.get("isError"): print(f"{name}: CAM FAIL {r}") return None 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)[:160]}") return None dest = f"{OUT}/{name}.jpg" shutil.copy(imgs[0], dest) print(f"{name}: OK -> {dest}") return dest def main(): a = rbx.call("execute_luau", {"datamodel_type": "Edit", "code": ANCHOR_CODE}, wait=40, timeout=300) txt = a if isinstance(a, str) else str(a) if "NOMAP" in txt or "NOSAMP" in txt: print("ANCHOR FAIL: " + txt[:200]) return trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt) if len(trip) < 5: print("ANCHOR PARSE FAIL: " + txt[:300]) return p = [tuple(float(v) for v in t) for t in trip] s520, s525, s538, s551, s556 = p print("anchors: " + str(p)) made = [] # LEFT OVAL: the formerly-bare run. Look along the road from upstream so the # barrier line is seen edge-on -- a gap would read as an obvious break. made.append(grab("fix_left_oval", s520[0] - 90, s520[1] + 55, s520[2] - 90, s538[0], s538[1], s538[2], fov=55)) # RIGHT CIRCLE: the transition where the fence used to jut out 26 studs. made.append(grab("fix_right_circle", s556[0] + 120, s556[1] + 50, s556[2] + 120, s551[0], s551[1], s551[2], fov=50)) # Top-down over each boundary: a jut shows instantly as a step in the line. made.append(grab("fix_boundary_525_top", s525[0], s525[1] + 300, s525[2], s525[0], s525[1], s525[2], fov=55)) made.append(grab("fix_boundary_551_top", s551[0], s551[1] + 300, s551[2], s551[0], s551[1], s551[2], fov=55)) # Wide overview matching the user's framing. made.append(grab("fix_overview", s538[0] - 420, s538[1] + 240, s538[2] - 420, s538[0], s538[1], s538[2], fov=60)) made = [m for m in made if m] print("\nMADE " + str(len(made)) + " shots") for m in made: print(m) if __name__ == "__main__": main()