Aux-lane suppression + PA geometry fixes; probe/verify tooling
CI / quality (push) Failing after 1m9s
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:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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) },
|
||||
})
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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, " | "),
|
||||
})
|
||||
Reference in New Issue
Block a user