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
113 lines
4.3 KiB
Luau
113 lines
4.3 KiB
Luau
-- 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, " | "),
|
|
})
|