Rework the PA junction to match the reference drawing: yellow = PA -> highway (entry), red = highway -> PA (exit). Entry and exit swap sides (entry now EAST, exit WEST) and both trunks hug the PA perimeter instead of running concentric spirals. The simplification: both carriageways sit at the same deck level, adjacent, so the median opening IS the far-side access path. The under-deck crossing and merge runs are deleted outright -- 8 runs collapse to 4, and the girder-soffit ceiling logic that constrained the crossing goes with them. Part count 24,210 -> 21,691. The median opening is now load-bearing rather than cosmetic (it is the only route from the far carriageway to the ramps), so it widens from 6 to 9 samples per junction. Orientation is a single flip-able constant (ORIENT) so a mirrored result is a one-line correction, not a rewrite. Measured on the built map (not asserted): - 600 entry + 600 exit ramp parts - max grade 8.00% / 8.27%, under the 9% limit - 0 collisions with piers/masts/girders (558 structural parts) - segment continuity <= 2.5 studs; trunk->merge handoff <= 1.7 studs - merge lands at dY 0.0 (exact deck level), 33 studs lateral - median open at junctions: 0 parts vs. control ~7 on plain mainline Two traps worth recording, both caught by controls: 1. Studio's require() cache silently rebuilt STALE code. The first "passing" build reported 17,575 parts and zero ramps, and the map contained PAOffRamp/PAOnRamp parts whose names exist nowhere in the current source. Every measurement taken before this was describing an old map. Added tools/rebuild_fresh.luau, which clones the module to bust the cache; use it rather than requiring MapBuilder directly. 2. A reported "median never opens" bug was a bad probe, not a defect. It counted `Barrier` (the outer edge parapet, which must NEVER open) instead of `Median_<i>`. Re-run against the right name: control 7, junctions 0. Both were only visible because the probes carry controls -- a 0-hit result with no control is vacuous. The verification harnesses are kept in tools/ with their controls built in.
162 lines
5.4 KiB
Luau
162 lines
5.4 KiB
Luau
--!strict
|
|
-- Verify the rebuilt Daikoku PA junction IN-ENGINE, after MapBuilder has run.
|
|
--
|
|
-- MEASURE FIRST (project rule): ray tests and code reasoning both mislead;
|
|
-- actual part positions are ground truth. This reads the built parts back out
|
|
-- of Workspace rather than trusting the source that generated them.
|
|
--
|
|
-- Checks:
|
|
-- 1. No PA ring/parapet part intrudes into the mainline carriageway.
|
|
-- 2. Mainline deck clears the ring/apron by a usable height.
|
|
-- 3. Both ramps form a continuous, climbable chain (no cliff, no gap).
|
|
-- 4. The old PARingPillar parts are gone.
|
|
local ws = workspace
|
|
|
|
local function findFirst(name: string): Instance?
|
|
return ws:FindFirstChild(name, true)
|
|
end
|
|
|
|
local out = {}
|
|
local function say(s: string)
|
|
table.insert(out, s)
|
|
end
|
|
|
|
-- Locate the PA + road containers -------------------------------------------
|
|
local pa = findFirst("ParkingArea") or findFirst("PA") or findFirst("DaikokuPA")
|
|
local road = findFirst("Road")
|
|
local struct = findFirst("Structure") or findFirst("Struct")
|
|
|
|
say("== CONTAINERS ==")
|
|
say(
|
|
("pa=%s road=%s struct=%s"):format(
|
|
pa and pa:GetFullName() or "NIL",
|
|
road and road:GetFullName() or "NIL",
|
|
struct and struct:GetFullName() or "NIL"
|
|
)
|
|
)
|
|
|
|
-- Gather parts by name pattern anywhere under Workspace ----------------------
|
|
local buckets: { [string]: { BasePart } } = {}
|
|
local function bucket(key: string, p: BasePart)
|
|
buckets[key] = buckets[key] or {}
|
|
table.insert(buckets[key], p)
|
|
end
|
|
|
|
for _, d in ipairs(ws:GetDescendants()) do
|
|
if d:IsA("BasePart") then
|
|
local n = d.Name
|
|
if n:match("^PARingWall") or n:match("^PARingRail") then
|
|
bucket("ringwall", d)
|
|
elseif n:match("^PARingPillar") then
|
|
bucket("ringpillar", d)
|
|
elseif n:match("^PARamp") and n:match("Pillar") then
|
|
bucket("ramppillar", d)
|
|
elseif n:match("^PARampKerb") then
|
|
bucket("rampkerb", d)
|
|
elseif n:match("^PAOnRamp") then
|
|
bucket("entryramp", d)
|
|
elseif n:match("^PAOffRamp") then
|
|
bucket("exitramp", d)
|
|
elseif n:match("^Road_") then
|
|
bucket("roaddeck", d)
|
|
end
|
|
end
|
|
end
|
|
|
|
say("")
|
|
say("== PART COUNTS ==")
|
|
for _, k in ipairs({ "roaddeck", "ringwall", "ringpillar", "entryramp", "exitramp", "ramppillar", "rampkerb" }) do
|
|
say(("%-12s %d"):format(k, buckets[k] and #buckets[k] or 0))
|
|
end
|
|
|
|
-- 4. Old ring pillars must be gone ------------------------------------------
|
|
say("")
|
|
say("== CHECK: legacy ring pillars removed ==")
|
|
local rp = buckets["ringpillar"]
|
|
if rp and #rp > 0 then
|
|
say(("FAIL: %d PARingPillar parts still present (ring should be ground-level)"):format(#rp))
|
|
else
|
|
say("PASS: no PARingPillar parts (ring is ground-level)")
|
|
end
|
|
|
|
-- 1 + 2. Ring structures vs mainline carriageway -----------------------------
|
|
say("")
|
|
say("== CHECK: ring walls do not intrude into the carriageway ==")
|
|
local decks = buckets["roaddeck"] or {}
|
|
local walls = buckets["ringwall"] or {}
|
|
local worstGap, worstDesc = math.huge, "n/a"
|
|
local intrusions = 0
|
|
for _, w in ipairs(walls) do
|
|
local wTop = w.Position.Y + w.Size.Y / 2
|
|
for _, dk in ipairs(decks) do
|
|
local dxz = (Vector3.new(dk.Position.X, 0, dk.Position.Z) - Vector3.new(w.Position.X, 0, w.Position.Z)).Magnitude
|
|
-- Only decks whose footprint actually overlaps this wall matter.
|
|
if dxz < (dk.Size.X / 2 + w.Size.X / 2 + 30) then
|
|
local deckBottom = dk.Position.Y - dk.Size.Y / 2
|
|
local gap = deckBottom - wTop
|
|
if gap < worstGap then
|
|
worstGap = gap
|
|
worstDesc = ("%s vs %s dxz=%.1f"):format(dk.Name, w.Name, dxz)
|
|
end
|
|
if gap < 0 then
|
|
intrusions += 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if worstGap == math.huge then
|
|
say("PASS: no ring wall shares a footprint with any mainline deck")
|
|
else
|
|
say(("closest ring-wall/deck pair: %s"):format(worstDesc))
|
|
say(("vertical gap = %.1f studs"):format(worstGap))
|
|
if intrusions > 0 then
|
|
say(("FAIL: %d wall/deck pairs overlap vertically -- wall in the road"):format(intrusions))
|
|
else
|
|
say("PASS: every overlapping ring wall passes UNDER the deck")
|
|
end
|
|
end
|
|
|
|
-- 3. Ramp continuity ---------------------------------------------------------
|
|
local function checkRamp(key: string, label: string)
|
|
say("")
|
|
say(("== CHECK: %s continuity =="):format(label))
|
|
local segs = buckets[key]
|
|
if not segs or #segs == 0 then
|
|
say("FAIL: no segments found")
|
|
return
|
|
end
|
|
table.sort(segs, function(a, b)
|
|
local ai = tonumber(a.Name:match("_(%d+)$")) or 0
|
|
local bi = tonumber(b.Name:match("_(%d+)$")) or 0
|
|
return ai < bi
|
|
end)
|
|
local loY = segs[1].Position.Y
|
|
local hiY = segs[#segs].Position.Y
|
|
say(("%d segments, Y from %.1f to %.1f (rise %.1f)"):format(#segs, loY, hiY, hiY - loY))
|
|
local maxStep, maxGap = 0, 0
|
|
for i = 2, #segs do
|
|
local a, b = segs[i - 1], segs[i]
|
|
local step = math.abs(b.Position.Y - a.Position.Y)
|
|
local horiz = (Vector3.new(a.Position.X, 0, a.Position.Z) - Vector3.new(b.Position.X, 0, b.Position.Z)).Magnitude
|
|
-- gap = horizontal distance minus the two half-lengths that face
|
|
-- each other; negative means the segments overlap (good).
|
|
local gap = horiz - (a.Size.Z / 2 + b.Size.Z / 2)
|
|
maxStep = math.max(maxStep, step)
|
|
maxGap = math.max(maxGap, gap)
|
|
end
|
|
say(("max vertical step between segments = %.2f studs"):format(maxStep))
|
|
say(("max horizontal gap between segments = %.2f studs (negative = overlap)"):format(maxGap))
|
|
if maxStep > 6 then
|
|
say("FAIL: a step that large is a cliff a car cannot climb")
|
|
elseif maxGap > 0.5 then
|
|
say("FAIL: segments do not meet -- wheels will drop into the seam")
|
|
else
|
|
say("PASS: ramp is continuous and climbable")
|
|
end
|
|
end
|
|
|
|
checkRamp("entryramp", "entry ramp")
|
|
checkRamp("exitramp", "exit ramp")
|
|
|
|
return table.concat(out, "\n")
|