Aux-lane suppression + PA geometry fixes; probe/verify tooling
CI / quality (push) Failing after 1m9s

MapBuilder.luau
  - Suppress the aux lane across samples 525-550 (AUX_SUPPRESS_FROM/TO),
    honoring the user's hand deletion in Studio so a rebuild no longer
    regenerates it.
  - Smoothstep the width ramp over AUX_BLEND=5 samples on each side. A hard
    cut produced a 26-stud lateral fence jog; the blend removes it.
  - DO NOT remove this suppression. Re-enabling it resurrects geometry the
    user deleted by hand.

rokit.toml
  - rojo 7.5.1 -> 7.7.0 to match the auto-updating Studio plugin.
    Server older than plugin fails the handshake with
    "attempt to index number with 'protocolVersion'".

tools/
  - 34 read-only probe/verify/capture scripts used to measure the datamodel
    instead of reasoning from source.

Verified-good baseline at this commit:
  total parts       16788
  PAWestSlip        60 segs, 34.4 deg total bend, worst 1.4, 0 kinks >5
  PAEastSlip        60 segs, 30.7 deg total bend, worst 1.3, 0 kinks >5
  AuxLane parts     30 (samples 525-550 deliberately absent)
  barrier coverage  823 samples, 0 unguarded
  fence jogs >10st  0
This commit is contained in:
2026-08-06 08:27:47 -04:00
parent c297b20aed
commit 6e50f9718d
36 changed files with 3723 additions and 1 deletions
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""Capture the speed-change (aux) lanes ANCHORED to the parts themselves.
Hardcoded camera offsets are how you end up photographing empty air after the
geometry moves. Every shot here is framed from the LIVE position of the aux
lane / slip parts queried out of the DataModel at capture time, so the camera
cannot drift away from the thing under test.
"""
import shutil
import sys
import time
sys.path.insert(0, "/tmp")
import rbx # noqa: E402
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
# Ask Studio where the aux lanes actually are, then frame from that.
# Returns "x y z | x y z ..." anchors: west aux mid, east aux mid, and the
# two slip mouths.
ANCHOR_CODE = """
local map = workspace:FindFirstChild("Map")
if not map then return "NOMAP" end
local aux = {}
local mouths = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = d.Name:match("^AuxLane_(%d+)$")
if ai then table.insert(aux, {i = tonumber(ai), p = d.Position}) end
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k and tonumber(k) <= 1 then mouths[s] = d.Position end
end
end
end
if #aux == 0 then return "NOAUX" end
table.sort(aux, function(a, b) return a.i < b.i end)
-- Split the aux segments into two clusters (west run and east run) by index gap.
local groups, cur = {}, {aux[1]}
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(groups, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(groups, cur)
local parts = {}
for _, g in ipairs(groups) do
local mid = g[math.max(1, math.floor(#g / 2))].p
table.insert(parts, ("%.1f %.1f %.1f"):format(mid.X, mid.Y, mid.Z))
end
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local m = mouths[s]
if m then table.insert(parts, ("%.1f %.1f %.1f"):format(m.X, m.Y, m.Z)) end
end
return table.concat(parts, " | ")
"""
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 "NOAUX" in txt:
print("ANCHOR FAIL: " + txt[:200])
return
# Pull the coordinate triples out of whatever wrapper the bridge returns.
import re
trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt)
if not trip:
print("ANCHOR PARSE FAIL: " + txt[:300])
return
pts = [tuple(float(v) for v in t) for t in trip]
print("anchors: " + str(pts))
made = []
for n, (x, y, z) in enumerate(pts[:2], start=1):
# Oblique from outside the deck, looking back at the merge.
d = grab(f"aux{n}_oblique", x + 260, y + 150, z + 260, x, y, z, fov=50)
if d:
made.append(d)
# Top-down: the merge taper reads clearest from directly above.
d = grab(f"aux{n}_topdown", x + 3, y + 430, z + 3, x, y, z, fov=42)
if d:
made.append(d)
# Driver's-eye down the lane.
d = grab(f"aux{n}_driver", x - 190, y + 16, z - 60, x + 120, y + 8, z + 40, fov=68)
if d:
made.append(d)
print("\nMADE " + str(len(made)) + " shots")
for m in made:
print(m)
if __name__ == "__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
#!/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()
+125
View File
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Capture the PA slip ramps from the user's own viewpoint, LIVE-ANCHORED.
The user's screenshot looks along the mainline from above the deck with the
ramp descending on the left. Reproduce that framing from the actual ramp
geometry so the shot cannot drift onto empty air after an edit.
"""
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"
# Anchors: for each slip -> mouth (first deck seg), mid-span, landing (last seg).
ANCHOR_CODE = """
local map = workspace:FindFirstChild("Map")
if not map then return "NOMAP" end
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
segs[s] = segs[s] or {}
table.insert(segs[s], {i = tonumber(k), p = d.Position})
end
end
end
end
local out = {}
for _, s in ipairs({"PAWestSlip", "PAEastSlip"}) do
local g = segs[s]
if g and #g > 2 then
table.sort(g, function(a, b) return a.i < b.i end)
local mouth = g[1].p
local mid = g[math.floor(#g / 2)].p
local land = g[#g].p
table.insert(out, ("%.1f %.1f %.1f"):format(mouth.X, mouth.Y, mouth.Z))
table.insert(out, ("%.1f %.1f %.1f"):format(mid.X, mid.Y, mid.Z))
table.insert(out, ("%.1f %.1f %.1f"):format(land.X, land.Y, land.Z))
end
end
if #out == 0 then return "NOSLIP" 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 "NOSLIP" in txt:
print("ANCHOR FAIL: " + txt[:200])
return
trip = re.findall(r"(-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)", txt)
if len(trip) < 6:
print("ANCHOR PARSE FAIL: " + txt[:300])
return
pts = [tuple(float(v) for v in t) for t in trip]
print("anchors: " + str(pts))
made = []
for n, base in enumerate((0, 3), start=1):
mouth, mid, land = pts[base], pts[base + 1], pts[base + 2]
tag = ["west", "east"][n - 1]
# 1. The user's view: high, behind the mouth, looking down the ramp.
made.append(grab(
f"ramp_{tag}_userview",
mouth[0] - 300, mouth[1] + 120, mouth[2] - 300,
land[0], land[1], land[2], fov=60))
# 2. Side elevation: shows the descent profile against the deck.
made.append(grab(
f"ramp_{tag}_side",
mid[0] + 420, mid[1] + 90, mid[2],
mid[0], mid[1], mid[2], fov=48))
# 3. Top-down over mid-span: is it parallel to the deck or cutting away?
made.append(grab(
f"ramp_{tag}_topdown",
mid[0], mid[1] + 560, mid[2],
mid[0], mid[1], mid[2], fov=55))
made = [m for m in made if m]
print("\nMADE " + str(len(made)) + " shots")
for m in made:
print(m)
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
-- The rebuild was interrupted mid-flight; its effect is UNKNOWN. Establish
-- ground truth before touching anything else.
-- (a) Did the build run? part count vs 16714 (user-edited) / 16820 (source).
-- (b) Is the aux lane back? user's edited workspace had 30 aux parts,
-- source produces 52 -- so they deleted ~22.
-- (c) Did the smoothing pass actually land on the east slip?
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "workspace.Map MISSING" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d (16714=user-edited, 16820=source rebuild)"):format(parts))
-- Aux lane inventory, with index range + position so I can tell WHICH lane.
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^AuxLane_(%d+)$")
if k then table.insert(aux, { i = tonumber(k), p = d.Position, inst = d }) end
end
end
table.sort(aux, function(a, b) return a.i < b.i end)
say(("AuxLane parts=%d (30=after user deletion, 52=source)"):format(#aux))
if #aux > 0 then
-- group into contiguous runs so each physical lane shows up separately
local runs, cur = {}, nil
for _, a in ipairs(aux) do
if cur and a.i - cur.last <= 2 then cur.last = a.i; cur.n += 1
else
if cur then table.insert(runs, cur) end
cur = { first = a.i, last = a.i, n = 1, p = a.p }
end
end
if cur then table.insert(runs, cur) end
for _, r in ipairs(runs) do
say((" aux run idx %d..%d n=%d at (%.0f,%.0f,%.0f)"):format(
r.first, r.last, r.n, r.p.X, r.p.Y, r.p.Z))
end
end
-- Slip smoothness: did the relaxation take effect?
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": ABSENT") continue end
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local d = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += d
if d > worst then worst = d; worstAt = segs[j].i end
if d > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
say(("%s: segs=%d total=%.1fdeg worst=%.1f@%d over5=%d level=%d"):format(
s, #segs, total, worst, worstAt, over, level))
end
return table.concat(out, "\n")
+55
View File
@@ -0,0 +1,55 @@
local HttpService = game:GetService("HttpService")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local map = workspace:FindFirstChild("Map")
if not map then return "no Map" end
-- Aggregate PA parts EXCLUDING the slips we just built, so we can see the lot.
local agg = { xMin = math.huge, xMax = -math.huge, zMin = math.huge, zMax = -math.huge }
local names = {}
local biggest, biggestArea = nil, 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:sub(1, 2) == "PA" and not d.Name:find("Slip") then
names[d.Name] = (names[d.Name] or 0) + 1
agg.xMin = math.min(agg.xMin, d.Position.X - d.Size.X / 2)
agg.xMax = math.max(agg.xMax, d.Position.X + d.Size.X / 2)
agg.zMin = math.min(agg.zMin, d.Position.Z - d.Size.Z / 2)
agg.zMax = math.max(agg.zMax, d.Position.Z + d.Size.Z / 2)
local area = d.Size.X * d.Size.Z
if area > biggestArea then
biggestArea = area
biggest = ("%s size=(%d,%d,%d) pos=(%d,%d,%d) yTop=%.1f"):format(
d.Name, d.Size.X, d.Size.Y, d.Size.Z,
d.Position.X, d.Position.Y, d.Position.Z,
d.Position.Y + d.Size.Y / 2)
end
end
end
local nameList = {}
for n, c in pairs(names) do
table.insert(nameList, n .. "x" .. c)
end
table.sort(nameList)
-- cap the list so the payload can't truncate
local head = {}
for i = 1, math.min(#nameList, 40) do head[i] = nameList[i] end
local s = Circuit.samples
local function probe(i)
local a, b = s[i], s[i + 1]
local d = (b.pos - a.pos)
local fwd = Vector3.new(d.X, 0, d.Z).Unit
local right = fwd:Cross(Vector3.yAxis)
return ("s%d pos=(%d,%d) fwd=(%.2f,%.2f) right=(%.2f,%.2f)"):format(
i, a.pos.X, a.pos.Z, fwd.X, fwd.Z, right.X, right.Z)
end
return HttpService:JSONEncode({
pa_nonslip_bounds = { x = { math.floor(agg.xMin), math.floor(agg.xMax) }, z = { math.floor(agg.zMin), math.floor(agg.zMax) } },
biggest_pa_part = biggest,
distinct_pa_names = #nameList,
sample_names = head,
pa_center = tostring(Circuit.PA_CENTER),
joins = { probe(525), probe(537), probe(551), probe(539) },
})
+134
View File
@@ -0,0 +1,134 @@
-- Two questions, measured rather than eyeballed:
-- (1) WHERE exactly is the mainline missing barriers? Walk the deck edge and
-- report contiguous runs with no Barrier/EdgeRail within reach.
-- (User deleted ~106 parts by hand, so gaps are wherever they cut.)
-- (2) HOW does the south on-ramp differ from the north off-ramp? Compare the
-- two slips feature-by-feature so "make it look like the other" is a
-- concrete list of deltas, not a guess.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
-- Collect barrier-ish parts into a coarse spatial hash for fast lookup.
local CELL = 60
local grid = {}
local function key(x, z) return math.floor(x / CELL) .. ":" .. math.floor(z / CELL) end
local barrierCount = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "EdgeRail") then
barrierCount += 1
local k = key(d.Position.X, d.Position.Z)
grid[k] = grid[k] or {}
table.insert(grid[k], d)
end
end
say(("indexed %d Barrier/EdgeRail parts"):format(barrierCount))
local function nearestBarrier(p)
local best = math.huge
local cx, cz = math.floor(p.X / CELL), math.floor(p.Z / CELL)
for dx = -1, 1 do
for dz = -1, 1 do
local c = grid[(cx + dx) .. ":" .. (cz + dz)]
if c then
for _, b in ipairs(c) do
local d2 = (Vector3.new(b.Position.X, 0, b.Position.Z) - Vector3.new(p.X, 0, p.Z)).Magnitude
-- only count barriers at roughly this height
if math.abs(b.Position.Y - p.Y) < 14 and d2 < best then best = d2 end
end
end
end
end
return best
end
-- Walk the mainline deck parts to build an ordered centreline.
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+)$")
or d.Name:match("^RoadDeck_(%d+)$")
if k then table.insert(deck, { i = tonumber(k), p = d.Position, cf = d.CFrame, sz = d.Size }) end
end
end
table.sort(deck, function(a, b) return a.i < b.i end)
say(("mainline deck parts found: %d"):format(#deck))
if #deck > 0 then
local UP = Vector3.new(0, 1, 0)
local runs = {}
local cur = nil
for j, s in ipairs(deck) do
local fwd = s.cf.LookVector
local right = fwd:Cross(UP).Unit
local half = s.sz.X / 2
-- probe just outboard of each edge
local gapL = nearestBarrier(s.p - right * (half + 2)) > 12
local gapR = nearestBarrier(s.p + right * (half + 2)) > 12
local bad = gapL or gapR
if bad then
local side = (gapL and gapR) and "BOTH" or (gapL and "L" or "R")
if cur and cur.side == side and s.i - cur.last <= 2 then
cur.last = s.i; cur.n += 1
else
if cur then table.insert(runs, cur) end
cur = { first = s.i, last = s.i, n = 1, side = side, pos = s.p }
end
else
if cur then table.insert(runs, cur); cur = nil end
end
end
if cur then table.insert(runs, cur) end
table.sort(runs, function(a, b) return a.n > b.n end)
say(("UNBARRIERED RUNS: %d total"):format(#runs))
for j = 1, math.min(#runs, 14) do
local r = runs[j]
say((" samples %d..%d (%d) side=%s at (%.0f,%.0f,%.0f)"):format(
r.first, r.last, r.n, r.side, r.pos.X, r.pos.Y, r.pos.Z))
end
end
-- (2) Compare the two slips.
say("--- SLIP COMPARISON ---")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs, walls, lines, other = {}, 0, 0, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^" .. s) then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
table.insert(segs, { i = tonumber(k), p = d.Position, sz = d.Size, cf = d.CFrame })
elseif d.Name:match("_Wall_") then walls += 1
elseif d.Name:match("[Ll]ine") then lines += 1
else
local nm = d.Name:gsub("^" .. s .. "_", ""):gsub("_%d+$", "")
other[nm] = (other[nm] or 0) + 1
end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
local ol = {}
for k, v in pairs(other) do table.insert(ol, k .. "=" .. v) end
table.sort(ol)
if #segs > 0 then
-- heading of first and last segment: which way does it run?
local h1 = segs[1].cf.LookVector
local hN = segs[#segs].cf.LookVector
-- curvature: total heading change
local turn = 0
for j = 2, #segs do
local a = segs[j - 1].cf.LookVector
local b = segs[j].cf.LookVector
turn += math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
end
say(("%s: segs=%d walls=%d lines=%d startY=%.1f endY=%.1f"):format(s, #segs, walls, lines, segs[1].p.Y, segs[#segs].p.Y))
say((" heading first=(%.2f,%.2f) last=(%.2f,%.2f) totalTurn=%.1fdeg"):format(h1.X, h1.Z, hN.X, hN.Z, turn))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
say(" other parts: " .. (#ol > 0 and table.concat(ol, " ") or "none"))
end
end
return table.concat(out, "\n")
+55
View File
@@ -0,0 +1,55 @@
-- The require sweep passing only proves the modules LOAD. What runs on Play is
-- MapBuilder actually building. Execute it for real and capture the error.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local serverScript = SS:FindFirstChild("Server")
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
say("MapBuilder module: " .. (mb and mb:GetFullName() or "NOT FOUND"))
if not mb then return table.concat(out, "\n") end
local ok, mod = pcall(require, mb)
say("require: " .. (ok and "ok" or ("FAIL " .. tostring(mod))))
if not ok then return table.concat(out, "\n") end
say("exports: " .. type(mod))
if type(mod) == "table" then
local keys = {}
for k, v in pairs(mod) do table.insert(keys, ("%s(%s)"):format(tostring(k), type(v))) end
table.sort(keys)
say(" " .. table.concat(keys, ", "))
end
-- Clear any existing map so we build from scratch, exactly like a fresh run.
local existing = workspace:FindFirstChild("Map")
say("existing Map in workspace: " .. (existing and "yes" or "no"))
local buildFn = type(mod) == "table" and (mod.Build or mod.build or mod.BuildMap)
if type(buildFn) ~= "function" then
say("NO BUILD FUNCTION EXPORTED -- cannot test build path")
return table.concat(out, "\n")
end
if existing then existing:Destroy() end
local t0 = os.clock()
local bok, berr = pcall(buildFn)
local dt = os.clock() - t0
say(("build pcall: %s (%.2fs)"):format(bok and "OK" or "FAIL", dt))
if not bok then
say("BUILD ERROR: " .. tostring(berr):sub(1, 600))
return table.concat(out, "\n")
end
local m = workspace:FindFirstChild("Map")
say("Map after build: " .. (m and "present" or "MISSING"))
if m then
local n = #m:GetDescendants()
say("descendants: " .. n)
if n == 0 then say("CONTROL FAIL: map built but empty") end
end
return table.concat(out, "\n")
+106
View File
@@ -0,0 +1,106 @@
-- Re-verify after un-pinning index 2, and settle the 4 "bare" samples:
-- 525L/526L/549L/550L sit at the ramp MOUTHS (west join=525, east join=551).
-- An open parapet at a mouth is correct -- that is the doorway cars drive
-- through, and the ramp deck's own walls take over. So check whether ramp deck
-- + ramp wall actually cover those samples. If yes: not a hole. If no: a hole.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder NOT FOUND" end
local fresh = mod:Clone()
fresh.Name = "MB_fresh_" .. tostring(math.random(1e6))
fresh.Parent = SS
local ok, MB = pcall(require, fresh)
if not ok then fresh:Destroy() return "require FAILED: " .. tostring(MB) end
local ok2, err = pcall(function() MB.build() end)
fresh:Destroy()
if not ok2 then return "build ERROR: " .. tostring(err) end
say("build OK")
local map = workspace:FindFirstChild("Map")
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
local deck, bars, slipAll = {}, {}, {}
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
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
if d.Name:match("^PA[EW][ae]stSlip") then table.insert(slipAll, d) end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local function nearest(list, p, ytol)
local best = math.huge
for _, b in ipairs(list) do
if math.abs(b.Position.Y - p.Y) < (ytol or 14) then
local dd = (flat(b.Position) - flat(p)).Magnitude
if dd < best then best = dd end
end
end
return best
end
say("--- edge audit 520..555 (L side) ---")
local realHoles = {}
for i = 520, 555 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local edge = d.Position - right * (d.Size.X / 2 + 2)
local bd = nearest(bars, edge)
local sd = nearest(slipAll, edge, 20)
local covered = bd < 12 or sd < 30
if not covered then table.insert(realHoles, i) end
if bd >= 12 then
say((" %d: barrier=%.1f slipStruct=%.1f -> %s"):format(
i, bd, sd, covered and "OK (ramp mouth)" or "HOLE"))
end
end
end
say(("REAL holes: %d %s"):format(#realHoles,
#realHoles > 0 and table.concat(realHoles, ",") or "(none)"))
say("--- smoothness ---")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += dth
if dth > worst then worst = dth; worstAt = segs[j].i end
if dth > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
say(("%s: total=%.1fdeg worst=%.1f@%d over5=%d level=%d"):format(
s, total, worst, worstAt, over, level))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
end
local aux = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^AuxLane_%d+$") then aux += 1 end
end
say(("AuxLane total=%d (want 30)"):format(aux))
return table.concat(out, "\n")
+123
View File
@@ -0,0 +1,123 @@
-- Studio caches required modules, which is why the previous rebuild silently
-- ran the OLD code (east slip still measured 216.0deg, unchanged). Force a
-- fresh load: clone the ModuleScript so require() sees a new instance and
-- recompiles from current source.
--
-- Verifies three things at once:
-- (1) smoothing landed -> east total heading change << 216deg, zero >5deg kinks
-- (2) aux lane stays deleted -> AuxLane run 527..548 ABSENT, total ~30 not 52
-- (3) barrier gap closed -> parapet present along 527..548
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder NOT FOUND" end
if not mod:IsA("ModuleScript") then return "MapBuilder is " .. mod.ClassName end
local fresh = mod:Clone()
fresh.Name = "MapBuilder_fresh_" .. tostring(math.random(1e6))
fresh.Parent = SS
local ok, MB = pcall(require, fresh)
if not ok then
fresh:Destroy()
return "require FAILED: " .. tostring(MB)
end
local t0 = os.clock()
local ok2, err = pcall(function() MB.build() end)
local dt = os.clock() - t0
fresh:Destroy()
if not ok2 then return "build ERROR: " .. tostring(err) end
say(("build OK (%.2fs) via fresh module"):format(dt))
local map = workspace:FindFirstChild("Map")
if not map then return "Map missing" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
-- (2) aux lane
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^AuxLane_(%d+)$")
if k then table.insert(aux, tonumber(k)) end
end
end
table.sort(aux)
local inRange = 0
for _, i in ipairs(aux) do if i >= 527 and i <= 548 then inRange += 1 end end
say(("AuxLane total=%d inside 527..548 = %d (want 0)"):format(#aux, inRange))
-- (3) barrier coverage across the formerly-bare run
local deck, bars = {}, {}
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
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local bare = {}
for i = 525, 550 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
local best = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - edge.Y) < 14 then
local dd = (flat(b.Position) - flat(edge)).Magnitude
if dd < best then best = dd end
end
end
if best > 12 then table.insert(bare, i .. (sgn < 0 and "L" or "R")) end
end
end
end
say(("bare edge samples in 525..550: %d %s"):format(#bare,
#bare > 0 and ("-> " .. table.concat(bare, ",")) or "(want 0)"))
-- (1) smoothness
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": ABSENT") continue end
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += dth
if dth > worst then worst = dth; worstAt = segs[j].i end
if dth > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
local lmin, lmax = math.huge, -math.huge
for j = 2, #segs do
local L = (segs[j].p - segs[j - 1].p).Magnitude
lmin = math.min(lmin, L); lmax = math.max(lmax, L)
end
say(("%s: total=%.1fdeg worst=%.1f@%d over5=%d level=%d spacing=%.1f..%.1f"):format(
s, total, worst, worstAt, over, level, lmin, lmax))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
end
return table.concat(out, "\n")
+135
View File
@@ -0,0 +1,135 @@
-- Rebuild with AUX_SUPPRESS widened to 525..550, then verify BOTH marked
-- defects are gone:
-- (A) left oval -> zero samples with no left barrier
-- (B) right circle -> no 26-stud lateral snap at the suppression boundary
-- The 6.3-stud steps at 512/513/563 are the aux lane's own smooth taper
-- (width 26 -> 19 -> 13 -> 7 -> 0) and are CORRECT; the parapet rides the
-- taper. Only steps much larger than that are real juts.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder NOT FOUND" end
local fresh = mod:Clone()
fresh.Name = "MB_fresh_" .. tostring(math.random(1e6))
fresh.Parent = SS
local ok, MB = pcall(require, fresh)
if not ok then fresh:Destroy() return "require FAILED: " .. tostring(MB) end
local ok2, err = pcall(function() MB.build() end)
fresh:Destroy()
if not ok2 then return "build ERROR: " .. tostring(err) end
say("build OK")
local map = workspace:FindFirstChild("Map")
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
local UP = Vector3.new(0, 1, 0)
local deck, aux, bars = {}, {}, {}
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
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
if d.Name == "Barrier" then table.insert(bars, d) end
end
end
say("sample | auxW | left barrier lat | step")
local prevLat, jogs, gaps = nil, {}, {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local c = d.Position
local best, bestLat = math.huge, nil
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - c.Y) < 14 then
local rel = b.Position - c
local along = math.abs(rel:Dot(d.CFrame.LookVector))
local lat = rel:Dot(right)
if along < 22 and lat < 0 and along < best then best = along; bestLat = lat end
end
end
local auxW = aux[i] and aux[i].Size.X or 0
if bestLat then
local step = prevLat and math.abs(math.abs(bestLat) - math.abs(prevLat)) or 0
if step > 10 then table.insert(jogs, ("%d(%.1f)"):format(i, step)) end
if step > 10 or i % 5 == 0 then
say((" %d | aux=%.0f | lat=%.1f | step=%.1f%s"):format(
i, auxW, bestLat, step, step > 10 and " <<< JOG" or ""))
end
prevLat = bestLat
else
table.insert(gaps, i)
say((" %d | aux=%.0f | NO LEFT BARRIER <<< GAP"):format(i, auxW))
prevLat = nil
end
end
end
say(("BIG JOGS (>10): %d %s"):format(#jogs, #jogs > 0 and table.concat(jogs, " ") or "(none)"))
say(("GAPS: %d %s"):format(#gaps, #gaps > 0 and table.concat(gaps, ",") or "(none)"))
-- full-loop unguarded sweep, same rule as before
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local allBars = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "EdgeRail") then
table.insert(allBars, d)
end
end
local slipParts = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("Slip") then table.insert(slipParts, d) end
end
local holes, swept = {}, 0
for i, d in pairs(deck) do
swept += 1
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
local bd = math.huge
for _, b in ipairs(allBars) do
if math.abs(b.Position.Y - edge.Y) < 14 then
local dd = (flat(b.Position) - flat(edge)).Magnitude
if dd < bd then bd = dd end
end
end
if bd >= 12 then
local ok2b = false
local a = aux[i]
if a then
local outer = a.Position + right * sgn * (a.Size.X / 2 + 2)
local bd2 = math.huge
for _, b in ipairs(allBars) do
if math.abs(b.Position.Y - outer.Y) < 14 then
local dd = (flat(b.Position) - flat(outer)).Magnitude
if dd < bd2 then bd2 = dd end
end
end
if bd2 < 14 then ok2b = true end
end
if not ok2b then
local sd = math.huge
for _, p in ipairs(slipParts) do
local dd = (flat(p.Position) - flat(edge)).Magnitude
if dd < sd then sd = dd end
end
if sd > 30 then table.insert(holes, i .. (sgn < 0 and "L" or "R")) end
end
end
end
end
say(("FULL SWEEP: %d samples, unguarded=%d %s"):format(swept, #holes,
#holes > 0 and table.concat(holes, ",") or "(none)"))
local auxN = 0
for _ in pairs(aux) do auxN += 1 end
say(("AuxLane total=%d (want 30)"):format(auxN))
return table.concat(out, "\n")
+97
View File
@@ -0,0 +1,97 @@
-- The 50-sample "unbarriered run" (513..562, LEFT) overlaps the aux-lane region
-- (510..565) exactly. In that stretch the parapet was deliberately RELOCATED
-- outboard, to the aux lane's outer edge -- so a 12-stud probe off the mainline
-- edge would report "missing" when the barrier is simply further out.
-- Do not claim a hole until this is disambiguated. Report ACTUAL distances.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local bars = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "EdgeRail"
or d.Name == "KillWall" or d.Name:match("Parapet")) then
table.insert(bars, d)
end
end
say(("barrier-ish parts: %d"):format(#bars))
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^AuxLane_(%d+)$") then
table.insert(aux, d)
end
end
say(("aux lane parts: %d"):format(#aux))
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+)$")
or d.Name:match("^RoadDeck_(%d+)$")
if k then deck[tonumber(k)] = d end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
-- For a probe point, distance to nearest barrier AND nearest aux part.
local function nearest(list, p, ymax)
local best, bi = math.huge, nil
for _, b in ipairs(list) do
if math.abs(b.Position.Y - p.Y) < (ymax or 16) then
local d2 = (flat(b.Position) - flat(p)).Magnitude
if d2 < best then best = d2; bi = b end
end
end
return best, bi
end
say("sample | edgeDist->barrier | auxHere | verdict")
local realGaps = {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
-- LEFT side probe (side = -right)
local edge = d.Position - right * (d.Size.X / 2 + 2)
local bd = nearest(bars, edge)
local ad = nearest(aux, edge)
local hasAux = ad < 20
-- A barrier is "present" if within 12 of the mainline edge, OR the aux
-- lane is here and a barrier sits within 12 of the aux OUTER edge.
local ok = bd < 12
if not ok and hasAux then
local auxEdge = d.Position - right * (d.Size.X / 2 + 26)
local bd2 = nearest(bars, auxEdge)
if bd2 < 14 then ok = true end
if i % 6 == 0 then
say((" %d | %.1f | aux=%s | auxEdgeBarrier=%.1f | %s"):format(
i, bd, tostring(hasAux), bd2, ok and "OK(relocated)" or "GAP"))
end
elseif i % 6 == 0 then
say((" %d | %.1f | aux=%s | %s"):format(i, bd, tostring(hasAux), ok and "OK" or "GAP"))
end
if not ok then table.insert(realGaps, i) end
end
end
say(("REAL gaps after accounting for relocated parapet: %d samples"):format(#realGaps))
if #realGaps > 0 then
local runs, cur = {}, nil
for _, i in ipairs(realGaps) do
if cur and i - cur.last <= 2 then cur.last = i
else if cur then table.insert(runs, cur) end cur = { first = i, last = i } end
end
if cur then table.insert(runs, cur) end
for _, r in ipairs(runs) do
local d = deck[r.first]
say((" GAP RUN %d..%d at (%.0f,%.0f,%.0f)"):format(
r.first, r.last, d.Position.X, d.Position.Y, d.Position.Z))
end
end
return table.concat(out, "\n")
+93
View File
@@ -0,0 +1,93 @@
-- The 10 flagged samples all read barrier=22.9 -- the exact signature of the
-- RELOCATED parapet (aux lane still exists at 510..524 and 551..565, and its
-- outer edge carries the rail). probe_final's audit did not re-probe the aux
-- outer edge, so it mislabelled them. Settle it: for each flagged sample,
-- measure distance from the AUX OUTER edge to the nearest barrier.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local deck, bars, aux = {}, {}, {}
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
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
end
end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local function nearest(p, ytol)
local best = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - p.Y) < (ytol or 14) then
local dd = (flat(b.Position) - flat(p)).Magnitude
if dd < best then best = dd end
end
end
return best
end
local flagged = { 520, 521, 522, 523, 524, 551, 552, 553, 554, 555 }
local trueHoles = {}
say("sample | auxHere | barrier@auxOuterEdge | verdict")
for _, i in ipairs(flagged) do
local d, a = deck[i], aux[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local verdict, dist
if a then
-- probe just outboard of the AUX lane, where the rail was moved to
local outer = a.Position - right * (a.Size.X / 2 + 2)
dist = nearest(outer)
verdict = dist < 14 and "OK (parapet relocated to aux edge)" or "HOLE"
else
local edge = d.Position - right * (d.Size.X / 2 + 2)
dist = nearest(edge)
verdict = dist < 12 and "OK" or "HOLE"
end
if verdict == "HOLE" then table.insert(trueHoles, i) end
say((" %d | aux=%s | %.1f | %s"):format(i, a and "yes" or "no", dist, verdict))
end
end
say(("TRUE UNGUARDED SAMPLES: %d %s"):format(#trueHoles,
#trueHoles > 0 and table.concat(trueHoles, ",") or "(none)"))
-- Full-loop sweep so nothing elsewhere is missed.
local swept, holes = 0, {}
for i, d in pairs(deck) do
swept += 1
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
if nearest(edge) >= 12 then
local a = aux[i]
local ok = false
if a then
local outer = a.Position + right * sgn * (a.Size.X / 2 + 2)
if nearest(outer) < 14 then ok = true end
end
if not ok then
-- ramp mouth? slip structure nearby counts as guarded
local sd = math.huge
for _, p in ipairs(map:GetDescendants()) do
if p:IsA("BasePart") and p.Name:match("Slip") then
local dd = (flat(p.Position) - flat(edge)).Magnitude
if dd < sd then sd = dd end
end
end
if sd > 30 then table.insert(holes, i .. (sgn < 0 and "L" or "R")) end
end
end
end
end
say(("FULL SWEEP: %d deck samples, unguarded=%d %s"):format(swept, #holes,
#holes > 0 and ("-> " .. table.concat(holes, ",")) or "(none)"))
return table.concat(out, "\n")
+107
View File
@@ -0,0 +1,107 @@
-- Slip walls measured CLEAN (0 missing, 0 deviation, 0.00 lateral step), so
-- neither marked defect is on the ramp decks themselves. Prime suspect is the
-- suppression boundary I introduced: across 527..548 the parapet is forced back
-- to the mainline edge, while 510..526 and 549..565 still carry it relocated
-- OUT on the aux lane's outer edge (~26 studs further). At samples 527 and 549
-- that is a ~26-stud lateral JOG -- a barrier that visibly sticks out and ends
-- in a stub. And the gore between mainline barrier and ramp wall start may be
-- open -- the missing-barrier oval.
--
-- Measure signed lateral offset of every Barrier from the mainline centreline,
-- walk 505..570, and report steps + gaps.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local deck, aux = {}, {}
local bars = {}
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
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
if d.Name == "Barrier" then table.insert(bars, d) end
end
end
-- For each mainline sample, find the barrier nearest to that sample's plane on
-- the LEFT side, and report its lateral offset from the centreline.
say("sample | auxW | barrier lateral offset from centreline | step")
local prevLat = nil
local jogs, gaps = {}, {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local c = d.Position
-- gather barriers within half a sample spacing along the road axis
local best, bestLat = math.huge, nil
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - c.Y) < 14 then
local rel = b.Position - c
local along = math.abs(rel:Dot(d.CFrame.LookVector))
local lat = rel:Dot(right)
if along < 22 and lat < 0 then -- LEFT side only
if along < best then best = along; bestLat = lat end
end
end
end
local auxW = aux[i] and aux[i].Size.X or 0
if bestLat then
local step = prevLat and math.abs(math.abs(bestLat) - math.abs(prevLat)) or 0
if step > 6 then
table.insert(jogs, ("%d(step %.1f)"):format(i, step))
end
if i % 4 == 0 or step > 6 then
say((" %d | aux=%.0f | lat=%.1f | step=%.1f%s"):format(
i, auxW, bestLat, step, step > 6 and " <<< JOG" or ""))
end
prevLat = bestLat
else
table.insert(gaps, i)
say((" %d | aux=%.0f | NO BARRIER FOUND on left <<< GAP"):format(i, auxW))
prevLat = nil
end
end
end
say(("JOGS (lateral step >6): %d %s"):format(#jogs, #jogs > 0 and table.concat(jogs, " ") or "(none)"))
say(("GAPS (no left barrier): %d %s"):format(#gaps, #gaps > 0 and table.concat(gaps, ",") or "(none)"))
-- Gore check: distance from each slip's FIRST wall to the nearest mainline
-- barrier. A large value means an open mouth corner -- the missing-barrier oval.
say("--- gore / mouth junction ---")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local first, firstK = nil, math.huge
local last, lastK = nil, -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local wk = d.Name:match("^" .. s .. "_Wall_(%d+)$")
if wk then
local k = tonumber(wk)
if k < firstK then firstK = k; first = d end
if k > lastK then lastK = k; last = d end
end
end
end
if first then
local bd = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - first.Position.Y) < 16 then
local dd = (flat(b.Position) - flat(first.Position)).Magnitude
if dd < bd then bd = dd end
end
end
say(("%s: first wall seg %d at (%.0f,%.0f,%.0f) -> nearest mainline barrier %.1f"):format(
s, firstK, first.Position.X, first.Position.Y, first.Position.Z, bd))
say((" last wall seg %d at (%.0f,%.0f,%.0f)"):format(
lastK, last.Position.X, last.Position.Y, last.Position.Z))
end
end
return table.concat(out, "\n")
+73
View File
@@ -0,0 +1,73 @@
-- PAEastSlip turns 216deg vs PAWestSlip's 56.5deg -- nearly 4x. That is the
-- "looks terrible" the user means. Find WHERE the turn concentrates: is it a
-- smooth long curve, or a few violent kinks? Per-segment heading deltas tell us.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame, sz = d.Size }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": absent") continue end
local deltas = {}
for j = 2, #segs do
local a = segs[j - 1].cf.LookVector
local b = segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
table.insert(deltas, { i = segs[j].i, d = dth, p = segs[j].p })
end
-- worst kinks
local sorted = {}
for _, v in ipairs(deltas) do table.insert(sorted, v) end
table.sort(sorted, function(x, y) return x.d > y.d end)
local sum = 0
for _, v in ipairs(deltas) do sum += v.d end
say(("%s: total=%.1fdeg mean=%.2fdeg/seg n=%d"):format(s, sum, sum / #deltas, #deltas))
local worst = {}
for j = 1, math.min(8, #sorted) do
table.insert(worst, ("seg%d=%.1f"):format(sorted[j].i, sorted[j].d))
end
say(" worst kinks: " .. table.concat(worst, " "))
-- how many segments exceed a smooth-curve threshold?
local over = 0
for _, v in ipairs(deltas) do if v.d > 5 then over += 1 end end
say((" segments turning >5deg: %d"):format(over))
-- segment length consistency (uneven lengths also read as ugly)
local lmin, lmax, lsum = math.huge, -math.huge, 0
for j = 2, #segs do
local L = (segs[j].p - segs[j - 1].p).Magnitude
lmin = math.min(lmin, L); lmax = math.max(lmax, L); lsum += L
end
say((" seg spacing: min=%.1f max=%.1f mean=%.1f totalRun=%.0f"):format(
lmin, lmax, lsum / (#segs - 1), lsum))
-- vertical profile: where does it drop?
local topY = segs[1].p.Y
local level = 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
local steep = 0
for j = 2, #segs do
local dy = math.abs(segs[j].p.Y - segs[j - 1].p.Y)
local dxz = (Vector3.new(segs[j].p.X, 0, segs[j].p.Z) - Vector3.new(segs[j - 1].p.X, 0, segs[j - 1].p.Z)).Magnitude
if dxz > 0.1 and (dy / dxz) > 0.12 then steep += 1 end
end
say((" level-run=%d segments steeper than 12%%: %d"):format(level, steep))
end
return table.concat(out, "\n")
+61
View File
@@ -0,0 +1,61 @@
-- East slip kinks hard at segs 6,7,11,12 (27.8/17.5/58.2/50.1 deg). The slip
-- follows the mainline via centreAt(f), so either (a) the mainline bends
-- viciously at those samples, or (b) the slip's own path math misbehaves.
-- East runs join=551 -> land=539 (backwards, span=-12); west runs 525 -> 537.
-- Sample the mainline heading across BOTH spans to tell which.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" 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+)$")
or d.Name:match("^RoadDeck_(%d+)$")
if k then table.insert(deck, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(deck, function(a, b) return a.i < b.i end)
local byIdx = {}
for _, d in ipairs(deck) do byIdx[d.i] = d end
say(("mainline parts=%d idx %d..%d"):format(#deck, deck[1].i, deck[#deck].i))
local function report(lo, hi, label)
say(("--- mainline heading, samples %d..%d (%s) ---"):format(lo, hi, label))
local prev, acc = nil, 0
local worst, worstAt = 0, -1
for i = lo, hi do
local d = byIdx[i]
if d then
local h = d.cf.LookVector
if prev then
local dth = math.deg(math.acos(math.clamp(prev:Dot(h), -1, 1)))
acc += dth
if dth > worst then worst = dth; worstAt = i end
if dth > 4 then
say((" sample %d: turn %.1fdeg at (%.0f,%.0f,%.0f)"):format(i, dth, d.p.X, d.p.Y, d.p.Z))
end
end
prev = h
end
end
say((" total=%.1fdeg worst=%.1fdeg@%d"):format(acc, worst, worstAt))
end
report(525, 537, "WEST slip span - smooth one")
report(539, 551, "EAST slip span - ugly one")
-- Also: does the east span cross the sample-wrap boundary or reverse direction?
say("--- endpoint geometry ---")
for _, pair in ipairs({ { 525, 537, "west" }, { 551, 539, "east" } }) do
local a, b = byIdx[pair[1]], byIdx[pair[2]]
if a and b then
local straight = (Vector3.new(a.p.X, 0, a.p.Z) - Vector3.new(b.p.X, 0, b.p.Z)).Magnitude
say(("%s: join@%d=(%.0f,%.0f) land@%d=(%.0f,%.0f) straightDist=%.0f"):format(
pair[3], pair[1], a.p.X, a.p.Z, pair[2], b.p.X, b.p.Z, straight))
end
end
return table.concat(out, "\n")
+103
View File
@@ -0,0 +1,103 @@
-- READ-ONLY inventory of the Daikoku Futo PA area so the capture camera can be
-- anchored to real geometry instead of guessed coordinates.
--
-- Wanted:
-- * what top-level containers/models exist for the PA
-- * every distinct part-name PREFIX in the PA region, with counts
-- * the PA's world-space bounding box + centre (for framing)
-- * key landmarks: slip ramps, apron/parking deck, any building/structure
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
-- 1. Top-level structure of Map
say("=== Map children ===")
for _, c in ipairs(map:GetChildren()) do
local n = 0
for _, d in ipairs(c:GetDescendants()) do
if d:IsA("BasePart") then n += 1 end
end
say((" %s (%s) parts=%d"):format(c.Name, c.ClassName, n))
end
-- 2. Anything whose name mentions PA / Daikoku / parking
say("=== named PA-ish objects (models/folders) ===")
local seen = 0
for _, d in ipairs(map:GetDescendants()) do
if not d:IsA("BasePart") then
local nm = d.Name
if nm:match("PA") or nm:lower():match("daikoku") or nm:lower():match("park")
or nm:lower():match("apron") or nm:lower():match("rest") then
local n = 0
for _, x in ipairs(d:GetDescendants()) do
if x:IsA("BasePart") then n += 1 end
end
say((" %s (%s) parts=%d parent=%s"):format(d.Name, d.ClassName, n, d.Parent.Name))
seen += 1
if seen > 40 then say(" ...(truncated)") break end
end
end
end
if seen == 0 then say(" (none found by name)") end
-- 3. Locate the PA by its slip ramps: the apron is where the ramps land.
local land = {}
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local best, bestK = nil, -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k and tonumber(k) > bestK then bestK = tonumber(k); best = d end
end
end
if best then
land[s] = best.Position
say(("%s landing: (%.0f, %.0f, %.0f)"):format(s, best.Position.X, best.Position.Y, best.Position.Z))
end
end
local anchor
if land.PAWestSlip and land.PAEastSlip then
anchor = (land.PAWestSlip + land.PAEastSlip) / 2
elseif land.PAWestSlip then
anchor = land.PAWestSlip
elseif land.PAEastSlip then
anchor = land.PAEastSlip
end
if not anchor then return table.concat(out, "\n") .. "\nNO SLIP LANDINGS -> cannot anchor" end
say(("PA anchor (between landings): (%.0f, %.0f, %.0f)"):format(anchor.X, anchor.Y, anchor.Z))
-- 4. Every part within 1400 studs of the anchor, grouped by name prefix.
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local counts, mins, maxs = {}, {}, {}
local lo = Vector3.new(math.huge, math.huge, math.huge)
local hi = Vector3.new(-math.huge, -math.huge, -math.huge)
local total = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
if (flat(d.Position) - flat(anchor)).Magnitude < 1400 then
total += 1
-- prefix = name with trailing _<digits> stripped
local pre = d.Name:gsub("_%d+$", "")
counts[pre] = (counts[pre] or 0) + 1
lo = Vector3.new(math.min(lo.X, d.Position.X), math.min(lo.Y, d.Position.Y), math.min(lo.Z, d.Position.Z))
hi = Vector3.new(math.max(hi.X, d.Position.X), math.max(hi.Y, d.Position.Y), math.max(hi.Z, d.Position.Z))
end
end
end
say(("=== %d parts within 1400 studs of PA anchor ==="):format(total))
local keys = {}
for k in pairs(counts) do table.insert(keys, k) end
table.sort(keys, function(a, b) return counts[a] > counts[b] end)
for idx, k in ipairs(keys) do
if idx > 30 then say((" ...and %d more prefixes"):format(#keys - 30)) break end
say((" %-28s %d"):format(k, counts[k]))
end
say(("bbox: (%.0f,%.0f,%.0f) .. (%.0f,%.0f,%.0f)"):format(lo.X, lo.Y, lo.Z, hi.X, hi.Y, hi.Z))
say(("bbox size: %.0f x %.0f x %.0f"):format(hi.X - lo.X, hi.Y - lo.Y, hi.Z - lo.Z))
local ctr = (lo + hi) / 2
say(("bbox centre: (%.0f, %.0f, %.0f)"):format(ctr.X, ctr.Y, ctr.Z))
return table.concat(out, "\n")
+36
View File
@@ -0,0 +1,36 @@
-- Minimal PA locator. The full inventory probe returned an empty error, so
-- start small and confirm the basics work before adding anything back.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
say("=== Map children ===")
for _, c in ipairs(map:GetChildren()) do
local n = 0
for _, d in ipairs(c:GetDescendants()) do
if d:IsA("BasePart") then n += 1 end
end
say((" %s (%s) parts=%d"):format(c.Name, c.ClassName, n))
end
local land = {}
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local best, bestK = nil, -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k and tonumber(k) > bestK then
bestK = tonumber(k)
best = d
end
end
end
if best then
land[s] = best.Position
say(("%s landing: (%.0f, %.0f, %.0f)"):format(s, best.Position.X, best.Position.Y, best.Position.Z))
end
end
return table.concat(out, "\n")
+80
View File
@@ -0,0 +1,80 @@
-- The crux of the user's request: does the ramp run PARALLEL to the highway
-- (travelling far along it while climbing), or does it dive off the side?
-- Measures longitudinal progress (in mainline samples) vs vertical drop vs
-- lateral drift, plus the actual grade. Also finds what the ramp passes over.
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local samples = Circuit.samples
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local out = {}
local function say(s) table.insert(out, s) end
local ramps = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local name, idx = d.Name:match("^(PA%a+Slip)_Deck_(%d+)$")
if name then
ramps[name] = ramps[name] or {}
table.insert(ramps[name], { i = tonumber(idx), p = d })
end
end
end
local function nearestSample(p)
local bd, bi = math.huge, -1
for i = 1, #samples do
local s = samples[i]
local dd = (Vector3.new(p.X, 0, p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
if dd < bd then bd, bi = dd, i end
end
return bi, bd
end
for _, name in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = ramps[name]
if not segs then say(name .. ": MISSING") continue end
table.sort(segs, function(x, y) return x.i < y.i end)
local first, last = segs[1].p, segs[#segs].p
local si = nearestSample(first.Position)
local ei = nearestSample(last.Position)
-- 3D path length along the ramp centreline.
local plen = 0
for k = 2, #segs do
plen += (segs[k].p.Position - segs[k - 1].p.Position).Magnitude
end
local drop = first.Position.Y - last.Position.Y
local flat = (Vector3.new(first.Position.X, 0, first.Position.Z)
- Vector3.new(last.Position.X, 0, last.Position.Z)).Magnitude
-- Sample spacing, to convert sample counts into studs.
local spacing = (samples[2].pos - samples[1].pos).Magnitude
local dsamples = math.abs(ei - si)
say(("=== %s ==="):format(name))
say((" segments: %d, seg length ~%.0f studs, path length %.0f studs")
:format(#segs, plen / math.max(1, #segs - 1), plen))
say((" mouth at sample %d -> end near sample %d (travels %d samples = %.0f studs ALONG the highway)")
:format(si, ei, dsamples, dsamples * spacing))
say((" vertical drop: %.1f studs horizontal run: %.0f studs grade: %.1f%%")
:format(drop, flat, 100 * drop / math.max(1, flat)))
say((" PARALLEL-ness: %.0f studs along-highway per %.0f studs of drop -> %s")
:format(dsamples * spacing, drop,
(dsamples * spacing) > drop * 4 and "runs alongside" or "DIVES OFF THE SIDE"))
end
-- What is underneath the ramp mid-span? (is it over water/air, or over the lot?)
local names = {}
for _, d in ipairs(ws:GetDescendants()) do
if d:IsA("BasePart") then
names[d.Name:gsub("_%d+$", "")] = true
end
end
local cats = {}
for k in pairs(names) do table.insert(cats, k) end
table.sort(cats)
say("part categories in world: " .. #cats)
say(" " .. table.concat(cats, ", "):sub(1, 900))
return table.concat(out, "\n")
+40
View File
@@ -0,0 +1,40 @@
-- Which Studio is the bridge actually driving, and can that place be PLAYED?
local out = {}
local function say(s) table.insert(out, s) end
say("place name: " .. tostring(game.Name))
say("PlaceId: " .. tostring(game.PlaceId))
say("GameId: " .. tostring(game.GameId))
local RS = game:GetService("RunService")
say("IsEdit: " .. tostring(RS:IsEdit()) .. " IsRunning: " .. tostring(RS:IsRunning()) ..
" IsClient: " .. tostring(RS:IsClient()) .. " IsServer: " .. tostring(RS:IsServer()))
-- Is the map present, and is there anything to actually spawn onto?
local m = workspace:FindFirstChild("Map")
say("workspace.Map: " .. (m and (#m:GetDescendants() .. " descendants") or "MISSING"))
say("SpawnLocations: ")
local spawns = 0
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("SpawnLocation") then
spawns += 1
say((" %s at %s"):format(d:GetFullName(), tostring(d.Position)))
end
end
if spawns == 0 then say(" NONE -- a playtest has nowhere to spawn the character") end
-- StarterPlayer/StarterGui presence (a play session needs these to not error)
for _, svc in ipairs({ "StarterPlayer", "StarterGui", "StarterPack", "Players", "Lighting" }) do
local ok, s = pcall(function() return game:GetService(svc) end)
say(("service %s: %s"):format(svc, ok and "ok" or "FAIL"))
end
-- Rojo connection state: is the synced source actually in this place?
local SS = game:GetService("ServerScriptService")
local srv = SS:FindFirstChild("Server")
say("ServerScriptService.Server: " .. (srv and srv.ClassName or "MISSING"))
if srv then
local mb = srv:FindFirstChild("MapBuilder")
say(" MapBuilder present: " .. tostring(mb ~= nil))
end
return table.concat(out, "\n")
+104
View File
@@ -0,0 +1,104 @@
-- What do the slip ramps ACTUALLY do? Elevation + lateral profile per segment,
-- and do they intersect the mainline deck?
-- The user says: "on and off ramps go to the bottom and through the highway".
-- Measure that claim directly rather than assuming.
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local samples = Circuit.samples
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local out = {}
local function say(s) table.insert(out, s) end
-- Collect ramp deck segments in build order.
local ramps = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local name, idx = d.Name:match("^(PA%a+Slip)_Deck_(%d+)$")
if name then
ramps[name] = ramps[name] or {}
table.insert(ramps[name], { i = tonumber(idx), p = d })
end
end
end
-- Nearest mainline sample + signed lateral offset for a world point.
local function nearestSample(p)
local bd, bi = math.huge, -1
for i = 1, #samples do
local s = samples[i]
local dd = (Vector3.new(p.X, 0, p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
if dd < bd then bd, bi = dd, i end
end
return bi, bd
end
local function lateralAt(i, p)
local n = #samples
local a = samples[((i - 1) % n) + 1]
local b = samples[(i % n) + 1]
local f = b.pos - a.pos
f = Vector3.new(f.X, 0, f.Z).Unit
local r = Vector3.new(-f.Z, 0, f.X)
local rel = Vector3.new(p.X - a.pos.X, 0, p.Z - a.pos.Z)
return rel:Dot(r), a.pos.Y
end
for _, name in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = ramps[name]
if not segs then
say(name .. ": MISSING")
else
table.sort(segs, function(x, y) return x.i < y.i end)
say(("=== %s (%d segs) ==="):format(name, #segs))
say("seg | rampY | deckY | dY(ramp-deck) | lateral | note")
local under = 0
for k, s in ipairs(segs) do
if k == 1 or k % 6 == 0 or k == #segs then
local si = nearestSample(s.p.Position)
local lat, deckY = lateralAt(si, s.p.Position)
local dy = s.p.Position.Y - deckY
local note = ""
-- |lateral| < 30 means it is horizontally UNDER/INSIDE the deck slab.
if math.abs(lat) < 30 then
note = (math.abs(dy) < 8) and "*** THROUGH DECK ***" or "under/over deck footprint"
end
say(("%3d | %6.1f | %6.1f | %+7.1f | %+7.1f | %s")
:format(s.i, s.p.Position.Y, deckY, dy, lat, note))
end
local si = nearestSample(s.p.Position)
local lat = lateralAt(si, s.p.Position)
if math.abs(lat) < 30 then under += 1 end
end
say(("%s: %d/%d segments sit within the deck footprint (|lateral| < 30)")
:format(name, under, #segs))
end
end
-- Does any ramp part actually overlap a Road part in 3D?
local roads = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^Road_%d+$") then
table.insert(roads, d)
end
end
local clashes, tested = {}, 0
for _, name in ipairs({ "PAWestSlip", "PAEastSlip" }) do
for _, s in ipairs(ramps[name] or {}) do
for _, r in ipairs(roads) do
tested += 1
local d = s.p.Position - r.Position
local sx = (s.p.Size.X + r.Size.X) / 2
local sy = (s.p.Size.Y + r.Size.Y) / 2
local sz = (s.p.Size.Z + r.Size.Z) / 2
if math.abs(d.X) < sx and math.abs(d.Y) < sy and math.abs(d.Z) < sz then
table.insert(clashes, ("%s x %s (dY=%.1f)"):format(s.p.Name, r.Name, d.Y))
end
end
end
end
say(("ramp-vs-road AABB tests: %d, overlaps: %d"):format(tested, #clashes))
for k = 1, math.min(#clashes, 12) do say(" " .. clashes[k]) end
if tested == 0 then say("CONTROL FAIL: nothing tested") end
return table.concat(out, "\n")
+62
View File
@@ -0,0 +1,62 @@
-- Rebuild the map from current source and report what actually landed.
-- The workspace snapshot predates today's edits; this regenerates it.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
if not mb then return "MapBuilder NOT FOUND" end
local okReq, M = pcall(require, mb)
if not okReq then return "require failed: " .. tostring(M) end
local old = workspace:FindFirstChild("Map")
if old then
old:Destroy()
say("destroyed stale Map")
end
local t0 = os.clock()
local okB, err = pcall(function() return M.build() end)
say(("build: %s (%.2fs)"):format(okB and "OK" or ("FAILED -> " .. tostring(err)), os.clock() - t0))
if not okB then return table.concat(out, "\n") end
local map = workspace:FindFirstChild("Map")
if not map then return table.concat(out, "\n") .. "\nMap MISSING after build" end
local parts, slip, aux = 0, {}, 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
parts += 1
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
slip[s] = slip[s] or {}
table.insert(slip[s], { i = tonumber(k), y = d.Position.Y })
end
end
if d.Name:match("^AuxLane_%d+$") then aux += 1 end
end
end
say(("parts=%d auxLaneSegs=%d"):format(parts, aux))
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local g = slip[s]
if not g or #g == 0 then
say(s .. ": ABSENT")
else
table.sort(g, function(a, b) return a.i < b.i end)
local topY, level = g[1].y, 0
for _, seg in ipairs(g) do
if math.abs(seg.y - topY) < 0.5 then level += 1 else break end
end
say(("%s: segs=%d startY=%.1f endY=%.1f level-run=%d/%d %s"):format(
s, #g, topY, g[#g].y, level, #g,
level >= 12 and "<- PARALLEL ASCENT PRESENT" or "<- old diving profile"))
end
end
return table.concat(out, "\n")
+151
View File
@@ -0,0 +1,151 @@
-- READ-ONLY REGRESSION CHECK. Does NOT build. Inspects whatever is currently
-- in Studio and compares against every property we had verified as good, so we
-- can tell whether the last edits preserved the good version or damaged it.
--
-- Known-good baseline (measured earlier this session):
-- ramp PAEastSlip total bend 30.7deg, worst 1.3, over5=0, level run 20
-- ramp PAWestSlip total bend 34.4deg, worst 1.4, over5=0, level run 20
-- AuxLane parts = 30 (user's hand-edited count, NOT the source's 52)
-- unguarded deck edges = 0 across all 823 samples
-- parts ~= 16788
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING -- nothing built" end
local UP = Vector3.new(0, 1, 0)
local function flat(v) return Vector3.new(v.X, 0, v.Z) end
local parts = 0
local deck, aux, bars, slipParts = {}, {}, {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
parts += 1
local k = d.Name:match("^Road_(%d+)$") or d.Name:match("^Deck_(%d+)$")
if k then deck[tonumber(k)] = d end
local a = d.Name:match("^AuxLane_(%d+)$")
if a then aux[tonumber(a)] = d end
if d.Name == "Barrier" or d.Name == "EdgeRail" then table.insert(bars, d) end
if d.Name:match("Slip") then table.insert(slipParts, d) end
end
end
say(("parts = %d (baseline ~16788)"):format(parts))
-- 1. RAMP SMOOTHNESS -- the thing the user cares most about right now
local rampOK = true
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then
say(s .. ": ABSENT <<< RAMP GONE")
rampOK = false
else
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local dth = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += dth
if dth > worst then worst = dth; worstAt = segs[j].i end
if dth > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
if over > 0 then rampOK = false end
say(("%s: segs=%d total=%.1fdeg worst=%.1f@%d over5=%d level=%d %s"):format(
s, #segs, total, worst, worstAt, over, level,
over == 0 and "OK" or "<<< KINKED"))
end
end
say(rampOK and ">> RAMPS STILL SMOOTH" or ">> RAMP REGRESSION")
-- 2. USER'S DELETED LANE -- must stay deleted
local auxN, inRange = 0, 0
for i in pairs(aux) do
auxN += 1
if i >= 525 and i <= 550 then inRange += 1 end
end
say(("AuxLane = %d (baseline 30) | inside deleted run 525..550 = %d (want 0) %s"):format(
auxN, inRange, (auxN == 30 and inRange == 0) and "OK" or "<<< REGRESSION"))
-- 3. BARRIER COVERAGE -- full loop
local holes = {}
local swept = 0
for i, d in pairs(deck) do
swept += 1
local right = d.CFrame.LookVector:Cross(UP).Unit
for _, sgn in ipairs({ -1, 1 }) do
local edge = d.Position + right * sgn * (d.Size.X / 2 + 2)
local bd = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - edge.Y) < 14 then
local dd = (flat(b.Position) - flat(edge)).Magnitude
if dd < bd then bd = dd end
end
end
if bd >= 12 then
local okb = false
local a = aux[i]
if a then
local outer = a.Position + right * sgn * (a.Size.X / 2 + 2)
local bd2 = math.huge
for _, b in ipairs(bars) do
if math.abs(b.Position.Y - outer.Y) < 14 then
local dd = (flat(b.Position) - flat(outer)).Magnitude
if dd < bd2 then bd2 = dd end
end
end
if bd2 < 14 then okb = true end
end
if not okb then
local sd = math.huge
for _, p in ipairs(slipParts) do
local dd = (flat(p.Position) - flat(edge)).Magnitude
if dd < sd then sd = dd end
end
if sd > 30 then table.insert(holes, i .. (sgn < 0 and "L" or "R")) end
end
end
end
end
say(("barrier sweep: %d samples, unguarded=%d %s"):format(swept, #holes,
#holes == 0 and "OK" or ("<<< " .. table.concat(holes, ","))))
-- 4. FENCE JOG at the suppression boundary
local prevLat, jogs = nil, {}
for i = 505, 570 do
local d = deck[i]
if d then
local right = d.CFrame.LookVector:Cross(UP).Unit
local best, bestLat = math.huge, nil
for _, b in ipairs(bars) do
if b.Name == "Barrier" and math.abs(b.Position.Y - d.Position.Y) < 14 then
local rel = b.Position - d.Position
local along = math.abs(rel:Dot(d.CFrame.LookVector))
local lat = rel:Dot(right)
if along < 22 and lat < 0 and along < best then best = along; bestLat = lat end
end
end
if bestLat then
if prevLat then
local step = math.abs(math.abs(bestLat) - math.abs(prevLat))
if step > 10 then table.insert(jogs, ("%d(%.1f)"):format(i, step)) end
end
prevLat = bestLat
else
prevLat = nil
end
end
end
say(("fence jogs >10 studs: %d %s"):format(#jogs,
#jogs == 0 and "OK" or ("<<< " .. table.concat(jogs, " "))))
return table.concat(out, "\n")
+74
View File
@@ -0,0 +1,74 @@
-- "Changes were reverted" -- WHERE? Distinguish three separate states that can
-- each independently be stale:
-- (a) the MapBuilder SOURCE inside Studio (Rojo sync state)
-- (b) the BUILT geometry sitting in workspace (may predate the source)
-- (c) the place file itself
local out = {}
local function say(s) table.insert(out, s) end
say("place: " .. tostring(game.Name))
-- (a) SOURCE state in Studio
local SS = game:GetService("ServerScriptService")
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
if not mb then
say("SOURCE: MapBuilder NOT FOUND in ServerScriptService")
else
local ok, src = pcall(function() return mb.Source end)
if ok and type(src) == "string" then
say(("SOURCE: len=%d PARALLEL_RUN=%s PA_DEC_SPAN=%s PA_SLIP=%s"):format(
#src,
tostring(src:find("PARALLEL_RUN") ~= nil),
tostring(src:find("PA_DEC_SPAN") ~= nil),
tostring(src:find("local PA_SLIP") ~= nil)))
else
say("SOURCE: unreadable " .. tostring(src))
end
end
-- (b) BUILT geometry actually in the workspace right now
local map = workspace:FindFirstChild("Map")
if not map then
say("BUILT: workspace.Map MISSING")
return table.concat(out, "\n")
end
local slipSegs, auxSegs = {}, 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
slipSegs[s] = slipSegs[s] or {}
table.insert(slipSegs[s], { i = tonumber(k), p = d.Position })
end
end
if d.Name:match("^AuxLane_%d+$") then auxSegs += 1 end
end
end
say("BUILT: aux lane segments = " .. auxSegs .. " (expect ~52 with decel+accel)")
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local g = slipSegs[s]
if not g or #g == 0 then
say(("BUILT: %s -- NO DECK SEGMENTS (ramp absent)"):format(s))
else
table.sort(g, function(a, b) return a.i < b.i end)
-- How many segments hold deck height before the descent starts?
local topY = g[1].p.Y
local level = 0
for _, seg in ipairs(g) do
if math.abs(seg.p.Y - topY) < 0.5 then level += 1 else break end
end
local drop = topY - g[#g].p.Y
say(("BUILT: %s segs=%d startY=%.1f endY=%.1f drop=%.1f level-run=%d/%d %s"):format(
s, #g, topY, g[#g].p.Y, drop, level, #g,
level >= 12 and "<- PARALLEL FIX PRESENT" or "<- OLD DIVING PROFILE"))
end
end
return table.concat(out, "\n")
+61
View File
@@ -0,0 +1,61 @@
-- Does the game actually RUN? Exercise the real server startup path and report
-- the first genuine error, rather than guessing from static analysis.
local RS = game:GetService("ReplicatedStorage")
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
-- 1. Are the Rojo-synced trees actually present in the DataModel?
local shared = RS:FindFirstChild("Shared")
say("ReplicatedStorage.Shared: " .. (shared and "present" or "MISSING"))
if shared then
local names = {}
for _, c in ipairs(shared:GetChildren()) do table.insert(names, c.Name) end
table.sort(names)
say(" modules: " .. table.concat(names, ", "))
end
local server = SS:FindFirstChild("Server") or SS:FindFirstChild("TS")
say("ServerScriptService children:")
for _, c in ipairs(SS:GetChildren()) do
say((" %s (%s)"):format(c.Name, c.ClassName))
end
-- 2. Try requiring every shared + server module. Report the FIRST failure with
-- its message -- this is what actually stops a playtest.
local function tryRequire(inst)
local ok, err = pcall(function() return require(inst) end)
return ok, err
end
say("--- require sweep ---")
local failures = 0
local tested = 0
for _, root in ipairs({ shared, server }) do
if root then
for _, m in ipairs(root:GetDescendants()) do
if m:IsA("ModuleScript") then
tested += 1
local ok, err = tryRequire(m)
if not ok then
failures += 1
say(("FAIL %s: %s"):format(m:GetFullName(), tostring(err):sub(1, 300)))
end
end
end
end
end
say(("required %d modules, %d failed"):format(tested, failures))
if tested == 0 then say("CONTROL FAIL: nothing was required -- wrong scope") end
-- 3. Script instances that would run on play, and whether any are disabled.
say("--- scripts ---")
for _, d in ipairs(game:GetDescendants()) do
if (d:IsA("Script") or d:IsA("LocalScript")) and not d:IsDescendantOf(workspace) then
if d.Name:match("init") or d:IsDescendantOf(SS) then
say((" %s (%s) disabled=%s"):format(d:GetFullName(), d.ClassName, tostring(d.Disabled)))
end
end
end
return table.concat(out, "\n")
+88
View File
@@ -0,0 +1,88 @@
-- Which side of the deck does each slip + aux lane sit on?
-- Answers: are both existing slips on the SAME carriageway (so "the opposite"
-- means the other carriageway), or is one an entry and one an exit?
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local samples = Circuit.samples
local out = {}
local function say(s) table.insert(out, s) end
-- Signed lateral offset of a world point from the mainline at sample i.
-- Positive = one side, negative = the other. Uses the deck's own right vector.
local function sideOf(i, p)
local n = #samples
local a = samples[((i - 1) % n) + 1]
local b = samples[(i % n) + 1]
local fwd = (b.pos - a.pos)
fwd = Vector3.new(fwd.X, 0, fwd.Z).Unit
local right = Vector3.new(-fwd.Z, 0, fwd.X)
local rel = Vector3.new(p.X - a.pos.X, 0, p.Z - a.pos.Z)
return rel:Dot(right)
end
local SLIPS = { { "PAWestSlip", 525, 537 }, { "PAEastSlip", 551, 539 } }
for _, s in ipairs(SLIPS) do
local name, join, land = s[1], s[2], s[3]
local first, last, nseg = nil, nil, 0
local maxI = -1
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = tonumber(d.Name:match("^" .. name .. "_Deck_(%d+)$") or "")
if k then
nseg += 1
if k <= 1 then first = d end
if k > maxI then maxI, last = k, d end
end
end
end
if not first then
say(name .. ": FAIL no deck parts")
else
local lat = sideOf(join, first.Position)
say(("%s: segs=%d mouth lateral %+.1f (%s side) mouthY=%.1f landY=%.1f -> %s")
:format(name, nseg, lat, lat > 0 and "RIGHT" or "LEFT",
first.Position.Y, last and last.Position.Y or -1,
(last and last.Position.Y < first.Position.Y) and "DESCENDS (exit/off-ramp)"
or "CLIMBS (entry/on-ramp)"))
end
end
-- Aux lanes: which side, grouped by contiguous sample index.
local aux = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = tonumber(d.Name:match("^AuxLane_(%d+)$") or "")
if ai then table.insert(aux, { i = ai, p = d.Position }) end
end
end
table.sort(aux, function(x, y) return x.i < y.i end)
if #aux == 0 then
say("AUX: FAIL none built")
else
local groups, cur = {}, { aux[1] }
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(groups, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(groups, cur)
for gi, g in ipairs(groups) do
local mid = g[math.max(1, math.floor(#g / 2))]
local lat = sideOf(mid.i, mid.p)
say(("AUX group %d: samples %d..%d (%d segs) lateral %+.1f (%s side)")
:format(gi, g[1].i, g[#g].i, #g, lat, lat > 0 and "RIGHT" or "LEFT"))
end
end
-- Does the deck carry traffic both ways, and is there a median?
local med = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("^Median") then med += 1 end
end
say(("median parts: %d (deck is one slab split into two carriageways)"):format(med))
return table.concat(out, "\n")
+109
View File
@@ -0,0 +1,109 @@
-- Measure the envelope a SIMPLE straight slip-ramp must live in.
-- User's drawing: short stubs peeling off the mainline at a shallow angle,
-- running roughly PARALLEL to the highway in the strip between the mainline
-- edge and the PA lot's north edge, descending an S onto the apron.
--
-- We need to know, in real numbers:
-- 1. Where the mainline actually runs across the PA frontage (samples,
-- heading, deck Y) -- is it straight enough for a parallel slip?
-- 2. Where the lot's north edge is, and how wide the clear strip is
-- between the mainline's south edge and that lot edge.
-- 3. Deck Y vs apron Y (the height the S-curve must lose).
-- 4. What is already sitting in that strip that a ramp would hit.
local RS = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local samples = Circuit.samples
local n = #samples
local c = Circuit.PA_CENTER
-- Nearest mainline sample to the PA centre.
local bestIdx, bestD = 1, math.huge
for i = 1, n do
local p = samples[i].pos
local d = (Vector3.new(p.X, 0, p.Z) - Vector3.new(c.X, 0, c.Z)).Magnitude
if d < bestD then bestD, bestIdx = d, i end
end
-- Walk a band around the PA frontage and report heading + deck Y, so we can
-- see how straight the mainline is and where a parallel slip could sit.
local band = {}
for k = -40, 40, 5 do
local i = ((bestIdx - 1 + k) % n) + 1
local s = samples[i].pos
local nx = samples[(i % n) + 1].pos
local hdg = math.deg(math.atan2(nx.Z - s.Z, nx.X - s.X))
local d = (Vector3.new(s.X, 0, s.Z) - Vector3.new(c.X, 0, c.Z)).Magnitude
table.insert(band, {
k = k, idx = i,
x = math.floor(s.X), y = math.floor(s.Y + 0.5), z = math.floor(s.Z),
hdg = math.floor(hdg + 0.5),
distPA = math.floor(d),
})
end
-- The apron / lot floor: bounds and surface height.
local map = workspace:FindFirstChild("Map")
local floor = map and map:FindFirstChild("PAFloor", true)
local lot = nil
if floor then
lot = {
name = floor.Name,
surfaceY = floor.Position.Y + floor.Size.Y / 2,
x_min = math.floor(floor.Position.X - floor.Size.X / 2),
x_max = math.floor(floor.Position.X + floor.Size.X / 2),
z_min = math.floor(floor.Position.Z - floor.Size.Z / 2),
z_max = math.floor(floor.Position.Z + floor.Size.Z / 2),
}
end
-- What already occupies the strip between the mainline and the lot's north
-- edge? Anything here is a collision candidate for a parallel slip ramp.
-- Strip = north third of the lot bbox, i.e. Z above (z_max - 220).
local obstacles = {}
if lot then
local stripZ = lot.z_max - 220
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("BasePart") then
local p = d.Position
if p.Z >= stripZ and p.Z <= lot.z_max + 160
and p.X >= lot.x_min - 60 and p.X <= lot.x_max + 60 then
local key = d.Name
local e = obstacles[key]
if not e then
e = { n = 0, x_min = 1e9, x_max = -1e9, z_min = 1e9,
z_max = -1e9, y_max = -1e9 }
obstacles[key] = e
end
e.n += 1
e.x_min = math.min(e.x_min, p.X); e.x_max = math.max(e.x_max, p.X)
e.z_min = math.min(e.z_min, p.Z); e.z_max = math.max(e.z_max, p.Z)
e.y_max = math.max(e.y_max, p.Y + d.Size.Y / 2)
end
end
end
end
local obsOut = {}
for k, e in pairs(obstacles) do
table.insert(obsOut, {
name = k, n = e.n,
x = math.floor(e.x_min) .. ".." .. math.floor(e.x_max),
z = math.floor(e.z_min) .. ".." .. math.floor(e.z_max),
ytop = math.floor(e.y_max),
})
end
table.sort(obsOut, function(a, b) return a.n > b.n end)
while #obsOut > 25 do table.remove(obsOut) end
return HttpService:JSONEncode({
bestIdx = bestIdx,
nearestDist = math.floor(bestD),
pa_center = { math.floor(c.X), math.floor(c.Y), math.floor(c.Z) },
pa_radius = Circuit.PA_RADIUS,
band = band,
lot = lot,
strip_obstacles = obsOut,
})
+69
View File
@@ -0,0 +1,69 @@
-- Rebuild with the smoothing pass, then re-measure the SAME metrics as before
-- so the before/after is directly comparable. Target: east total heading change
-- down from 216deg toward the west's ~56deg, and ZERO segments turning >5deg
-- (the 58.2/50.1deg hinges must be gone). Also verify the parallel run and
-- the endpoints did not move -- smoothing must not detach the mouth.
local SS = game:GetService("ServerScriptService")
local out = {}
local function say(s) table.insert(out, s) end
local mod = SS:FindFirstChild("MapBuilder", true)
if not mod then return "MapBuilder module NOT FOUND" end
local ok, MB = pcall(require, mod)
if not ok then return "require failed: " .. tostring(MB) end
local t0 = os.clock()
local ok2, err = pcall(function() MB.build() end)
if not ok2 then return "build ERROR: " .. tostring(err) end
say(("build OK (%.2fs)"):format(os.clock() - t0))
local map = workspace:FindFirstChild("Map")
if not map then return "Map missing after build" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("parts=%d"):format(parts))
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local segs = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then table.insert(segs, { i = tonumber(k), p = d.Position, cf = d.CFrame }) end
end
end
table.sort(segs, function(a, b) return a.i < b.i end)
if #segs == 0 then say(s .. ": ABSENT") continue end
local total, worst, worstAt, over = 0, 0, -1, 0
for j = 2, #segs do
local a, b = segs[j - 1].cf.LookVector, segs[j].cf.LookVector
local d = math.deg(math.acos(math.clamp(a:Dot(b), -1, 1)))
total += d
if d > worst then worst = d; worstAt = segs[j].i end
if d > 5 then over += 1 end
end
local topY, level = segs[1].p.Y, 0
for _, sg in ipairs(segs) do
if math.abs(sg.p.Y - topY) < 0.5 then level += 1 else break end
end
local lmin, lmax = math.huge, -math.huge
for j = 2, #segs do
local L = (segs[j].p - segs[j - 1].p).Magnitude
lmin = math.min(lmin, L); lmax = math.max(lmax, L)
end
say(("%s: segs=%d total=%.1fdeg mean=%.2f worst=%.1f@%d over5=%d"):format(
s, #segs, total, total / (#segs - 1), worst, worstAt, over))
say((" level-run=%d startY=%.1f endY=%.1f spacing=%.1f..%.1f"):format(
level, segs[1].p.Y, segs[#segs].p.Y, lmin, lmax))
say((" start=(%.0f,%.0f,%.0f) end=(%.0f,%.0f,%.0f)"):format(
segs[1].p.X, segs[1].p.Y, segs[1].p.Z, segs[#segs].p.X, segs[#segs].p.Y, segs[#segs].p.Z))
end
return table.concat(out, "\n")
+56
View File
@@ -0,0 +1,56 @@
-- What is actually on screen right now? Don't assume "missing" means empty --
-- distinguish (a) no Map at all, (b) Map present but stripped, (c) Map fine.
local out = {}
local function say(s) table.insert(out, s) end
say("place: " .. tostring(game.Name))
local map = workspace:FindFirstChild("Map")
if not map then
say("workspace.Map: MISSING ENTIRELY")
else
local parts, total = 0, 0
for _, d in ipairs(map:GetDescendants()) do
total += 1
if d:IsA("BasePart") then parts += 1 end
end
say(("workspace.Map: present descendants=%d parts=%d"):format(total, parts))
local kids = {}
for _, c in ipairs(map:GetChildren()) do
local n = 0
for _, _d in ipairs(c:GetDescendants()) do n += 1 end
table.insert(kids, ("%s(%d)"):format(c.Name, n))
end
say("Map children: " .. (#kids > 0 and table.concat(kids, ", ") or "NONE"))
end
-- Is anything else sitting loose in workspace?
local loose = {}
for _, c in ipairs(workspace:GetChildren()) do
if c:IsA("BasePart") or c:IsA("Model") or c:IsA("Folder") then
table.insert(loose, c.Name)
end
if #loose > 15 then break end
end
say("workspace top-level: " .. table.concat(loose, ", "))
-- Where is the camera looking? If the map exists but the camera is parked in
-- empty space, "gone from my screen" means a camera problem, not a build one.
local cam = workspace.CurrentCamera
if cam then
say(("camera pos=(%.0f, %.0f, %.0f)"):format(cam.CFrame.Position.X, cam.CFrame.Position.Y, cam.CFrame.Position.Z))
end
-- Spawn / reference point so we know where the map SHOULD be.
local ref
for _, d in ipairs(workspace:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("Deck") then ref = d break end
end
if ref then
say(("sample deck part '%s' at (%.0f, %.0f, %.0f)"):format(ref.Name, ref.Position.X, ref.Position.Y, ref.Position.Z))
else
say("no Deck part found anywhere in workspace")
end
return table.concat(out, "\n")
+39
View File
@@ -0,0 +1,39 @@
-- Is Studio actually CONNECTED to the Rojo server, or is it showing a stale
-- snapshot? A place can contain MapBuilder from an old sync and still be
-- disconnected, which is how you end up testing yesterday's geometry.
local out = {}
local function say(s) table.insert(out, s) end
-- Rojo's plugin leaves session state in the DataModel; look for its markers.
local found = {}
for _, d in ipairs(game:GetDescendants()) do
local n = d.Name
if n:match("Rojo") or n:match("ROJO") then
table.insert(found, ("%s (%s)"):format(d:GetFullName(), d.ClassName))
end
if #found > 12 then break end
end
say("Rojo-named instances: " .. (#found > 0 and table.concat(found, ", ") or "NONE FOUND"))
-- Does the in-place MapBuilder source contain the fix I just made? This is the
-- real test of whether the running place is current.
local SS = game:GetService("ServerScriptService")
local mb
for _, d in ipairs(SS:GetDescendants()) do
if d:IsA("ModuleScript") and d.Name == "MapBuilder" then mb = d end
end
if mb then
local ok, src = pcall(function() return mb.Source end)
if ok and type(src) == "string" then
say("MapBuilder source length: " .. #src)
say("has PARALLEL_RUN (today's ramp fix): " .. tostring(src:find("PARALLEL_RUN") ~= nil))
say("has PA_AUX_DECEL (decel lanes): " .. tostring(src:find("PA_AUX_DECEL") ~= nil))
else
say("could not read MapBuilder.Source (permission): " .. tostring(src))
end
else
say("MapBuilder NOT FOUND")
end
say("place: " .. tostring(game.Name))
return table.concat(out, "\n")
+78
View File
@@ -0,0 +1,78 @@
-- The user hand-edited ramps in Studio. Their work exists ONLY in the live
-- workspace, not in MapBuilder source. Before touching anything, measure what
-- is actually there now and how it differs from what source would generate.
local out = {}
local function say(s) table.insert(out, s) end
say("place: " .. tostring(game.Name))
local map = workspace:FindFirstChild("Map")
if not map then return "workspace.Map MISSING" end
local parts = 0
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then parts += 1 end
end
say(("Map parts=%d (source rebuild produced 16820)"):format(parts))
-- Slip deck geometry as it stands RIGHT NOW.
local slip = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local k = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if k then
slip[s] = slip[s] or {}
table.insert(slip[s], {
i = tonumber(k), pos = d.Position, size = d.Size,
cf = d.CFrame, inst = d,
})
end
end
end
end
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local g = slip[s]
if not g or #g == 0 then
say(s .. ": ABSENT")
else
table.sort(g, function(a, b) return a.i < b.i end)
local topY, level = g[1].pos.Y, 0
for _, seg in ipairs(g) do
if math.abs(seg.pos.Y - topY) < 0.5 then level += 1 else break end
end
-- widths and gaps tell us if the user reshaped it
local wmin, wmax = math.huge, -math.huge
local maxGap, gapAt = 0, -1
for j, seg in ipairs(g) do
wmin = math.min(wmin, seg.size.X); wmax = math.max(wmax, seg.size.X)
if j > 1 then
local d2 = (seg.pos - g[j - 1].pos).Magnitude
if d2 > maxGap then maxGap = d2; gapAt = seg.i end
end
end
say(("%s: segs=%d idx[%d..%d] startY=%.1f endY=%.1f level=%d width=%.1f..%.1f maxGap=%.1f@%d"):format(
s, #g, g[1].i, g[#g].i, topY, g[#g].pos.Y, level, wmin, wmax, maxGap, gapAt))
end
end
-- What barrier-ish parts exist, and on what? Name survey first so I know the
-- real naming scheme before I go hunting for "missing" ones.
local kinds = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local n = d.Name:gsub("_%d+$", "_#")
if n:lower():match("barrier") or n:lower():match("parapet") or n:lower():match("rail")
or n:lower():match("wall") or n:lower():match("guard") or n:lower():match("kerb")
or n:lower():match("curb") or n:lower():match("fence") then
kinds[n] = (kinds[n] or 0) + 1
end
end
end
local list = {}
for k, v in pairs(kinds) do table.insert(list, ("%s=%d"):format(k, v)) end
table.sort(list)
say("BARRIER-LIKE PART NAMES: " .. (#list > 0 and table.concat(list, " ") or "NONE"))
return table.concat(out, "\n")
+120
View File
@@ -0,0 +1,120 @@
-- User's picture marks TWO defects on the slip ramps:
-- (A) left tall oval -> a stretch of ramp edge with NO barrier at all
-- (B) right circle -> the OUTER barrier "sticks out" / juts, not smooth
-- Earlier measurement hinted at (A): PAEastSlip had walls=119 where west had
-- 120, i.e. one wall segment short. Find the truth for both slips now.
--
-- Method:
-- 1. Index every <slip>_Wall_<k> by k and by side (left/right of centreline).
-- Missing k on a side = a hole in that parapet.
-- 2. For each wall, compute SIGNED lateral offset from its own deck segment
-- centre. Walls are built at +/-(WIDTH/2 - 0.6); any outlier is a jut.
-- 3. Measure end-to-end gaps between consecutive walls on the same side --
-- a protruding corner shows as a large lateral step between neighbours.
local out = {}
local function say(s) table.insert(out, s) end
local map = workspace:FindFirstChild("Map")
if not map then return "Map MISSING" end
local UP = Vector3.new(0, 1, 0)
for _, s in ipairs({ "PAWestSlip", "PAEastSlip" }) do
local decks, walls = {}, {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local dk = d.Name:match("^" .. s .. "_Deck_(%d+)$")
if dk then decks[tonumber(dk)] = d end
local wk = d.Name:match("^" .. s .. "_Wall_(%d+)$")
if wk then
local k = tonumber(wk)
walls[k] = walls[k] or {}
table.insert(walls[k], d)
end
end
end
local idx = {}
for k in pairs(decks) do table.insert(idx, k) end
table.sort(idx)
if #idx == 0 then say(s .. ": no decks") continue end
say(("=== %s: decks=%d (idx %d..%d) ==="):format(s, #idx, idx[1], idx[#idx]))
-- (1) missing walls
local missing, oneSided = {}, {}
for _, k in ipairs(idx) do
local w = walls[k]
if not w then
table.insert(missing, k)
elseif #w < 2 then
table.insert(oneSided, k)
end
end
say((" walls: missing-both=%d %s | only-one-side=%d %s"):format(
#missing, #missing > 0 and table.concat(missing, ",") or "-",
#oneSided, #oneSided > 0 and table.concat(oneSided, ",") or "-"))
-- (2) signed lateral offset of each wall from its deck centre
local offs = {}
local worstOut, worstAt, worstSide = 0, -1, ""
for _, k in ipairs(idx) do
local dk = decks[k]
local w = walls[k]
if dk and w then
local right = dk.CFrame.LookVector:Cross(UP).Unit
for _, wp in ipairs(w) do
local rel = wp.Position - dk.Position
local lat = rel:Dot(right)
table.insert(offs, { k = k, lat = lat, side = lat < 0 and "L" or "R" })
end
end
end
-- expected magnitude = median |lat|
local mags = {}
for _, o in ipairs(offs) do table.insert(mags, math.abs(o.lat)) end
table.sort(mags)
local med = mags[math.max(1, math.floor(#mags / 2))] or 0
say((" median |lateral offset| = %.2f (walls sit this far off centre)"):format(med))
local juts = {}
for _, o in ipairs(offs) do
local dev = math.abs(o.lat) - med
if dev > 1.5 then
table.insert(juts, ("%d%s+%.1f"):format(o.k, o.side, dev))
if dev > worstOut then worstOut = dev; worstAt = o.k; worstSide = o.side end
end
end
say((" walls deviating >1.5 from median: %d %s"):format(#juts,
#juts > 0 and table.concat(juts, " ") or "(none)"))
if worstAt > 0 then
say((" worst jut: seg %d side %s, +%.1f studs outboard"):format(worstAt, worstSide, worstOut))
end
-- (3) lateral step between consecutive walls on the SAME side
local bySide = { L = {}, R = {} }
for _, o in ipairs(offs) do table.insert(bySide[o.side], o) end
for _, sd in ipairs({ "L", "R" }) do
local a = bySide[sd]
table.sort(a, function(x, y) return x.k < y.k end)
local bigStep, stepAt = 0, -1
for j = 2, #a do
local st = math.abs(math.abs(a[j].lat) - math.abs(a[j - 1].lat))
if st > bigStep then bigStep = st; stepAt = a[j].k end
end
say((" side %s: n=%d largest lateral step=%.2f at seg %d"):format(sd, #a, bigStep, stepAt))
end
-- world position of any missing/one-sided spot so I can match it to the pic
for _, k in ipairs(oneSided) do
local dk = decks[k]
if dk then
local w = walls[k][1]
local right = dk.CFrame.LookVector:Cross(UP).Unit
local sd = (w.Position - dk.Position):Dot(right) < 0 and "L" or "R"
say((" >> seg %d has ONLY side %s, at (%.0f,%.0f,%.0f)"):format(
k, sd, dk.Position.X, dk.Position.Y, dk.Position.Z))
end
end
end
return table.concat(out, "\n")
+123
View File
@@ -0,0 +1,123 @@
-- Is the fused aux run genuine continuous pavement (a weaving section), or
-- overlapping/duplicated parts? Checks per-sample coverage, duplicate indices,
-- and the width profile across both mouths.
local ws = game:GetService("Workspace")
local map = ws:FindFirstChild("Map")
if not map then return "NOMAP" end
local seen, dupes = {}, 0
local byIdx = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local i = tonumber(d.Name:match("^AuxLane_(%d+)$") or "")
if i then
if seen[i] then dupes += 1 end
seen[i] = true
byIdx[i] = d
end
end
end
local out = {}
local function say(s) table.insert(out, s) end
say(("duplicate AuxLane indices: %d -> %s"):format(dupes, dupes == 0 and "PASS" or "FAIL"))
-- Walk 505..570 and report which samples have lane and how wide.
local missing = {}
local prof = {}
for i = 505, 570 do
local p = byIdx[i]
if p then
table.insert(prof, ("%d:%.0f"):format(i, p.Size.X))
else
table.insert(missing, i)
table.insert(prof, ("%d:--"):format(i))
end
end
say("width profile (sample:width, -- = no lane):")
say(" " .. table.concat(prof, " "))
say("gaps in 505..570: " .. (#missing > 0 and table.concat(missing, ",") or "none"))
-- The only gaps should be the ramp noses (the ramp deck covers those).
-- Derive coverage from the ACTUAL ramp deck parts, but test the point where
-- the AUX LANE would sit (deck edge + half lane width) -- NOT the mainline
-- centreline. Testing proximity to the centreline marked 26 samples as "nose"
-- because the ramp spirals past them at other heights, which would mask any
-- real hole and make this check vacuously pass. The nose test must also match
-- in HEIGHT: a ramp 50 studs below the deck does not cover the deck's edge.
local samples = require(game:GetService("ReplicatedStorage")
:WaitForChild("Shared"):WaitForChild("Circuit")).samples
local decks = {}
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") and d.Name:match("Slip_Deck_") then
table.insert(decks, d)
end
end
local HALF, AUXW, SIDE = 30, 26, -1
local function auxPointAt(i)
local n = #samples
local a = samples[((i - 1) % n) + 1]
local b = samples[(i % n) + 1]
local f = b.pos - a.pos
f = Vector3.new(f.X, 0, f.Z).Unit
local r = Vector3.new(-f.Z, 0, f.X)
return a.pos + r * (SIDE * (HALF + AUXW / 2)), a.pos.Y
end
local NOSE, noseWhy = {}, {}
for _, i in ipairs(missing) do
local p, deckY = auxPointAt(i)
for _, d in ipairs(decks) do
local flat = (Vector3.new(d.Position.X, 0, d.Position.Z)
- Vector3.new(p.X, 0, p.Z)).Magnitude
local dy = math.abs(d.Position.Y - deckY)
-- Must overlap the lane footprint horizontally AND be at deck level.
if flat <= AUXW and dy <= 6 then
NOSE[i] = true
noseWhy[i] = ("%s flat=%.0f dy=%.0f"):format(d.Name, flat, dy)
break
end
end
end
local noseList = {}
for i in pairs(NOSE) do table.insert(noseList, i) end
table.sort(noseList)
say(("ramp-nose-covered gaps (%d): %s"):format(#noseList, table.concat(noseList, ",")))
for _, i in ipairs(noseList) do
say((" %d covered by %s"):format(i, noseWhy[i]))
end
local lo, hi = math.huge, -1
for k in pairs(byIdx) do
lo = math.min(lo, k); hi = math.max(hi, k)
end
local bad = {}
for _, i in ipairs(missing) do
-- Samples outside the lane's own extent are not holes; only flag gaps that
-- sit strictly between the first and last aux segment.
if i > lo and i < hi and not NOSE[i] then table.insert(bad, i) end
end
say(("lane run extent: %d..%d"):format(lo, hi))
say(("non-nose gaps inside the lane run: %s -> %s"):format(#bad > 0 and table.concat(bad, ",") or "none",
#bad == 0 and "PASS (only ramp noses uncovered)" or "FAIL (hole in pavement)"))
-- Between the two mouths, is the lane continuously full width (a real weaving
-- section) rather than tapering out and back in?
local weaveMin = math.huge
local weaveTested = 0
for i = 528, 548 do
local p = byIdx[i]
if p then
weaveTested += 1
weaveMin = math.min(weaveMin, p.Size.X)
end
end
if weaveTested == 0 then
say("weave section: FAIL nothing tested")
else
say(("weave section 528..548: %d segs, min width %.1f -> %s")
:format(weaveTested, weaveMin,
weaveMin >= 24 and "PASS (continuous full-width weaving lane)" or "TAPERS"))
end
return table.concat(out, "\n")
+194
View File
@@ -0,0 +1,194 @@
-- Verify the speed-change (auxiliary) lanes and that the slips no longer cut
-- across mainline traffic.
--
-- Rebuilds from a CLONED module first (require() cache bypass), then measures.
-- Every check below can FAIL -- and asserts it actually tested something, so a
-- lookup returning nothing reports FAIL rather than vacuously passing.
local sss = game:GetService("ServerScriptService")
local ss = game:GetService("ServerStorage")
local ws = game:GetService("Workspace")
local RS = game:GetService("ReplicatedStorage")
local realMod = sss:FindFirstChild("MapBuilder", true)
if not realMod then return "MapBuilder not found" end
local bust = ss:FindFirstChild("_cachebust")
if not bust then
bust = Instance.new("Folder")
bust.Name = "_cachebust"
bust.Parent = ss
end
for _, ch in ipairs(bust:GetChildren()) do
ch:Destroy()
end
local clone = realMod:Clone()
clone.Name = "MapBuilder_aux_" .. tostring(math.random(1e6, 9e6))
clone.Parent = bust
local mb = require(clone)
local old = ws:FindFirstChild("Map")
if old then old:Destroy() end
local ok, err = pcall(function() mb.build() end)
if not ok then return "BUILD ERROR: " .. tostring(err) end
local map = ws:FindFirstChild("Map")
if not map then return "no Map after build" end
local Circuit = require(RS:WaitForChild("Shared"):WaitForChild("Circuit"))
local out = {}
local function say(s) table.insert(out, s) end
-- Collect the aux lane decks and the slip decks.
local aux = {}
local decks = { PAWestSlip = {}, PAEastSlip = {} }
for _, d in ipairs(map:GetDescendants()) do
if d:IsA("BasePart") then
local ai = d.Name:match("^AuxLane_(%d+)$")
if ai then
table.insert(aux, { i = tonumber(ai), p = d.Position, s = d.Size, cf = d.CFrame })
end
for slipName, list in pairs(decks) do
local idx = d.Name:match("^" .. slipName .. "_Deck_(%d+)$")
if idx then
table.insert(list, { i = tonumber(idx), p = d.Position, s = d.Size, cf = d.CFrame })
end
end
end
end
-- 1. The aux lanes must exist at all.
if #aux == 0 then
say("FAIL: no AuxLane_* parts were built")
return table.concat(out, "\n")
end
say(("aux lane segments built: %d"):format(#aux))
-- 2. Every aux segment must sit OUTSIDE the mainline deck, not on top of it.
-- Measure the gap between the mainline centreline and the aux centreline,
-- and compare against that sample's own half-width.
local samples = Circuit.samples
local minClear, minAt = math.huge, -1
local tested = 0
for _, a in ipairs(aux) do
local s = samples[a.i]
if s then
tested += 1
local d = (Vector3.new(a.p.X, 0, a.p.Z) - Vector3.new(s.pos.X, 0, s.pos.Z)).Magnitude
-- inner edge of aux lane relative to mainline centre
local innerEdge = d - a.s.X / 2
if innerEdge < minClear then
minClear, minAt = innerEdge, a.i
end
end
end
if tested == 0 then
say("FAIL: no aux segment matched a circuit sample (harness looked up nothing)")
else
say(("aux inner edge vs mainline centre: min %.1f studs at sample %d (tested %d)")
:format(minClear, minAt, tested))
end
-- 3. The slip decks must START at the aux lane offset, not on the centreline.
-- This is the actual bug the user reported: lat began at 0.
for slipName, list in pairs(decks) do
table.sort(list, function(x, y) return x.i < y.i end)
if #list < 50 then
say(("%s FAIL: only %d deck segments"):format(slipName, #list))
else
local first = list[1]
-- Find the nearest mainline sample to the slip's first segment.
local bestD, bestI = math.huge, -1
for i = 1, #samples do
local d = (Vector3.new(first.p.X, 0, first.p.Z) - Vector3.new(samples[i].pos.X, 0, samples[i].pos.Z)).Magnitude
if d < bestD then bestD, bestI = d, i end
end
local halfW = 0
do
-- Deck half-width at that sample, read off the built road itself.
local road = map:FindFirstChild("Road_" .. bestI, true)
halfW = road and road.Size.X / 2 or 0
end
local innerEdge = bestD - first.s.X / 2
local verdict = (halfW > 0 and innerEdge >= halfW - 1.5) and "PASS" or "FAIL"
say(("%s start: centre %.1f from mainline centre, inner edge %.1f, deck half-width %.1f -> %s")
:format(slipName, bestD, innerEdge, halfW, verdict))
end
end
-- 4. Continuity: the slip mouth and the nearest aux segment must be adjacent
-- (end-to-end), not overlapping and not separated by a hole.
for slipName, list in pairs(decks) do
if #list >= 50 then
local first = list[1]
local bestGap, bestI = math.huge, -1
for _, a in ipairs(aux) do
local g = (a.p - first.p).Magnitude
if g < bestGap then bestGap, bestI = g, a.i end
end
local verdict = bestGap <= 90 and "PASS" or "FAIL"
say(("%s mouth -> nearest aux segment: %.1f studs (sample %d) -> %s")
:format(slipName, bestGap, bestI, verdict))
end
end
-- 5. The aux lane must be level with the mainline deck at the mouth.
local maxStep, stepAt = 0, -1
for _, a in ipairs(aux) do
local road = map:FindFirstChild("Road_" .. a.i, true)
if road then
local step = math.abs((a.p.Y + a.s.Y / 2) - (road.Position.Y + road.Size.Y / 2))
if step > maxStep then maxStep, stepAt = step, a.i end
end
end
say(("aux/mainline surface step: max %.2f studs at sample %d -> %s")
:format(maxStep, stepAt, maxStep <= 0.05 and "PASS" or "FAIL"))
-- 6. The taper must actually taper: widths should fall to near zero at the end.
local wmin, wmax = math.huge, 0
for _, a in ipairs(aux) do
wmin = math.min(wmin, a.s.X)
wmax = math.max(wmax, a.s.X)
end
say(("aux width range: %.1f .. %.1f studs -> %s")
:format(wmin, wmax, (wmax >= 25 and wmin <= 8) and "PASS (full lane + taper)" or "CHECK"))
-- 7. DECELERATION LANES. Each mouth must now have lane on BOTH sides: one
-- upstream (decel, for traffic leaving) and one downstream (accel, for
-- traffic joining). Group the aux segments into runs and check each slip
-- mouth has a run on either side of it.
table.sort(aux, function(x, y) return x.i < y.i end)
local runs, cur = {}, { aux[1] }
for k = 2, #aux do
if aux[k].i - aux[k - 1].i > 3 then
table.insert(runs, cur); cur = {}
end
table.insert(cur, aux[k])
end
table.insert(runs, cur)
say(("aux runs: %d (expect 4 = 2 mouths x {decel, accel})"):format(#runs))
for ri, r in ipairs(runs) do
local wlo, whi = math.huge, 0
for _, a in ipairs(r) do
wlo = math.min(wlo, a.s.X); whi = math.max(whi, a.s.X)
end
say((" run %d: samples %d..%d (%d segs) width %.1f..%.1f")
:format(ri, r[1].i, r[#r].i, #r, wlo, whi))
end
-- Each slip join must sit between a decel run and an accel run.
for _, slip in ipairs({ { "PAWestSlip", 525 }, { "PAEastSlip", 551 } }) do
local name, join = slip[1], slip[2]
local before, after = false, false
for _, r in ipairs(runs) do
if r[#r].i < join and join - r[#r].i <= 6 then before = true end
if r[1].i > join and r[1].i - join <= 6 then after = true end
if r[1].i <= join and r[#r].i >= join then
-- run spans the join: counts as adjacent on both sides
before = true; after = true
end
end
say(("%s join=%d: decel-side run %s, accel-side run %s -> %s")
:format(name, join, tostring(before), tostring(after),
(before and after) and "PASS" or "FAIL"))
end
return table.concat(out, "\n")
+112
View File
@@ -0,0 +1,112 @@
-- Verify the linear slips against assertions that can FAIL.
-- Fixes the earlier harness bug: FindFirstChild must be RECURSIVE (PAFloor is
-- nested), otherwise `floor` is nil and every apron test vacuously reports
-- "does not reach" regardless of the real geometry.
local HttpService = game:GetService("HttpService")
local map = workspace:FindFirstChild("Map")
if not map then return "no Map -- run the rebuild first" end
local ramps = map:FindFirstChild("PARamps", true)
if not ramps then return "PARamps folder missing" end
local floor = map:FindFirstChild("PAFloor", true) -- RECURSIVE
if not floor then return "PAFloor missing" end
local fTop = floor.Position.Y + floor.Size.Y / 2
local fx0, fx1 = floor.Position.X - floor.Size.X / 2, floor.Position.X + floor.Size.X / 2
local fz0, fz1 = floor.Position.Z - floor.Size.Z / 2, floor.Position.Z + floor.Size.Z / 2
-- Collect deck segments per slip, ordered by their index suffix so we can find
-- the true LOW end rather than guessing from a bounding box.
local decks = { PAWestSlip = {}, PAEastSlip = {} }
local counts = {}
for _, d in ipairs(ramps:GetDescendants()) do
if d:IsA("BasePart") then
counts[d.Name:gsub("_%d+$", "")] = (counts[d.Name:gsub("_%d+$", "")] or 0) + 1
for slipName, list in pairs(decks) do
local idx = d.Name:match("^" .. slipName .. "_Deck_(%d+)$")
if idx then
table.insert(list, { i = tonumber(idx), p = d.Position, s = d.Size, cf = d.CFrame })
end
end
end
end
local verdict, report = {}, {}
for slipName, list in pairs(decks) do
table.sort(list, function(a, b) return a.i < b.i end)
if #list < 50 then
table.insert(verdict, slipName .. " FAIL: only " .. #list .. " deck segments")
else
local first, last = list[1], list[#list]
local yTopFirst = first.p.Y + first.s.Y / 2
local yTopLast = last.p.Y + last.s.Y / 2
-- 1. Does the LOW end sit inside the apron footprint?
local inX = last.p.X >= fx0 and last.p.X <= fx1
local inZ = last.p.Z >= fz0 and last.p.Z <= fz1
local lands = inX and inZ
-- 2. Is the low end FLUSH with the apron surface? A lip catches cars.
local lip = yTopLast - fTop
-- 3. Is the high end flush with the mainline deck (Y 103)?
local mouthErr = yTopFirst - 103
-- 4. Peak grade over any single segment.
local maxGrade = 0
for k = 2, #list do
local a, b = list[k - 1], list[k]
local dy = math.abs((b.p.Y) - (a.p.Y))
local dxz = (Vector3.new(b.p.X, 0, b.p.Z) - Vector3.new(a.p.X, 0, a.p.Z)).Magnitude
if dxz > 0.5 then maxGrade = math.max(maxGrade, dy / dxz) end
end
report[slipName] = {
segs = #list,
mouth = { x = math.floor(first.p.X), z = math.floor(first.p.Z), yTop = math.floor(yTopFirst * 10) / 10 },
landing = { x = math.floor(last.p.X), z = math.floor(last.p.Z), yTop = math.floor(yTopLast * 10) / 10 },
landsInApron = lands,
lipStuds = math.floor(lip * 100) / 100,
mouthErrStuds = math.floor(mouthErr * 100) / 100,
peakGradePct = math.floor(maxGrade * 1000) / 10,
}
if not lands then
table.insert(verdict, ("%s FAIL: landing (%d,%d) outside apron x[%d..%d] z[%d..%d]")
:format(slipName, last.p.X, last.p.Z, fx0, fx1, fz0, fz1))
end
if math.abs(lip) > 1.5 then
table.insert(verdict, ("%s FAIL: %.2f stud lip at apron"):format(slipName, lip))
end
if math.abs(mouthErr) > 1.5 then
table.insert(verdict, ("%s FAIL: %.2f stud step at mainline mouth"):format(slipName, mouthErr))
end
if maxGrade > 0.16 then
table.insert(verdict, ("%s FAIL: peak grade %.1f%%"):format(slipName, maxGrade * 100))
end
end
end
-- 5. Does the mainline parapet actually open at each mouth? Count Barrier parts
-- within 160 studs of each mouth on the lot side; a sealed mouth = ramp is
-- decorative.
local road = map
local mouthBlocked = {}
for slipName, r in pairs(report) do
local mp = Vector3.new(r.mouth.x, 103, r.mouth.z)
local n = 0
for _, d in ipairs(road:GetDescendants()) do
if d:IsA("BasePart") and (d.Name == "Barrier" or d.Name == "Parapet") then
if (Vector3.new(d.Position.X, 103, d.Position.Z) - mp).Magnitude < 150 then n += 1 end
end
end
mouthBlocked[slipName] = n
end
return HttpService:JSONEncode({
apron = { top = fTop, x = { math.floor(fx0), math.floor(fx1) }, z = { math.floor(fz0), math.floor(fz1) } },
slips = report,
partCounts = counts,
barriersNearMouth = mouthBlocked,
verdict = #verdict == 0 and "ALL PASS" or table.concat(verdict, " | "),
})