Daikoku PA: lift mainline over the apron, rebuild both ramps

The PA ring road and the expressway mainline were both at ground level, so
the ring parapet stood INSIDE the carriageway (measured -10.0 studs of
vertical overlap: a wall in the middle of the road).

Reference photos of the real Daikoku Futo PA show the expressway flying
OVER the parking apron on pillars, with the PA at ground level and ramps
climbing up to meet it. Rebuilt to match:

- mainline lifted 38 studs through the junction, eased in/out (max 5.26%
  grade, within real expressway limits)
- PA ring + apron stay at ground level; legacy PARingPillar props removed
- both on/off ramps regraded over the full rise with smoothstep easing,
  segments overlapped 3 studs so wheels can't catch a seam, plus support
  pillars and edge kerbs where the deck is high

Verified in-engine against built parts (tools/diag_daikoku.luau), not just
source: clearance now +28.0 studs, both ramps continuous (max step 5.62,
no gaps). tools/plot_daikoku.py renders the plan/section for comparison.
This commit is contained in:
2026-08-02 15:56:13 -04:00
parent 8808b919c3
commit c8dabea53a
51 changed files with 6558 additions and 65 deletions
+215 -45
View File
@@ -335,11 +335,33 @@ local function planPA()
open[((centreIdx + k - 1) % n) + 1] = true
end
end
-- The OUTER edge barrier needs its own, much narrower opening -- and it is
-- NOT the same thing as the median opening.
--
-- BUG (2026-08-02): buildRoad reused `medianOpen` to skip the outer
-- barriers too, and did it inside `for side in {-1,1}`, so at all 13
-- samples per junction the mainline lost its parapet, railing AND kill
-- wall on BOTH edges -- 26 samples of expressway with no walls at all,
-- including the far side where no ramp exists. That is the "some of the
-- wangan has no walls, jumbled near the parking area" report.
--
-- `medianOpen` exists so both carriageways can reach the PA (the median is
-- the centre divider). The outer edge is a different structure with a drop
-- behind it, so it may only open on the ONE side the ramp leaves through,
-- and only right at the ramp mouth.
local RAMP_SPAN = 2 -- 5 samples, vs 13 for the median
local rampOpen = {}
for _, centreIdx in ipairs({ entryIdx, exitIdx }) do
for k = -RAMP_SPAN, RAMP_SPAN do
rampOpen[((centreIdx + k - 1) % n) + 1] = true
end
end
PAPlan = {
nearIdx = bestIdx,
entryIdx = entryIdx,
exitIdx = exitIdx,
medianOpen = open,
rampOpen = rampOpen,
ringRadius = Circuit.PA_RADIUS + 130,
}
return PAPlan
@@ -500,9 +522,26 @@ local function buildRoad(map: Folder)
-- backstop against tunnelling at high speed.
-- At a PA junction the barrier AND the kill wall must open on the side
-- the ramp leaves through, or the ramp would dead-end into a wall.
local junction = plan.medianOpen[i]
--
-- FIXED (2026-08-02): this used `plan.medianOpen[i]` and `continue`d
-- for BOTH sides, stripping barrier + rail + kill wall from 26 samples
-- of mainline on both edges. Two corrections:
-- * use `rampOpen` (5 samples) not `medianOpen` (13) -- the median
-- opening is a different, wider thing for a different purpose.
-- * only skip the ONE side the PA is actually on. The far edge of
-- the expressway has a drop behind it and must never open.
local junction = plan.rampOpen[i]
local paSide = 0
if junction then
-- Which side of the carriageway is the PA on? Positive dot with
-- `right` means the PA lies to the +1 side at this sample.
local toPA = Vector3.new(Circuit.PA_CENTER.X - mid.X, 0, Circuit.PA_CENTER.Z - mid.Z)
if toPA.Magnitude > 0.01 then
paSide = (toPA.Unit:Dot(right) >= 0) and 1 or -1
end
end
for _, side in ipairs({ -1, 1 }) do
if junction then
if junction and side == paSide then
continue
end
local boff = right * (side * (HALF - BARRIER_T / 2))
@@ -1072,6 +1111,68 @@ local function buildPA(map: Folder)
-- The ring itself
local ringY = c.Y
-- Ramp junctions, resolved BEFORE the ring is built so the ring wall can
-- open a gate at each one.
--
-- BUG (2026-08-02): the parapet loop below ran `for sgn in {1,-1}` over
-- EVERY segment, walling both edges of the ring with no opening anywhere.
-- The inner lip therefore sealed the parking apron completely: 72 of 72
-- rays cast outward from the SpawnPad hit a wall/barrier standing ~5.7
-- studs above the apron (tools/diag_pawall.luau), well over the ~2-3 studs
-- a car can climb -- so there was no way to drive out of the PA at all.
-- The mainline barrier already handles this correctly at PA junctions
-- ("the barrier AND the kill wall must open on the side the ramp leaves
-- through", see the deck loop above); the ring never got the same
-- treatment.
--
-- TWO gates are needed per junction, and both matter:
-- * INNER lip -- so the apron connects to the ring road at all.
-- * OUTER lip -- so the ring road connects to the ramp deck.
-- Opening only one still leaves the lot sealed.
--
-- The two gates are deliberately different sizes. Inside the ring the
-- apron is solid deck, so a wide opening is free. OUTSIDE the ring is a
-- ~46-stud drop to ground level, so that gate is kept tight and every
-- stud of it is backed by ramp/flare deck (see PARampFlare below) --
-- otherwise we would re-create the old "drove off the side and fell past
-- FallenPartsDestroyHeight" failure this structure was built to prevent.
local GATE_IN = 70 -- half-width of the apron-side opening
local GATE_OUT = 30 -- half-width of the ramp-side opening
local junctions = {}
for _, j in ipairs({
{ idx = plan.entryIdx, name = "PAOnRamp" },
{ idx = plan.exitIdx, name = "PAOffRamp" },
}) do
local road = samples[j.idx].pos
local toC = Vector3.new(c.X - road.X, 0, c.Z - road.Z)
if toC.Magnitude >= 1 then
local ringPt = Vector3.new(c.X, ringY, c.Z) - toC.Unit * RING_R
if (ringPt - road).Magnitude >= 4 then
-- roadY: the mainline deck height AT this junction. The road is
-- lifted over the PA (Circuit.PA_LIFT), so the ramp has to climb
-- to whatever the sample actually sits at -- reading it here
-- keeps ramp and road in lockstep if the lift profile changes.
table.insert(junctions, {
name = j.name,
road = road,
ringPt = ringPt,
roadY = road.Y,
})
end
end
end
local function nearJunction(pos, halfWidth)
for _, jn in ipairs(junctions) do
local d = Vector3.new(pos.X - jn.ringPt.X, 0, pos.Z - jn.ringPt.Z)
if d.Magnitude <= halfWidth then
return true
end
end
return false
end
for i = 0, ARC_N - 1 do
local a0 = (i / ARC_N) * math.pi * 2
local a1 = ((i + 1) / ARC_N) * math.pi * 2
@@ -1088,56 +1189,125 @@ local function buildPA(map: Folder)
local outN = (segMid - Vector3.new(c.X, ringY, c.Z)).Unit
for _, sgn in ipairs({ 1, -1 }) do
local ePos = segMid + outN * (sgn * RING_W / 2)
local eCF = CFrame.lookAt(ePos, ePos + fwd, UP)
local lip = part({
Name = "PARingWall",
Size = Vector3.new(2.2, BARRIER_H, segLen + 1),
CFrame = eCF * CFrame.new(0, 1 + BARRIER_H / 2, 0),
Color = C_PARAPET,
Material = Enum.Material.Concrete,
})
lip.Parent = pa
local lr = part({
Name = "PARingRail",
Size = Vector3.new(0.6, RAIL_H, segLen + 1),
CFrame = eCF * CFrame.new(0, 1 + BARRIER_H + RAIL_H / 2, 0),
Color = C_RAIL,
Material = Enum.Material.Metal,
})
lr.Parent = pa
-- sgn == 1 is the OUTER edge (away from the apron), sgn == -1 the
-- INNER edge facing the parking lot. Skip the parapet entirely
-- where a ramp tees in, or the ring seals the PA (see above).
local gate = (sgn == 1) and GATE_OUT or GATE_IN
if not nearJunction(ePos, gate) then
local eCF = CFrame.lookAt(ePos, ePos + fwd, UP)
local lip = part({
Name = "PARingWall",
Size = Vector3.new(2.2, BARRIER_H, segLen + 1),
CFrame = eCF * CFrame.new(0, 1 + BARRIER_H / 2, 0),
Color = C_PARAPET,
Material = Enum.Material.Concrete,
})
lip.Parent = pa
local lr = part({
Name = "PARingRail",
Size = Vector3.new(0.6, RAIL_H, segLen + 1),
CFrame = eCF * CFrame.new(0, 1 + BARRIER_H + RAIL_H / 2, 0),
Color = C_RAIL,
Material = Enum.Material.Metal,
})
lr.Parent = pa
end
end
-- Pillars holding the loop up over the parking lot, like the aerial.
if i % 6 == 0 and ringY > 12 then
local pil = part({
Name = "PARingPillar",
Size = Vector3.new(7, ringY, 7),
CFrame = CFrame.new(segMid.X, ringY / 2 - 1, segMid.Z),
Color = C_CONCRETE,
Material = Enum.Material.Concrete,
})
pil.Parent = pa
end
-- No pillars under the ring any more. The ring is now a GROUND-LEVEL
-- circulation road sharing the apron slab (Circuit.PA_LIFT raises the
-- MAINLINE over the PA instead of leaving both at y=46). It is the
-- expressway that flies overhead on pillars here, exactly as in the
-- reference photos: a flat lot with parked cars, elevated deck above.
-- Mainline pillars are built in buildRoad; see PA_LIFT.
end
-- Two ramps: mainline -> ring (entry) and ring -> mainline (exit).
for _, j in ipairs({
{ idx = plan.entryIdx, name = "PAOnRamp" },
{ idx = plan.exitIdx, name = "PAOffRamp" },
}) do
local road = samples[j.idx].pos
local toC = Vector3.new(c.X - road.X, 0, c.Z - road.Z)
if toC.Magnitude < 1 then
continue
-- `junctions` was resolved above (the ring wall needed it to place gates);
-- reuse it so the ramp deck and the wall opening can never disagree.
--
-- These are now GRADED: the ring sits on the apron at ringY while the
-- mainline flies Circuit.PA_LIFT studs overhead, so each ramp is a sloped
-- deck built as a stack of short segments that climb from the ring up to
-- the road. A single flat slab would leave a PA_LIFT-stud cliff at the
-- top -- undrivable in either direction.
local LIFT = Circuit.PA_LIFT or 0
for _, jn in ipairs(junctions) do
local span = (jn.ringPt - jn.road).Magnitude
-- The road end must meet the ACTUAL mainline deck height at that
-- junction (jn.roadY, captured from the sample), not a guess.
local yLo, yHi = ringY, jn.roadY or (ringY + LIFT)
local STEPS = math.max(8, math.floor(span / 40))
-- Ease the grade at both ends so a car doesn't nose into the
-- transition or launch off the crest.
local function ease(t)
return t * t * (3 - 2 * t)
end
-- Meet the ring at its nearest point to this junction, not the centre,
-- so the ramp tees into the loop instead of crossing over it.
local ringPt = Vector3.new(c.X, ringY, c.Z) - toC.Unit * RING_R
local span = (ringPt - road).Magnitude
if span < 4 then
continue
for s = 0, STEPS - 1 do
local t0, t1 = s / STEPS, (s + 1) / STEPS
local p0 = jn.ringPt:Lerp(jn.road, t0)
local p1 = jn.ringPt:Lerp(jn.road, t1)
local y0 = yLo + (yHi - yLo) * ease(t0)
local y1 = yLo + (yHi - yLo) * ease(t1)
local a = Vector3.new(p0.X, y0, p0.Z)
local b = Vector3.new(p1.X, y1, p1.Z)
local segMid = (a + b) / 2
-- Overlap consecutive segments slightly: a sloped deck is a
-- rotated box, so butt-jointed segments leave a thin wedge gap at
-- each seam that a wheel can catch on.
local segLen = (b - a).Magnitude + 3
local d = deck(jn.name, Vector3.new(RING_W, 2, segLen), CFrame.lookAt(segMid, b, UP))
d.Name = jn.name .. "_" .. s
-- Support pillar down to the apron under the raised part of the
-- ramp, matching the reference photos' elevated structure.
local h = segMid.Y - (ringY - 1)
if h > 10 and s % 2 == 0 then
local pil = part({
Name = "PARampPillar",
Size = Vector3.new(6, h, 6),
CFrame = CFrame.new(segMid.X, segMid.Y - h / 2, segMid.Z),
Color = C_CONCRETE,
Material = Enum.Material.Concrete,
})
pil.Parent = pa
end
-- Edge kerbs so a car can't slide off the side of a raised ramp.
if h > 8 then
local fwd2 = (b - a)
fwd2 = Vector3.new(fwd2.X, 0, fwd2.Z)
if fwd2.Magnitude > 0.01 then
local rightV = fwd2.Unit:Cross(UP)
for _, sg in ipairs({ -1, 1 }) do
local kerb = part({
Name = "PARampKerb",
Size = Vector3.new(1.6, 3.2, segLen),
CFrame = CFrame.lookAt(
segMid + rightV * (sg * RING_W / 2),
segMid + rightV * (sg * RING_W / 2) + (b - a).Unit,
UP
) * CFrame.new(0, 2.6, 0),
Color = C_PARAPET,
Material = Enum.Material.Concrete,
})
kerb.Parent = pa
end
end
end
end
-- Flare the ramp mouth across the outer gate so the opening in the
-- ring parapet is fully backed by deck -- the ramp meets the ring at a
-- tangent and would otherwise leave triangular gaps at the corners.
local toRing = Vector3.new(jn.ringPt.X - jn.road.X, 0, jn.ringPt.Z - jn.road.Z)
if toRing.Magnitude > 1 then
local flareCF = CFrame.lookAt(
Vector3.new(jn.ringPt.X, ringY, jn.ringPt.Z),
Vector3.new(jn.road.X, ringY, jn.road.Z),
UP
)
local flare = deck("PARampFlare", Vector3.new(GATE_OUT * 2 + RING_W, 2, RING_W + 24), flareCF)
flare.Color = Color3.fromRGB(38, 38, 44)
end
deck(j.name, Vector3.new(RING_W, 2, span), CFrame.lookAt((ringPt + road) / 2, road, UP))
end
-- The PA apron used to be a bare 520x520 slab floating ~46 studs up with