Disable PA on/off ramps; restore mainline to pre-ramp state
Three iterations of the Daikoku PA ramps never read as a real
interchange. Measured why, rather than tuning further:
* the entry and exit decks INTERSECTED -- 2547 sample pairs within
30 studs horizontally at <12 studs dY, worst case 0.5 studs apart
at dY=0.0. On screen that is the bowtie tangle of white rails.
* both real Daikoku ramps turn the SAME way (clockwise; the guide
video says "right, right"). This build mirrored them into each
other.
* the real arc wraps OUTSIDE the lot and forms its north/east
boundary. This build cut across the apron interior.
Gated behind BUILD_PA_RAMPS=false rather than deleted -- the radius
was already correct (470 studs vs an OSM-measured mean of 469) and
the merge lands tangent at 0.1-0.2 deg divergence. Both worth keeping.
Also removed two leftovers that would have outlived the ramps:
* rampOpen punched holes in the mainline's outer parapet, railing
and kill wall for ramps that no longer exist -- a barrier gap with
a drop behind it. Sealed via RAMP_EDGE_OPENINGS=false. Verified:
Barrier/EdgeRail/KillWall = 1646 each = 823 samples x 2 edges,
0 road samples without a guard within 90 studs.
* gore zebra hatching painted 1024 bars + 64 boundary lines marking
a junction that is not there. Sealed via RAMP_GORE_MARKINGS=false.
Verified after a cache-busting fresh rebuild (Studio's require() cache
otherwise serves the previous map and reports a false pass):
parts 22628 -> 15988
all ramp part classes = 0
Road = 823 (unchanged), spawn pad + PASpawn intact
clip probe TRUSTWORTHY (control +11/-0), CLIPPING=0
Research and rebuild spec kept for when this is revisited:
reference/ramps/BUILD_SPEC.md, INDEX.md (10 CC-licensed Commons
photos), docs/daikoku-reference-spec.md (OSM circle fits).
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Eyes-on shots at the two REPORTED defects (not generic beauty angles).
|
||||
|
||||
Targets:
|
||||
A entry ramp start (-4122, 47, -7860) -- the 390-stud access gap
|
||||
B exit ramp start (-4258, 47, -7836)
|
||||
C mainline junction (-3700, 102, -7184) -- the dY -6.0 step
|
||||
D ramp vs nearest building(-4419, 223, -6751)
|
||||
"""
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, "/tmp")
|
||||
import rbx # noqa: E402
|
||||
|
||||
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
|
||||
|
||||
SHOTS = [
|
||||
# name, camera pos, look-at
|
||||
("D1_entry_ramp_start", (-4122 + 210, 47 + 120, -7860 + 210), (-4122, 47, -7860)),
|
||||
("D2_entry_start_low", (-4122 + 70, 47 + 25, -7860 + 70), (-4122, 55, -7860)),
|
||||
("D3_exit_ramp_start", (-4258 - 200, 47 + 110, -7836 + 200), (-4258, 47, -7836)),
|
||||
("D4_junction_step", (-3700 + 190, 102 + 90, -7184 + 190), (-3700, 102, -7184)),
|
||||
("D5_ramp_vs_building", (-4419 + 520, 223 + 180, -6751 + 520), (-4300, 120, -7100)),
|
||||
("D6_corridor_along", (-4122, 47 + 260, -7860 + 480), (-4300, 110, -7200)),
|
||||
]
|
||||
|
||||
CAM = """
|
||||
local cam = workspace.CurrentCamera
|
||||
cam.CameraType = Enum.CameraType.Scriptable
|
||||
cam.FieldOfView = {fov}
|
||||
cam.CFrame = CFrame.lookAt(Vector3.new({px},{py},{pz}), Vector3.new({tx},{ty},{tz}))
|
||||
return "ok"
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
made = []
|
||||
for name, (px, py, pz), (tx, ty, tz) in SHOTS:
|
||||
fov = 70 if "low" in name or "along" in name else 55
|
||||
r = rbx.call(
|
||||
"execute_luau",
|
||||
{
|
||||
"datamodel_type": "Edit",
|
||||
"code": CAM.format(
|
||||
fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz
|
||||
),
|
||||
},
|
||||
wait=12,
|
||||
timeout=300,
|
||||
)
|
||||
if isinstance(r, dict) and r.get("isError"):
|
||||
print(f"{name}: CAM FAIL {str(r)[:120]}")
|
||||
continue
|
||||
time.sleep(1.5)
|
||||
cap = rbx.call(
|
||||
"screen_capture", {"capture_id": "ScreenCapture_1"}, wait=22, timeout=300
|
||||
)
|
||||
imgs = cap.get("images") if isinstance(cap, dict) else None
|
||||
if not imgs:
|
||||
print(f"{name}: CAPTURE FAIL {str(cap)[:140]}")
|
||||
continue
|
||||
dest = f"{OUT}/{name}.jpg"
|
||||
shutil.copy(imgs[0], dest)
|
||||
made.append(dest)
|
||||
print(f"{name}: OK -> {dest}")
|
||||
print(f"\nMADE {len(made)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Eyes-on shots of the PA flyover ramps.
|
||||
|
||||
Camera targets are QUERIED FROM THE LIVE GEOMETRY on every run, never
|
||||
hardcoded. The previous capture script pinned literal coordinates at the
|
||||
old defect sites; after the ramps were redesigned those coords pointed at
|
||||
empty air and the shots silently framed the wrong thing. Same class of bug
|
||||
as the hardcoded yFork=96 -- a constant that stops matching the model.
|
||||
|
||||
Each shot names a part and an offset; the part's real position is looked up
|
||||
in Studio at capture time. If a named part is missing the shot is SKIPPED
|
||||
with a loud message rather than falling back to a stale guess.
|
||||
"""
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, "/tmp")
|
||||
import rbx # noqa: E402
|
||||
|
||||
OUT = "/home/jesse/roblox/tokyo-wangan-racing/tools/shots"
|
||||
|
||||
# name, anchor part, camera offset from anchor, look-at offset from anchor, fov
|
||||
SHOTS = [
|
||||
# The flyover itself, from the side -- the money shot: ramp over deck.
|
||||
("F1_flyover_side", "PAEntryRampCross_100", (300, 150, 300), (0, -20, 0), 55),
|
||||
# Underneath the crossing, looking up: proves real clearance.
|
||||
("F2_flyover_under", "PAEntryRampCross_100", (140, -70, 140), (0, 10, 0), 75),
|
||||
# The spiral descending into the PA apron.
|
||||
("F3_spiral_into_pa", "PAEntryRampTrunk_200", (340, 220, 340), (-120, -60, -120), 60),
|
||||
# Exit ramp climbing out of the PA.
|
||||
("F4_exit_climb", "PAExitRampTrunk_200", (-340, 200, 300), (60, -40, 40), 60),
|
||||
# Where the descent lands on the far carriageway.
|
||||
("F5_landing_merge", "PAEntryRampNear_200", (220, 130, 220), (0, -15, 0), 55),
|
||||
# Wide establishing shot of the whole interchange.
|
||||
("F6_wide", "PAEntryRampCross_100", (900, 620, 900), (-250, -120, -250), 60),
|
||||
]
|
||||
|
||||
POS_QUERY = """
|
||||
local ws=game:GetService("Workspace"); local map=ws:FindFirstChild("Map")
|
||||
if not map then return "NOMAP" end
|
||||
local want={}
|
||||
for _,n in ipairs({%s}) do want[n]=true end
|
||||
local found={}
|
||||
for _,d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") and want[d.Name] then
|
||||
found[#found+1]=("%%s=%%.1f,%%.1f,%%.1f"):format(d.Name,d.Position.X,d.Position.Y,d.Position.Z)
|
||||
end
|
||||
end
|
||||
return table.concat(found,"|")
|
||||
"""
|
||||
|
||||
CAM = """
|
||||
local cam = workspace.CurrentCamera
|
||||
cam.CameraType = Enum.CameraType.Scriptable
|
||||
cam.FieldOfView = {fov}
|
||||
cam.CFrame = CFrame.lookAt(Vector3.new({px},{py},{pz}), Vector3.new({tx},{ty},{tz}))
|
||||
return "ok"
|
||||
"""
|
||||
|
||||
|
||||
def fetch_anchors(names):
|
||||
"""Look up each anchor part's real position in the live datamodel."""
|
||||
lst = ",".join('"%s"' % n for n in sorted(set(names)))
|
||||
r = rbx.call(
|
||||
"execute_luau",
|
||||
{"datamodel_type": "Edit", "code": POS_QUERY % lst},
|
||||
wait=40,
|
||||
timeout=400,
|
||||
)
|
||||
txt = str(r)
|
||||
out = {}
|
||||
for chunk in txt.split("|"):
|
||||
if "=" not in chunk:
|
||||
continue
|
||||
nm, _, vals = chunk.partition("=")
|
||||
nm = nm.strip().strip("'\"[]{}() ")
|
||||
parts = vals.strip().strip("'\"[]{}() ").split(",")
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
try:
|
||||
out[nm] = tuple(float(v) for v in parts)
|
||||
except ValueError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
anchors = fetch_anchors([s[1] for s in SHOTS])
|
||||
print(f"resolved {len(anchors)} anchors: {sorted(anchors)}")
|
||||
made, skipped = [], []
|
||||
for name, anchor, cam_off, look_off, fov in SHOTS:
|
||||
if anchor not in anchors:
|
||||
print(f"{name}: SKIP -- anchor {anchor} not found in model")
|
||||
skipped.append(name)
|
||||
continue
|
||||
ax, ay, az = anchors[anchor]
|
||||
px, py, pz = ax + cam_off[0], ay + cam_off[1], az + cam_off[2]
|
||||
tx, ty, tz = ax + look_off[0], ay + look_off[1], az + look_off[2]
|
||||
r = rbx.call(
|
||||
"execute_luau",
|
||||
{
|
||||
"datamodel_type": "Edit",
|
||||
"code": CAM.format(
|
||||
fov=fov, px=px, py=py, pz=pz, tx=tx, ty=ty, tz=tz
|
||||
),
|
||||
},
|
||||
wait=12,
|
||||
timeout=300,
|
||||
)
|
||||
if isinstance(r, dict) and r.get("isError"):
|
||||
print(f"{name}: CAM FAIL {str(r)[:120]}")
|
||||
skipped.append(name)
|
||||
continue
|
||||
time.sleep(1.5)
|
||||
cap = rbx.call(
|
||||
"screen_capture", {"capture_id": "ScreenCapture_1"}, wait=22, timeout=300
|
||||
)
|
||||
imgs = cap.get("images") if isinstance(cap, dict) else None
|
||||
if not imgs:
|
||||
print(f"{name}: CAPTURE FAIL {str(cap)[:140]}")
|
||||
skipped.append(name)
|
||||
continue
|
||||
dest = f"{OUT}/{name}.jpg"
|
||||
shutil.copy(imgs[0], dest)
|
||||
made.append(dest)
|
||||
print(f"{name}: OK ({anchor} @ {ax:.0f},{ay:.0f},{az:.0f}) -> {dest}")
|
||||
print(f"\nMADE {len(made)} SKIPPED {len(skipped)} {skipped if skipped else ''}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -16,7 +16,14 @@ for _, d in ipairs(map:GetDescendants()) do
|
||||
if nm:find("PierColumn") or nm:find("PAMast") or nm:find("GirderRib") then
|
||||
table.insert(structural, d)
|
||||
end
|
||||
for _, base in ipairs({ "PAEntryRampTrunk", "PAEntryRampNear", "PAExitRampTrunk", "PAExitRampNear" }) do
|
||||
for _, base in ipairs({
|
||||
"PAEntryRampTrunk",
|
||||
"PAEntryRampCross",
|
||||
"PAEntryRampNear",
|
||||
"PAExitRampTrunk",
|
||||
"PAExitRampCross",
|
||||
"PAExitRampNear",
|
||||
}) do
|
||||
if nm:sub(1, #base + 1) == base .. "_" then
|
||||
runs[base] = runs[base] or {}
|
||||
table.insert(runs[base], d)
|
||||
@@ -56,7 +63,14 @@ table.insert(out, "structuralParts=" .. #structural)
|
||||
local function sortKey(p)
|
||||
return tonumber(p.Name:match("_(%d+)$")) or 0
|
||||
end
|
||||
for _, base in ipairs({ "PAEntryRampTrunk", "PAEntryRampNear", "PAExitRampTrunk", "PAExitRampNear" }) do
|
||||
for _, base in ipairs({
|
||||
"PAEntryRampTrunk",
|
||||
"PAEntryRampCross",
|
||||
"PAEntryRampNear",
|
||||
"PAExitRampTrunk",
|
||||
"PAExitRampCross",
|
||||
"PAExitRampNear",
|
||||
}) do
|
||||
local parts = runs[base]
|
||||
if not parts then
|
||||
table.insert(out, base .. ": MISSING")
|
||||
@@ -73,8 +87,15 @@ for _, base in ipairs({ "PAEntryRampTrunk", "PAEntryRampNear", "PAExitRampTrunk"
|
||||
end
|
||||
end
|
||||
|
||||
-- Trunk -> Near handoff
|
||||
for _, pair in ipairs({ { "PAEntryRampTrunk", "PAEntryRampNear" }, { "PAExitRampTrunk", "PAExitRampNear" } }) do
|
||||
-- Handoffs along the flyover chain: Trunk -> Cross -> Near. The ramps now
|
||||
-- cross OVER the mainline, so `Cross` sits between the spiral and the
|
||||
-- descent; checking Trunk->Near directly would report a false gap.
|
||||
for _, pair in ipairs({
|
||||
{ "PAEntryRampTrunk", "PAEntryRampCross" },
|
||||
{ "PAEntryRampCross", "PAEntryRampNear" },
|
||||
{ "PAExitRampTrunk", "PAExitRampCross" },
|
||||
{ "PAExitRampCross", "PAExitRampNear" },
|
||||
}) do
|
||||
local t, nr = runs[pair[1]], runs[pair[2]]
|
||||
if t and nr then
|
||||
table.sort(t, function(a, b)
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
-- Diagnose the two reported defects:
|
||||
-- (1) ramps not accessible in either direction (connectivity)
|
||||
-- (2) ramps pass through buildings (clipping)
|
||||
-- Uses native GetPartBoundsInBox so this stays fast on ~22k parts.
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local BUILDING = {
|
||||
Building = true,
|
||||
TokyoTower = true,
|
||||
TowerDeck = true,
|
||||
TowerAntenna = true,
|
||||
ShiodomeTower = true,
|
||||
TowerCrown = true,
|
||||
DietTower = true,
|
||||
RoppongiHills = true,
|
||||
TokyoStation = true,
|
||||
StationDome = true,
|
||||
Skytree = true,
|
||||
GinzaBlock = true,
|
||||
NationalDiet = true,
|
||||
BridgeTower = true,
|
||||
TowerCrossbeam = true,
|
||||
Structure = true,
|
||||
PalaceWall = true,
|
||||
}
|
||||
|
||||
local ramp, drivable = {}, {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
local n = d.Name
|
||||
if n:find("PAEntryRamp") or n:find("PAExitRamp") then
|
||||
table.insert(ramp, d)
|
||||
elseif n:match("^Road_%d+$") or n == "PAFloor" then
|
||||
table.insert(drivable, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #ramp == 0 then
|
||||
return "no ramp parts"
|
||||
end
|
||||
|
||||
-- ---------- (2) BUILDING CLIP CHECK ----------
|
||||
local op = OverlapParams.new()
|
||||
op.MaxParts = 40
|
||||
local clipHits, clipNames, worst = 0, {}, {}
|
||||
for _, r in ipairs(ramp) do
|
||||
local hits = ws:GetPartBoundsInBox(r.CFrame, r.Size, op)
|
||||
for _, h in ipairs(hits) do
|
||||
if h ~= r and BUILDING[h.Name] then
|
||||
clipHits += 1
|
||||
clipNames[h.Name] = (clipNames[h.Name] or 0) + 1
|
||||
if #worst < 5 then
|
||||
table.insert(worst, ("%s@%.0f,%.0f,%.0f"):format(r.Name, r.Position.X, r.Position.Y, r.Position.Z))
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- CONTROL: same probe against mainline Road_ parts (known-good, should be ~0)
|
||||
local ctrlHits, ctrlN = 0, 0
|
||||
for i = 1, #drivable, 9 do
|
||||
local d = drivable[i]
|
||||
ctrlN += 1
|
||||
local hits = ws:GetPartBoundsInBox(d.CFrame, d.Size, op)
|
||||
for _, h in ipairs(hits) do
|
||||
if h ~= d and BUILDING[h.Name] then
|
||||
ctrlHits += 1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local cn = {}
|
||||
for k, v in pairs(clipNames) do
|
||||
table.insert(cn, k .. "=" .. v)
|
||||
end
|
||||
|
||||
-- ---------- (1) CONNECTIVITY ----------
|
||||
-- Ramp basename groups, then endpoints of each group by trailing index.
|
||||
local groups = {}
|
||||
for _, r in ipairs(ramp) do
|
||||
local base, idx = r.Name:match("^(.-)_?(%d+)$")
|
||||
base = base or r.Name
|
||||
idx = tonumber(idx) or 0
|
||||
local g = groups[base]
|
||||
if not g then
|
||||
g = { lo = math.huge, hi = -math.huge, loP = nil, hiP = nil, n = 0 }
|
||||
groups[base] = g
|
||||
end
|
||||
g.n += 1
|
||||
if idx < g.lo then
|
||||
g.lo, g.loP = idx, r
|
||||
end
|
||||
if idx > g.hi then
|
||||
g.hi, g.hiP = idx, r
|
||||
end
|
||||
end
|
||||
|
||||
-- For an endpoint, find nearest drivable surface and report gap + height delta.
|
||||
local function nearestDrivable(p)
|
||||
local bd, bp = math.huge, nil
|
||||
for _, d in ipairs(drivable) do
|
||||
local dd = (d.Position - p).Magnitude
|
||||
if dd < bd then
|
||||
bd, bp = dd, d
|
||||
end
|
||||
end
|
||||
return bd, bp
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for base, g in pairs(groups) do
|
||||
if g.loP and g.hiP then
|
||||
local d1, p1 = nearestDrivable(g.loP.Position)
|
||||
local d2, p2 = nearestDrivable(g.hiP.Position)
|
||||
table.insert(
|
||||
lines,
|
||||
("%s n=%d | START gap=%.0f dY=%.1f to %s | END gap=%.0f dY=%.1f to %s"):format(
|
||||
base,
|
||||
g.n,
|
||||
d1,
|
||||
p1 and (g.loP.Position.Y - p1.Position.Y) or -999,
|
||||
p1 and p1.Name or "nil",
|
||||
d2,
|
||||
p2 and (g.hiP.Position.Y - p2.Position.Y) or -999,
|
||||
p2 and p2.Name or "nil"
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return ("CLIP ramp=%d/%d parts hit buildings [%s] samples=[%s] || CONTROL road %d/%d hit\n%s"):format(
|
||||
clipHits,
|
||||
#ramp,
|
||||
table.concat(cn, ", "),
|
||||
table.concat(worst, " ; "),
|
||||
ctrlHits,
|
||||
ctrlN,
|
||||
table.concat(lines, "\n")
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
-- Why did the building-clip probe return 0 with a 0 control? Diagnose the metric.
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
|
||||
local BUILDING = {
|
||||
Building = true,
|
||||
TokyoTower = true,
|
||||
TowerDeck = true,
|
||||
TowerAntenna = true,
|
||||
ShiodomeTower = true,
|
||||
TowerCrown = true,
|
||||
DietTower = true,
|
||||
RoppongiHills = true,
|
||||
TokyoStation = true,
|
||||
StationDome = true,
|
||||
Skytree = true,
|
||||
GinzaBlock = true,
|
||||
NationalDiet = true,
|
||||
BridgeTower = true,
|
||||
TowerCrossbeam = true,
|
||||
Structure = true,
|
||||
PalaceWall = true,
|
||||
}
|
||||
|
||||
local ramp, bld = {}, {}
|
||||
local anyQueryOff, anyCollideOff = 0, 0
|
||||
for _, d in ipairs(ws:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
if d.Name:find("PAEntryRamp") or d.Name:find("PAExitRamp") then
|
||||
table.insert(ramp, d)
|
||||
elseif BUILDING[d.Name] then
|
||||
table.insert(bld, d)
|
||||
if not d.CanQuery then
|
||||
anyQueryOff += 1
|
||||
end
|
||||
if not d.CanCollide then
|
||||
anyCollideOff += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #bld == 0 then
|
||||
return "NO building parts found in Workspace at all (name set wrong?)"
|
||||
end
|
||||
|
||||
-- nearest building to the ramp chain (subsample ramps for speed)
|
||||
local bd, br, bb = math.huge, nil, nil
|
||||
for i = 1, #ramp, 7 do
|
||||
local r = ramp[i]
|
||||
for _, b in ipairs(bld) do
|
||||
local dd = (r.Position - b.Position).Magnitude
|
||||
if dd < bd then
|
||||
bd, br, bb = dd, r, b
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- how many buildings are anywhere near the PA at all?
|
||||
local cen = Vector3.new(-4128, 81, -7438)
|
||||
local near2000 = 0
|
||||
for _, b in ipairs(bld) do
|
||||
if (b.Position - cen).Magnitude < 2000 then
|
||||
near2000 += 1
|
||||
end
|
||||
end
|
||||
|
||||
-- does the raw overlap API return ANYTHING for a ramp part? (metric sanity)
|
||||
local op = OverlapParams.new()
|
||||
op.MaxParts = 50
|
||||
local sample = ramp[math.floor(#ramp / 2)]
|
||||
local hits = ws:GetPartBoundsInBox(sample.CFrame, sample.Size * 1.2, op)
|
||||
local hn = {}
|
||||
for i = 1, math.min(#hits, 8) do
|
||||
table.insert(hn, hits[i].Name)
|
||||
end
|
||||
|
||||
return ("buildings=%d (CanQuery off=%d, CanCollide off=%d) | nearestBldToRamp=%.0f (%s -> %s @%.0f,%.0f,%.0f) | bldWithin2000ofPA=%d | overlapSanity hits=%d [%s]"):format(
|
||||
#bld,
|
||||
anyQueryOff,
|
||||
anyCollideOff,
|
||||
bd,
|
||||
br and br.Name or "nil",
|
||||
bb and bb.Name or "nil",
|
||||
bb and bb.Position.X or 0,
|
||||
bb and bb.Position.Y or 0,
|
||||
bb and bb.Position.Z or 0,
|
||||
near2000,
|
||||
#hits,
|
||||
table.concat(hn, ",")
|
||||
)
|
||||
@@ -0,0 +1,108 @@
|
||||
-- Building-clip probe, CORRECTLY SCOPED.
|
||||
--
|
||||
-- The previous version tested only PAEntryRamp*/PAExitRamp* against buildings
|
||||
-- and reported 0/1200 -- but its control ALSO returned 0, which makes the
|
||||
-- result vacuous. Worse, the part visibly speared by a tower is a Road_*
|
||||
-- segment, i.e. it was sitting in the control group, not the test group.
|
||||
--
|
||||
-- This version:
|
||||
-- * tests EVERY drivable part (Road_*, ramps, PA surfaces) -- 21k parts
|
||||
-- * proves the metric can detect a hit BEFORE trusting a zero (positive
|
||||
-- control: a synthetic part deliberately placed inside a building)
|
||||
-- * reports the worst offenders with coordinates so they can be shot
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local BUILDING = {
|
||||
Building = true,
|
||||
TokyoTower = true,
|
||||
TowerDeck = true,
|
||||
TowerAntenna = true,
|
||||
ShiodomeTower = true,
|
||||
TowerCrown = true,
|
||||
DietTower = true,
|
||||
RoppongiHills = true,
|
||||
TokyoStation = true,
|
||||
StationDome = true,
|
||||
Skytree = true,
|
||||
GinzaBlock = true,
|
||||
NationalDiet = true,
|
||||
BridgeTower = true,
|
||||
TowerCrossbeam = true,
|
||||
PalaceWall = true,
|
||||
}
|
||||
|
||||
local function isDrivable(n)
|
||||
return n:match("^Road_")
|
||||
or n:find("Ramp")
|
||||
or n == "PAFloor"
|
||||
or n:match("^Apron")
|
||||
or n:match("^Slip")
|
||||
or n:match("^Merge")
|
||||
end
|
||||
|
||||
local drive, bld = {}, {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
if BUILDING[d.Name] then
|
||||
table.insert(bld, d)
|
||||
elseif isDrivable(d.Name) then
|
||||
table.insert(drive, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #bld == 0 or #drive == 0 then
|
||||
return ("SCOPE FAIL drive=%d bld=%d"):format(#drive, #bld)
|
||||
end
|
||||
|
||||
-- Build an OverlapParams limited to building parts only.
|
||||
local op = OverlapParams.new()
|
||||
op.FilterType = Enum.RaycastFilterType.Include
|
||||
op.FilterDescendantsInstances = bld
|
||||
op.MaxParts = 12
|
||||
|
||||
-- POSITIVE CONTROL: put a probe cube dead-centre inside a building and
|
||||
-- confirm the metric reports it. If this returns 0, the metric is broken and
|
||||
-- every other number below is meaningless.
|
||||
local target = bld[math.floor(#bld / 2)]
|
||||
local ctrlHits = ws:GetPartBoundsInBox(CFrame.new(target.Position), Vector3.new(4, 4, 4), op)
|
||||
local controlOK = #ctrlHits > 0
|
||||
|
||||
local hits, samples = 0, {}
|
||||
for _, p in ipairs(drive) do
|
||||
-- shrink slightly so parts merely TOUCHING a facade aren't flagged;
|
||||
-- we want genuine interpenetration
|
||||
local probe = p.Size - Vector3.new(1.5, 1.5, 1.5)
|
||||
if probe.X > 0.5 and probe.Y > 0.5 and probe.Z > 0.5 then
|
||||
local h = ws:GetPartBoundsInBox(p.CFrame, probe, op)
|
||||
if #h > 0 then
|
||||
hits += 1
|
||||
if #samples < 6 then
|
||||
table.insert(
|
||||
samples,
|
||||
("%s@%.0f,%.0f,%.0f in %s"):format(
|
||||
p.Name,
|
||||
p.Position.X,
|
||||
p.Position.Y,
|
||||
p.Position.Z,
|
||||
h[1].Name
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return ("CONTROL=%s (%d hits on a part inside a building) || drivable=%d bld=%d CLIPPING=%d [%s]"):format(
|
||||
controlOK and "PASS" or "FAIL-METRIC-BROKEN",
|
||||
#ctrlHits,
|
||||
#drive,
|
||||
#bld,
|
||||
hits,
|
||||
table.concat(samples, " ; ")
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
-- CLIP PROBE v3 -- INVERTED SCOPE.
|
||||
--
|
||||
-- v2 tested drivable parts against a HARDCODED WHITELIST of 16 building
|
||||
-- names (Building, TokyoTower, GinzaBlock, ...). It reported CLIPPING=0 with
|
||||
-- a passing control, and I reported that as fact. The user can see ramps
|
||||
-- going through buildings, so the zero was false.
|
||||
--
|
||||
-- The bug is the whitelist: any obstacle whose name isn't one of those 16 is
|
||||
-- invisible to the test. The PA structures (shops, kiosks, canopies) are the
|
||||
-- prime suspect -- they sit exactly where the ramp ends land.
|
||||
--
|
||||
-- v3 inverts the scope: an obstacle is ANY part that is not drivable and not
|
||||
-- infrastructure (pier/rail/sign/ground). No name list to go stale.
|
||||
-- It also reports offenders GROUPED BY OBSTACLE NAME, so the answer names
|
||||
-- the thing being speared instead of just counting.
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local function isDrivable(n)
|
||||
return n:match("^Road_")
|
||||
or n:find("Ramp")
|
||||
or n == "PAFloor"
|
||||
or n:match("^Apron")
|
||||
or n:match("^Slip")
|
||||
or n:match("^Merge")
|
||||
end
|
||||
|
||||
-- things that are ALLOWED to intersect a road (structure, decor, markings)
|
||||
local function isInfra(n)
|
||||
return n:find("Pier")
|
||||
or n:find("Pillar")
|
||||
or n:find("Column")
|
||||
or n:find("Support")
|
||||
or n:find("Beam")
|
||||
or n:find("Rail")
|
||||
or n:find("Guard")
|
||||
or n:find("Barrier")
|
||||
or n:find("Sign")
|
||||
or n:find("Light")
|
||||
or n:find("Lamp")
|
||||
or n:find("Line")
|
||||
or n:find("Mark")
|
||||
or n:find("Stripe")
|
||||
or n:find("Deck")
|
||||
or n:find("Water")
|
||||
or n == "Ground"
|
||||
or n:find("Grounds")
|
||||
or n:find("Median")
|
||||
or n:find("Curb")
|
||||
or n:find("Wall")
|
||||
end
|
||||
|
||||
local drive, obst = {}, {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
local n = d.Name
|
||||
if isDrivable(n) then
|
||||
table.insert(drive, d)
|
||||
elseif not isInfra(n) then
|
||||
table.insert(obst, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #obst == 0 or #drive == 0 then
|
||||
return ("SCOPE FAIL drive=%d obst=%d"):format(#drive, #obst)
|
||||
end
|
||||
|
||||
local op = OverlapParams.new()
|
||||
op.FilterType = Enum.RaycastFilterType.Include
|
||||
op.FilterDescendantsInstances = obst
|
||||
op.MaxParts = 12
|
||||
|
||||
-- POSITIVE CONTROL: probe dead-centre of a known obstacle must report a hit.
|
||||
local target = obst[math.floor(#obst / 2)]
|
||||
local ctrl = ws:GetPartBoundsInBox(CFrame.new(target.Position), Vector3.new(4, 4, 4), op)
|
||||
local controlOK = #ctrl > 0
|
||||
|
||||
-- SECOND CONTROL: a box far out in empty sky must report ZERO. Guards the
|
||||
-- opposite failure -- a filter so broad everything "hits".
|
||||
local negCtrl = ws:GetPartBoundsInBox(CFrame.new(0, 40000, 0), Vector3.new(8, 8, 8), op)
|
||||
local negOK = #negCtrl == 0
|
||||
|
||||
local hits = 0
|
||||
local byObst, byRamp = {}, {}
|
||||
local samples = {}
|
||||
for _, p in ipairs(drive) do
|
||||
local probe = p.Size - Vector3.new(1.5, 1.5, 1.5)
|
||||
if probe.X > 0.5 and probe.Y > 0.5 and probe.Z > 0.5 then
|
||||
local h = ws:GetPartBoundsInBox(p.CFrame, probe, op)
|
||||
if #h > 0 then
|
||||
hits += 1
|
||||
local on = h[1].Name
|
||||
byObst[on] = (byObst[on] or 0) + 1
|
||||
local rn = p.Name:gsub("_%d+$", "")
|
||||
byRamp[rn] = (byRamp[rn] or 0) + 1
|
||||
if #samples < 10 then
|
||||
table.insert(
|
||||
samples,
|
||||
("%s@%.0f,%.0f,%.0f IN %s"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z, on)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local out = {}
|
||||
table.insert(
|
||||
out,
|
||||
("CONTROL+=%s(%d) CONTROL-=%s(%d) -> probe %s"):format(
|
||||
controlOK and "PASS" or "FAIL",
|
||||
#ctrl,
|
||||
negOK and "PASS" or "FAIL",
|
||||
#negCtrl,
|
||||
(controlOK and negOK) and "TRUSTWORTHY" or "BROKEN-IGNORE-RESULTS"
|
||||
)
|
||||
)
|
||||
table.insert(out, ("drivable=%d obstacles=%d CLIPPING=%d"):format(#drive, #obst, hits))
|
||||
|
||||
local function dump(t, label)
|
||||
local l = {}
|
||||
for k, v in pairs(t) do
|
||||
table.insert(l, ("%s=%d"):format(k, v))
|
||||
end
|
||||
table.sort(l, function(a, b)
|
||||
return a > b
|
||||
end)
|
||||
local keep = {}
|
||||
for i = 1, math.min(#l, 12) do
|
||||
table.insert(keep, l[i])
|
||||
end
|
||||
table.insert(out, label .. ": " .. table.concat(keep, " "))
|
||||
end
|
||||
if hits > 0 then
|
||||
dump(byObst, "speared obstacles")
|
||||
dump(byRamp, "offending road groups")
|
||||
for _, s in ipairs(samples) do
|
||||
table.insert(out, " " .. s)
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(out, "\n")
|
||||
@@ -0,0 +1,125 @@
|
||||
-- CLIP PROBE v4 -- FULL-WORKSPACE SCOPE. This is the one that finds the
|
||||
-- defect the user can see.
|
||||
--
|
||||
-- WHY v2 AND v3 BOTH RETURNED A FALSE ZERO:
|
||||
-- both scoped obstacles to the `Map` folder. The PA buildings are NOT in
|
||||
-- Map -- they are Creator-Store models parented directly to Workspace as
|
||||
-- siblings: McDonald's (1666 parts), Gas Station 1 (1463), 7 eleven (627),
|
||||
-- Restaurant1, Updated Ramen Shop, Fast Food Restaurant, すき家, Parking lot.
|
||||
-- The probes literally could not see the things being speared. A passing
|
||||
-- positive control did NOT save me: the control proved the metric works,
|
||||
-- not that the SCOPE covers the real obstacles.
|
||||
--
|
||||
-- v4 collects obstacles from the ENTIRE workspace, excluding only the Map
|
||||
-- folder's own drivable/infra parts. Reports per-model hit counts so we know
|
||||
-- exactly which shop each ramp is driving through.
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local function isDrivable(n)
|
||||
return n:match("^Road_")
|
||||
or n:find("Ramp")
|
||||
or n == "PAFloor"
|
||||
or n:match("^Apron")
|
||||
or n:match("^Slip")
|
||||
or n:match("^Merge")
|
||||
end
|
||||
|
||||
-- collect drivable parts (ramps especially) from Map
|
||||
local drive = {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") and isDrivable(d.Name) then
|
||||
table.insert(drive, d)
|
||||
end
|
||||
end
|
||||
|
||||
-- collect obstacles: every BasePart in workspace that is NOT inside Map.
|
||||
-- These are the imported building models.
|
||||
local obst = {}
|
||||
local modelOf = {}
|
||||
for _, top in ipairs(ws:GetChildren()) do
|
||||
if top ~= map and not top:IsA("Terrain") and not top:IsA("Camera") then
|
||||
if top:IsA("BasePart") then
|
||||
-- loose parts like stray PAPerimeter/PARampKerb: skip, they're infra
|
||||
if not isDrivable(top.Name) and not top.Name:find("Perimeter") and not top.Name:find("Kerb") then
|
||||
table.insert(obst, top)
|
||||
modelOf[top] = top.Name
|
||||
end
|
||||
else
|
||||
for _, d in ipairs(top:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
table.insert(obst, d)
|
||||
modelOf[d] = top.Name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #obst == 0 then
|
||||
return "SCOPE FAIL: no obstacles found outside Map"
|
||||
end
|
||||
|
||||
local op = OverlapParams.new()
|
||||
op.FilterType = Enum.RaycastFilterType.Include
|
||||
op.FilterDescendantsInstances = obst
|
||||
op.MaxParts = 20
|
||||
|
||||
-- CONTROL+ : a box at a known obstacle's centre must report a hit
|
||||
local tgt = obst[math.floor(#obst / 2)]
|
||||
local cPos = ws:GetPartBoundsInBox(CFrame.new(tgt.Position), Vector3.new(4, 4, 4), op)
|
||||
-- CONTROL- : empty sky must report zero
|
||||
local cNeg = ws:GetPartBoundsInBox(CFrame.new(0, 40000, 0), Vector3.new(8, 8, 8), op)
|
||||
local trustworthy = #cPos > 0 and #cNeg == 0
|
||||
|
||||
local hits = 0
|
||||
local byModel, byRamp = {}, {}
|
||||
local samples = {}
|
||||
for _, p in ipairs(drive) do
|
||||
local probe = p.Size - Vector3.new(1.0, 1.0, 1.0)
|
||||
if probe.X > 0.5 and probe.Y > 0.5 and probe.Z > 0.5 then
|
||||
local h = ws:GetPartBoundsInBox(p.CFrame, probe, op)
|
||||
if #h > 0 then
|
||||
hits += 1
|
||||
local mn = modelOf[h[1]] or h[1].Name
|
||||
byModel[mn] = (byModel[mn] or 0) + 1
|
||||
local rn = p.Name:gsub("_%d+$", "")
|
||||
byRamp[rn] = (byRamp[rn] or 0) + 1
|
||||
if #samples < 10 then
|
||||
table.insert(
|
||||
samples,
|
||||
("%s@%.0f,%.0f,%.0f IN %s"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z, mn)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local out = {}
|
||||
table.insert(
|
||||
out,
|
||||
("CONTROL+=%d CONTROL-=%d -> %s"):format(#cPos, #cNeg, trustworthy and "TRUSTWORTHY" or "BROKEN-IGNORE")
|
||||
)
|
||||
table.insert(out, ("drivable=%d obstacles(outside Map)=%d CLIPPING=%d"):format(#drive, #obst, hits))
|
||||
|
||||
local function dump(t, label)
|
||||
local l = {}
|
||||
for k, v in pairs(t) do
|
||||
table.insert(l, ("%s=%d"):format(k, v))
|
||||
end
|
||||
table.sort(l)
|
||||
table.insert(out, label .. ": " .. table.concat(l, " | "))
|
||||
end
|
||||
if hits > 0 then
|
||||
dump(byModel, "MODELS SPEARED")
|
||||
dump(byRamp, "offending ramp groups")
|
||||
for _, s in ipairs(samples) do
|
||||
table.insert(out, " " .. s)
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(out, "\n")
|
||||
@@ -0,0 +1,139 @@
|
||||
-- FLOATING SCENERY PROBE.
|
||||
--
|
||||
-- The wide captures show grey slabs/towers hanging in the sky with nothing
|
||||
-- under them. No existing probe catches this: the clip probe only asks
|
||||
-- "does road intersect building", which is 0 and correct. Floating geometry
|
||||
-- is the opposite failure -- parts that touch NOTHING at all.
|
||||
--
|
||||
-- Metric: for each candidate scenery part, raycast straight DOWN from its
|
||||
-- underside and report the distance to the first thing hit. A part sitting
|
||||
-- on ground/deck returns ~0. A floating part returns a large number or nil
|
||||
-- (nothing beneath it at all).
|
||||
--
|
||||
-- CONTROL: the same ray cast from a part known to be seated (a Road part)
|
||||
-- must return a small distance. If the control comes back nil/huge the
|
||||
-- raycast itself is broken and every "floating" result is meaningless --
|
||||
-- the silent-zero trap that produced two phantom defects earlier.
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local candidates, roads = {}, {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
local nm = d.Name
|
||||
if nm:sub(1, 5) == "Road_" then
|
||||
table.insert(roads, d)
|
||||
elseif
|
||||
nm:find("Building")
|
||||
or nm:find("Tower")
|
||||
or nm:find("Skyline")
|
||||
or nm:find("Block")
|
||||
or nm:find("Slab")
|
||||
or nm:find("Distant")
|
||||
then
|
||||
table.insert(candidates, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local params = RaycastParams.new()
|
||||
params.FilterType = Enum.RaycastFilterType.Exclude
|
||||
|
||||
-- distance from a part's underside straight down to the first hit
|
||||
local function dropDist(p)
|
||||
params.FilterDescendantsInstances = { p }
|
||||
local origin = p.Position - Vector3.new(0, p.Size.Y / 2 + 1, 0)
|
||||
local res = ws:Raycast(origin, Vector3.new(0, -6000, 0), params)
|
||||
if not res then
|
||||
return nil
|
||||
end
|
||||
return (origin - res.Position).Magnitude
|
||||
end
|
||||
|
||||
local out = {}
|
||||
|
||||
-- CONTROL: a road part must find something close beneath it (deck/pier/ground).
|
||||
local ctl, ctlN = nil, "none"
|
||||
for _, rd in ipairs(roads) do
|
||||
local d = dropDist(rd)
|
||||
if d then
|
||||
ctl, ctlN = d, rd.Name
|
||||
break
|
||||
end
|
||||
end
|
||||
if ctl == nil then
|
||||
table.insert(out, "CONTROL=FAIL raycast found nothing under ANY road -- probe is broken, ignore results below")
|
||||
else
|
||||
table.insert(out, ("CONTROL=PASS %.1f studs under %s (raycast works)"):format(ctl, ctlN))
|
||||
end
|
||||
|
||||
-- Classify candidates.
|
||||
local FLOAT_MIN = 60 -- studs of empty air below = floating
|
||||
local floating, seated, voidBelow = 0, 0, 0
|
||||
local worst, worstNm, worstPos = -1, "none", nil
|
||||
local samples = {}
|
||||
for _, p in ipairs(candidates) do
|
||||
local d = dropDist(p)
|
||||
if d == nil then
|
||||
voidBelow += 1
|
||||
if #samples < 8 then
|
||||
table.insert(
|
||||
samples,
|
||||
("%s@%.0f,%.0f,%.0f NOTHING-BELOW"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z)
|
||||
)
|
||||
end
|
||||
elseif d > FLOAT_MIN then
|
||||
floating += 1
|
||||
if d > worst then
|
||||
worst, worstNm, worstPos = d, p.Name, p.Position
|
||||
end
|
||||
if #samples < 8 then
|
||||
table.insert(
|
||||
samples,
|
||||
("%s@%.0f,%.0f,%.0f drop=%.0f"):format(p.Name, p.Position.X, p.Position.Y, p.Position.Z, d)
|
||||
)
|
||||
end
|
||||
else
|
||||
seated += 1
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(
|
||||
out,
|
||||
("candidates=%d roads=%d | seated=%d floating(>%d)=%d nothingBelow=%d"):format(
|
||||
#candidates,
|
||||
#roads,
|
||||
seated,
|
||||
FLOAT_MIN,
|
||||
floating,
|
||||
voidBelow
|
||||
)
|
||||
)
|
||||
if worstPos then
|
||||
table.insert(
|
||||
out,
|
||||
("WORST %s drop=%.0f at %.0f,%.0f,%.0f"):format(worstNm, worst, worstPos.X, worstPos.Y, worstPos.Z)
|
||||
)
|
||||
end
|
||||
for _, s in ipairs(samples) do
|
||||
table.insert(out, " " .. s)
|
||||
end
|
||||
|
||||
-- Name histogram so we can see WHICH generator made them.
|
||||
local hist = {}
|
||||
for _, p in ipairs(candidates) do
|
||||
local base = p.Name:gsub("_%d+$", "")
|
||||
hist[base] = (hist[base] or 0) + 1
|
||||
end
|
||||
local hl = {}
|
||||
for k, v in pairs(hist) do
|
||||
table.insert(hl, ("%s=%d"):format(k, v))
|
||||
end
|
||||
table.sort(hl)
|
||||
table.insert(out, "byName: " .. table.concat(hl, " "))
|
||||
|
||||
return table.concat(out, "\n")
|
||||
@@ -0,0 +1,104 @@
|
||||
-- FLYOVER PROBE. Two things the connectivity check cannot tell you:
|
||||
--
|
||||
-- 1. Does the crossing actually pass OVER the mainline deck, with real
|
||||
-- vertical clearance? A "flyover" that intersects the deck is just a
|
||||
-- wall. Measured as: for every Cross part, find mainline Road parts
|
||||
-- directly beneath it in plan, and report the worst (smallest) gap
|
||||
-- between the ramp underside and the road topside.
|
||||
--
|
||||
-- 2. Does the descent land tangent and flush on the FAR carriageway?
|
||||
-- Reported as the dY at the last descent part vs the road beneath it.
|
||||
--
|
||||
-- Every metric carries a CONTROL that must come back non-zero, because a
|
||||
-- silent 0 from a mis-scoped query is indistinguishable from a genuine pass
|
||||
-- -- that exact trap produced two phantom "defects" earlier in this build.
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local cross, descent, roads = {}, {}, {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
local nm = d.Name
|
||||
if nm:find("RampCross_") then
|
||||
table.insert(cross, d)
|
||||
elseif nm:find("RampNear_") then
|
||||
table.insert(descent, d)
|
||||
elseif nm:sub(1, 5) == "Road_" then
|
||||
table.insert(roads, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- plan-view overlap (ignores Y entirely)
|
||||
local function planOverlap(a, b)
|
||||
return math.abs(a.Position.X - b.Position.X) < (a.Size.X + b.Size.X) / 2
|
||||
and math.abs(a.Position.Z - b.Position.Z) < (a.Size.Z + b.Size.Z) / 2
|
||||
end
|
||||
|
||||
local out = {}
|
||||
|
||||
-- CONTROL 1: a road part must plan-overlap itself.
|
||||
local ctlSelf = (#roads > 0 and planOverlap(roads[1], roads[1])) and 1 or 0
|
||||
table.insert(out, ("CONTROL planOverlap-self=%d (want 1)"):format(ctlSelf))
|
||||
|
||||
-- CLEARANCE over the deck.
|
||||
local worst, worstAt, nOver = 1e9, "none", 0
|
||||
for _, cp in ipairs(cross) do
|
||||
for _, rd in ipairs(roads) do
|
||||
if planOverlap(cp, rd) then
|
||||
nOver += 1
|
||||
-- ramp underside minus road topside
|
||||
local gap = (cp.Position.Y - cp.Size.Y / 2) - (rd.Position.Y + rd.Size.Y / 2)
|
||||
if gap < worst then
|
||||
worst, worstAt = gap, cp.Name .. " over " .. rd.Name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if nOver == 0 then
|
||||
table.insert(out, "CROSSING: 0 road parts under any Cross part -- ramp does NOT span the deck")
|
||||
else
|
||||
table.insert(out, ("CROSSING: %d overlaps | worst vertical gap=%.1f at %s"):format(nOver, worst, worstAt))
|
||||
end
|
||||
|
||||
-- LANDING: last descent part of each ramp vs the road under it.
|
||||
local function tailOf(prefix)
|
||||
local best, bestIdx = nil, -1
|
||||
for _, p in ipairs(descent) do
|
||||
if p.Name:sub(1, #prefix) == prefix then
|
||||
local i = tonumber(p.Name:match("_(%d+)$")) or 0
|
||||
if i > bestIdx then
|
||||
best, bestIdx = p, i
|
||||
end
|
||||
end
|
||||
end
|
||||
return best
|
||||
end
|
||||
for _, pre in ipairs({ "PAEntryRampNear", "PAExitRampNear" }) do
|
||||
local t = tailOf(pre)
|
||||
if not t then
|
||||
table.insert(out, pre .. ": no tail part")
|
||||
else
|
||||
local bestGap, bestNm = 1e9, "none"
|
||||
for _, rd in ipairs(roads) do
|
||||
if planOverlap(t, rd) then
|
||||
local dy = (t.Position.Y - t.Size.Y / 2) - (rd.Position.Y + rd.Size.Y / 2)
|
||||
if math.abs(dy) < math.abs(bestGap) then
|
||||
bestGap, bestNm = dy, rd.Name
|
||||
end
|
||||
end
|
||||
end
|
||||
if bestNm == "none" then
|
||||
table.insert(out, ("%s LANDING: tail is over NO road -- lands off the carriageway"):format(pre))
|
||||
else
|
||||
table.insert(out, ("%s LANDING: dY=%.1f onto %s"):format(pre, bestGap, bestNm))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(out, ("counts cross=%d descent=%d roads=%d"):format(#cross, #descent, #roads))
|
||||
return table.concat(out, "\n")
|
||||
@@ -0,0 +1,119 @@
|
||||
-- GROUND-TRUTH PROBE for the floating-scenery finding.
|
||||
--
|
||||
-- probe_floating reported 664 parts with "nothing below". That number alone
|
||||
-- is ambiguous: it reads identically whether (a) the buildings genuinely
|
||||
-- hang in the air, or (b) the map has no ground/terrain plane at all, so
|
||||
-- NOTHING can ever return a hit. Distinguish before calling it a defect.
|
||||
--
|
||||
-- Measure:
|
||||
-- 1. Does the map contain terrain / a baseplate / a water or ground slab?
|
||||
-- 2. Bottom-face Y of each scenery part vs the road deck Y and vs 0.
|
||||
-- A building whose bottom is at/below ~0 is "planted" even with no
|
||||
-- ground mesh; one whose bottom is hundreds of studs up is FLOATING.
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local out = {}
|
||||
|
||||
-- 1. terrain / ground candidates
|
||||
local terr = ws:FindFirstChildOfClass("Terrain")
|
||||
local terrHas = false
|
||||
if terr then
|
||||
local ok, reg = pcall(function()
|
||||
return terr.MaxExtents
|
||||
end)
|
||||
-- MaxExtents is a Region3int16; its Min/Max are Vector3int16 which have
|
||||
-- no .Magnitude. Compare components instead.
|
||||
if ok and reg then
|
||||
local mn, mx = reg.Min, reg.Max
|
||||
terrHas = (mx.X - mn.X) > 1 or (mx.Y - mn.Y) > 1 or (mx.Z - mn.Z) > 1
|
||||
end
|
||||
end
|
||||
table.insert(out, ("Terrain present=%s hasVoxels=%s"):format(tostring(terr ~= nil), tostring(terrHas)))
|
||||
|
||||
local groundish = {}
|
||||
for _, d in ipairs(ws:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
local n = d.Name:lower()
|
||||
if
|
||||
n:find("ground")
|
||||
or n:find("base")
|
||||
or n:find("water")
|
||||
or n:find("sea")
|
||||
or n:find("bay")
|
||||
or n:find("floor")
|
||||
then
|
||||
local sz = d.Size
|
||||
if sz.X > 200 or sz.Z > 200 then
|
||||
table.insert(
|
||||
groundish,
|
||||
("%s size=%.0fx%.0f topY=%.1f"):format(d.Name, sz.X, sz.Z, d.Position.Y + sz.Y / 2)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
table.insert(out, ("large ground-like slabs=%d"):format(#groundish))
|
||||
for i = 1, math.min(#groundish, 6) do
|
||||
table.insert(out, " " .. groundish[i])
|
||||
end
|
||||
|
||||
-- 2. bottom-face Y of scenery vs deck
|
||||
local roadY, roadN = {}, 0
|
||||
local scen = {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
local nm = d.Name
|
||||
if nm:sub(1, 5) == "Road_" then
|
||||
roadN += 1
|
||||
table.insert(roadY, d.Position.Y)
|
||||
elseif
|
||||
nm:find("Building")
|
||||
or nm:find("Tower")
|
||||
or nm:find("Ginza")
|
||||
or nm:find("Shiodome")
|
||||
or nm:find("Diet")
|
||||
then
|
||||
table.insert(scen, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(roadY)
|
||||
local medRoad = roadY[math.max(1, math.floor(#roadY / 2))] or 0
|
||||
table.insert(out, ("median road Y=%.1f (n=%d)"):format(medRoad, roadN))
|
||||
|
||||
local planted, floatHigh = 0, 0
|
||||
local hist = {}
|
||||
local worst, worstNm, worstP = -1e9, "none", nil
|
||||
for _, p in ipairs(scen) do
|
||||
local btm = p.Position.Y - p.Size.Y / 2
|
||||
if btm <= 5 then
|
||||
planted += 1
|
||||
else
|
||||
floatHigh += 1
|
||||
local base = p.Name:gsub("_%d+$", "")
|
||||
hist[base] = (hist[base] or 0) + 1
|
||||
if btm > worst then
|
||||
worst, worstNm, worstP = btm, p.Name, p.Position
|
||||
end
|
||||
end
|
||||
end
|
||||
table.insert(out, ("scenery=%d | bottom<=5 (planted)=%d | bottom>5 (airborne)=%d"):format(#scen, planted, floatHigh))
|
||||
if worstP then
|
||||
table.insert(
|
||||
out,
|
||||
("HIGHEST bottom: %s btmY=%.0f at %.0f,%.0f,%.0f"):format(worstNm, worst, worstP.X, worstP.Y, worstP.Z)
|
||||
)
|
||||
end
|
||||
local hl = {}
|
||||
for k, v in pairs(hist) do
|
||||
table.insert(hl, ("%s=%d"):format(k, v))
|
||||
end
|
||||
table.sort(hl)
|
||||
table.insert(out, "airborne byName: " .. (#hl > 0 and table.concat(hl, " ") or "(none)"))
|
||||
|
||||
return table.concat(out, "\n")
|
||||
@@ -0,0 +1,109 @@
|
||||
-- What actually sits near the four ramp ENDS? The user reports ramps going
|
||||
-- through buildings; the clip probe only found pillars vs perimeter wall.
|
||||
-- So: name every non-drivable part within 140 studs of each ramp end, and
|
||||
-- separately list PA-area structures with their footprints, so the ramp
|
||||
-- can be re-aimed at a genuinely clear corridor instead of guessed at.
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
if not map then
|
||||
return "NO MAP"
|
||||
end
|
||||
|
||||
local WANT = {
|
||||
PAEntryRampNear_200 = true,
|
||||
PAExitRampNear_200 = true,
|
||||
PAEntryRampTrunk_1 = true,
|
||||
PAExitRampTrunk_1 = true,
|
||||
}
|
||||
|
||||
local ends = {}
|
||||
local all = {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
if WANT[d.Name] then
|
||||
ends[d.Name] = d.Position
|
||||
end
|
||||
table.insert(all, d)
|
||||
end
|
||||
end
|
||||
|
||||
local function drivable(n)
|
||||
return n:match("^Road_") or n:find("Ramp") or n == "PAFloor" or n == "Ground" or n:find("Water")
|
||||
end
|
||||
|
||||
local out = {}
|
||||
for n, p in pairs(ends) do
|
||||
local near = {}
|
||||
local closest, closeName = 1e9, "none"
|
||||
for _, d in ipairs(all) do
|
||||
if not drivable(d.Name) then
|
||||
local dist = (d.Position - p).Magnitude
|
||||
if dist < 140 then
|
||||
near[d.Name] = (near[d.Name] or 0) + 1
|
||||
if dist < closest then
|
||||
closest, closeName = dist, d.Name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local l = {}
|
||||
for k, v in pairs(near) do
|
||||
table.insert(l, ("%s x%d"):format(k, v))
|
||||
end
|
||||
table.sort(l)
|
||||
local keep = {}
|
||||
for i = 1, math.min(#l, 10) do
|
||||
table.insert(keep, l[i])
|
||||
end
|
||||
table.insert(
|
||||
out,
|
||||
("%s @%.0f,%.0f,%.0f closest=%s(%.0f) near: %s"):format(
|
||||
n,
|
||||
p.X,
|
||||
p.Y,
|
||||
p.Z,
|
||||
closeName,
|
||||
closest,
|
||||
table.concat(keep, ", ")
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
-- PA-area structures with footprints so we can aim at a clear corridor.
|
||||
table.insert(out, "--- PA structures (within 700 of PA centre) ---")
|
||||
local paf = nil
|
||||
for _, d in ipairs(all) do
|
||||
if d.Name == "PAFloor" then
|
||||
paf = d
|
||||
end
|
||||
end
|
||||
if paf then
|
||||
local c = paf.Position
|
||||
table.insert(out, ("PAFloor centre %.0f,%.0f,%.0f size %.0fx%.0f"):format(c.X, c.Y, c.Z, paf.Size.X, paf.Size.Z))
|
||||
local seen = {}
|
||||
local rows = {}
|
||||
for _, d in ipairs(all) do
|
||||
if not drivable(d.Name) and (d.Position - c).Magnitude < 700 then
|
||||
local base = d.Name:gsub("_%d+$", "")
|
||||
if not seen[base] then
|
||||
seen[base] = { minx = 1e9, maxx = -1e9, minz = 1e9, maxz = -1e9, n = 0 }
|
||||
end
|
||||
local s = seen[base]
|
||||
s.n += 1
|
||||
s.minx = math.min(s.minx, d.Position.X - d.Size.X / 2)
|
||||
s.maxx = math.max(s.maxx, d.Position.X + d.Size.X / 2)
|
||||
s.minz = math.min(s.minz, d.Position.Z - d.Size.Z / 2)
|
||||
s.maxz = math.max(s.maxz, d.Position.Z + d.Size.Z / 2)
|
||||
end
|
||||
end
|
||||
for k, s in pairs(seen) do
|
||||
table.insert(rows, ("%s n=%d X[%.0f..%.0f] Z[%.0f..%.0f]"):format(k, s.n, s.minx, s.maxx, s.minz, s.maxz))
|
||||
end
|
||||
table.sort(rows)
|
||||
for _, r in ipairs(rows) do
|
||||
table.insert(out, " " .. r)
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(out, "\n")
|
||||
@@ -0,0 +1,103 @@
|
||||
-- Geometry facts needed to re-cut the ramps so they (a) merge at the highway
|
||||
-- EDGE instead of landing on top of it, (b) run a corridor that misses the PA
|
||||
-- shops, and (c) get pole/beam supports instead of whatever's there now.
|
||||
--
|
||||
-- Outputs:
|
||||
-- 1. Mainline cross-section at the PA: per-carriageway edge X/Z + width, so
|
||||
-- a merge can target the OUTER edge rather than the deck centreline.
|
||||
-- 2. Plan-footprint of every shop model near the PA, as an X/Z box, so a
|
||||
-- clear corridor can be computed instead of guessed.
|
||||
-- 3. Current support part sizes/shapes, to see what needs replacing.
|
||||
|
||||
local ws = game:GetService("Workspace")
|
||||
local map = ws:FindFirstChild("Map")
|
||||
|
||||
local out = {}
|
||||
|
||||
-- 1. mainline cross-section near the PA (sample by Z band)
|
||||
local band = {}
|
||||
for _, d in ipairs(map:GetDescendants()) do
|
||||
if d:IsA("BasePart") and d.Name:match("^Road_") then
|
||||
local p = d.Position
|
||||
if p.Z > -7250 and p.Z < -7050 and p.X > -4700 and p.X < -3400 then
|
||||
table.insert(band, d)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(band, function(a, b)
|
||||
return a.Position.Z < b.Position.Z
|
||||
end)
|
||||
table.insert(out, ("mainline sample parts in band=%d"):format(#band))
|
||||
-- cluster by Z to find the two carriageways
|
||||
local zmin, zmax = 1e9, -1e9
|
||||
for _, d in ipairs(band) do
|
||||
zmin = math.min(zmin, d.Position.Z - d.Size.Z / 2)
|
||||
zmax = math.max(zmax, d.Position.Z + d.Size.Z / 2)
|
||||
end
|
||||
table.insert(out, ("mainline Z extent %.1f .. %.1f (total %.1f)"):format(zmin, zmax, zmax - zmin))
|
||||
for i = 1, math.min(#band, 6) do
|
||||
local d = band[i]
|
||||
table.insert(
|
||||
out,
|
||||
(" %s pos %.0f,%.1f,%.1f size %.1fx%.1f rotY=%.0f"):format(
|
||||
d.Name,
|
||||
d.Position.X,
|
||||
d.Position.Y,
|
||||
d.Position.Z,
|
||||
d.Size.X,
|
||||
d.Size.Z,
|
||||
math.deg(select(2, d.CFrame:ToEulerAnglesYXZ()))
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
-- 2. shop model footprints near the PA
|
||||
table.insert(out, "--- shop model footprints (plan boxes) ---")
|
||||
for _, top in ipairs(ws:GetChildren()) do
|
||||
if top ~= map and not top:IsA("Terrain") and not top:IsA("Camera") and not top:IsA("BasePart") then
|
||||
local x0, x1, z0, z1, y0, y1, n = 1e9, -1e9, 1e9, -1e9, 1e9, -1e9, 0
|
||||
for _, d in ipairs(top:GetDescendants()) do
|
||||
if d:IsA("BasePart") then
|
||||
n += 1
|
||||
x0 = math.min(x0, d.Position.X - d.Size.X / 2)
|
||||
x1 = math.max(x1, d.Position.X + d.Size.X / 2)
|
||||
z0 = math.min(z0, d.Position.Z - d.Size.Z / 2)
|
||||
z1 = math.max(z1, d.Position.Z + d.Size.Z / 2)
|
||||
y0 = math.min(y0, d.Position.Y - d.Size.Y / 2)
|
||||
y1 = math.max(y1, d.Position.Y + d.Size.Y / 2)
|
||||
end
|
||||
end
|
||||
if n > 0 and x0 > -6000 and x1 < -2500 and z0 > -9000 and z1 < -6000 then
|
||||
table.insert(
|
||||
out,
|
||||
(" %-42s n=%-5d X[%.0f..%.0f] Z[%.0f..%.0f] Y[%.0f..%.0f]"):format(top.Name, n, x0, x1, z0, z1, y0, y1)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 3. what the current supports look like
|
||||
table.insert(out, "--- current ramp supports ---")
|
||||
local sup = {}
|
||||
for _, d in ipairs(ws:GetDescendants()) do
|
||||
if d:IsA("BasePart") and (d.Name:find("RampPillar") or d.Name:find("PierColumn") or d.Name:find("PierCap")) then
|
||||
local k = ("%s shape=%s size=%.1fx%.1fx%.1f"):format(
|
||||
d.Name,
|
||||
d.Shape and d.Shape.Name or "n/a",
|
||||
d.Size.X,
|
||||
d.Size.Y,
|
||||
d.Size.Z
|
||||
)
|
||||
sup[k] = (sup[k] or 0) + 1
|
||||
end
|
||||
end
|
||||
local sl = {}
|
||||
for k, v in pairs(sup) do
|
||||
table.insert(sl, ("%s x%d"):format(k, v))
|
||||
end
|
||||
table.sort(sl)
|
||||
for i = 1, math.min(#sl, 12) do
|
||||
table.insert(out, " " .. sl[i])
|
||||
end
|
||||
|
||||
return table.concat(out, "\n")
|
||||
Reference in New Issue
Block a user