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
+96 -20
View File
@@ -155,7 +155,21 @@ end
-- Build one corner of the suspension. `isFront` corners get a steering servo.
-- All positions are in BODY-LOCAL space, converted via body.CFrame.
local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: number, zSign: number)
local isFront = zSign < 0 -- car forward is -Z
-- FORWARD CONVENTION: car forward is body-local -Z. This is the Roblox
-- standard -- CFrame.LookVector IS the -Z basis vector.
--
-- DO NOT "fix" this to `zSign > 0`. That was tried on 2026-08-02 and made
-- things strictly worse: 2 of 5 cars were backwards before, all 5 after.
-- It was justified by tools/diag_seatdir.luau reporting "throttle drives
-- toward local +Z", which was a SIGN ERROR in the diagnostic, not a fact
-- about the game: expressing a part's own LookVector in its own local
-- frame is (0,0,-1) BY DEFINITION, so the script mixed world and local
-- space and then "confirmed" the inversion it was looking for.
--
-- Lesson: a measurement that merely restates a definition cannot be
-- evidence. Before trusting a diagnostic, ask what result would DISPROVE
-- the hypothesis -- if no possible output would, the check is worthless.
local isFront = zSign < 0
local cornerName = (isFront and "F" or "R") .. (xSign < 0 and "L" or "R")
local wheelLocal = Vector3.new(xSign * Chassis.TRACK_HALF, -Chassis.SUSPENSION_DROP, zSign * Chassis.WHEELBASE_HALF)
@@ -247,6 +261,20 @@ local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: numb
local parentAtt = makeAttachment(wheelParent, "WheelAxleAtt", CFrame.new())
local wheelAtt = makeAttachment(wheel, "WheelAtt", CFrame.new())
-- STEERING/MOTOR CONFLICT -- UNRESOLVED, see docs note below.
--
-- A steered front corner is two hinges in series: Hub --SteerServo(Y)-->
-- Knuckle --WheelMotor(X)--> Wheel. Measured (tools/diag_steer4.luau,
-- gtr_r34, target -20 deg): the servo tracks to within 0.01 deg ONLY when
-- the front wheel is detached or its hinge disabled; with the wheel
-- attached it stays pinned near 0 deg even hoisted in free air with
-- ServoMaxTorque up to 1e9. So it is NOT grip and NOT a torque contest.
--
-- Do NOT "fix" this by skipping the front motor: the motor hinge is the
-- only joint attaching Wheel_F* to Knuckle_F*, so omitting it leaves the
-- front wheels physically detached (verified, tools/diag_frontjoint.luau)
-- -- the servo then turns freely because it carries no load, and the car
-- generates no front lateral force at all.
local motor = Instance.new("HingeConstraint")
motor.Name = Chassis.WHEEL_MOTOR_NAME
motor.Attachment0 = parentAtt
@@ -410,10 +438,22 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
-- Derive each shell's true forward from its OWN wheel names rather
-- than a hand-maintained per-car constant: F* wheels are the front
-- axle, R* the rear, so (front - rear) is the nose direction in the
-- shell's local frame. Solve the yaw that maps it onto -Z. This
-- also covers 90-degree-rotated shells, and self-corrects for any
-- shell added later. If the cue is missing or degenerate, rotate
-- nothing (fail soft to previous behaviour) rather than guess.
-- shell's local frame. Solve the yaw that maps it onto the chassis
-- forward axis. This also covers 90-degree-rotated shells, and
-- self-corrects for any shell added later. If the cue is missing or
-- degenerate, rotate nothing (fail soft) rather than guess.
--
-- HISTORY: a SHELL_YAW_OVERRIDE table used to pin hatch_junk and
-- hyper_x to yaw 0, on the theory that their wheel labels were
-- reversed. That was WRONG and has been removed. Measurement
-- (tools/diag_shellyaw.luau) showed shell-vs-body yaw was already
-- 0.0 degrees on those cars -- there was no shell rotation error at
-- all. The real defect was the chassis forward convention being
-- inverted against the VehicleSeat (see buildCorner, isFront), which
-- made every car drive out of its labelled rear. With that fixed,
-- an override forcing yaw 0 would REINTRODUCE a 180-degree flip.
-- Do not re-add per-car yaw constants without first running
-- diag_shellyaw.luau and showing a non-zero yawDelta.
local shellYaw = 0
local wheelsFolder = shell:FindFirstChild("Wheels")
if wheelsFolder then
@@ -421,10 +461,22 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
local fSum, fN = Vector3.zero, 0
local rSum, rN = Vector3.zero, 0
for _, w in ipairs(wheelsFolder:GetChildren()) do
local part = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
if part then
-- Use the group's CENTROID, not an arbitrary first BasePart:
-- FindFirstChildWhichIsA can return a rotor or caliper that
-- sits up to ~1 stud off the wheel centre (measured, see the
-- root-pick note in the wheel re-hang below).
local sum, n = Vector3.zero, 0
if w:IsA("BasePart") then
sum, n = w.Position, 1
end
for _, d in ipairs(w:GetDescendants()) do
if d:IsA("BasePart") then
sum, n = sum + d.Position, n + 1
end
end
if n > 0 then
local head = w.Name:sub(1, 1):upper()
local localPos = pivotInv * part.Position
local localPos = pivotInv * (sum / n)
if head == "F" then
fSum, fN = fSum + localPos, fN + 1
elseif head == "R" then
@@ -437,6 +489,8 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
-- Horizontal component only; a shell sitting nose-down must not
-- tilt the whole body.
if math.abs(nose.X) + math.abs(nose.Z) > 0.05 then
-- Map the shell's nose onto chassis forward, which is -Z
-- (see the forward-convention note in buildCorner).
shellYaw = -math.atan2(nose.X, -nose.Z)
end
end
@@ -572,19 +626,36 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
if wheelGroups then
for _, w in ipairs(wheelGroups:GetChildren()) do
local phys = model:FindFirstChild("Wheel_" .. w.Name:upper())
local root = w:IsA("BasePart") and w or w:FindFirstChildWhichIsA("BasePart", true)
local group, inGroup = {}, {}
if w:IsA("BasePart") then
group[#group + 1] = w
inGroup[w] = true
end
for _, d in ipairs(w:GetDescendants()) do
if d:IsA("BasePart") then
group[#group + 1] = d
inGroup[d] = true
end
end
-- ROOT MUST BE THE TYRE, so pick the largest-volume part in the
-- group. This previously used FindFirstChildWhichIsA, i.e. an
-- ARBITRARY first BasePart -- on the 18-part wheel groups that
-- lands on a rotor/caliper offset from the wheel centre. The
-- group is then translated to make root concentric with the
-- axle (see delta below), so a bad root displaces the visible
-- tyre by exactly that offset, which reads as wheels sticking
-- out past the bodywork. Measured root-to-centre offsets on the
-- templates (tools/diag_wheelroot2.luau): rx7_fc 0.955,
-- silv_s15 0.988, gtr_r34 0.285-0.310, hatch_junk 0.000-0.035
-- -- matching the report that the RX-7 was the obvious one.
local root, bestVol = nil, -1
for _, p in ipairs(group) do
local vol = p.Size.X * p.Size.Y * p.Size.Z
if vol > bestVol then
root, bestVol = p, vol
end
end
if phys and root then
local group, inGroup = {}, {}
if w:IsA("BasePart") then
group[#group + 1] = w
inGroup[w] = true
end
for _, d in ipairs(w:GetDescendants()) do
if d:IsA("BasePart") then
group[#group + 1] = d
inGroup[d] = true
end
end
-- Drop only the welds that tie this group to the OUTSIDE
-- (i.e. to the bodywork). Welds wholly inside the group are
-- the wheel's own construction and must survive.
@@ -968,4 +1039,9 @@ function CarManager.getRoot(player: Player): BasePart?
return model.PrimaryPart
end
-- Test seam: build a bare chassis with no player, seat or network ownership.
-- Used by tools/diag_steering.luau to measure handling headlessly. Not part of
-- the game path -- CarManager.spawn is the only way a player ever gets a car.
CarManager._buildForDiag = buildCarModel
return CarManager