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.
141 lines
4.1 KiB
Luau
141 lines
4.1 KiB
Luau
--[[
|
|
diag_shellyaw.luau -- measure the ACTUAL shell orientation on LIVE cars.
|
|
|
|
Why this exists: the previous harness measured Wheel_* positions, which come
|
|
from the constant Chassis.WHEEL_MOTOR/WHEELBASE_HALF (5.9). Those are
|
|
identical for every car whether or not any fix applied, so the test passed
|
|
vacuously. The visible "backwards" car is the COSMETIC SHELL, which is a
|
|
separate model attached to the chassis -- that is what must be measured.
|
|
|
|
Runs against the running game (Server datamodel), inspecting cars that the
|
|
real spawn path actually produced. Reports, per live car:
|
|
|
|
bodyLook : chassis Body forward (should be the car's true nose direction)
|
|
shellLook : shell model forward
|
|
yawDelta : signed angle between them, degrees. ~0 = aligned,
|
|
~180 = shell rendering backwards relative to chassis.
|
|
|
|
Also dumps the shell's own internal cue (mean Z of its F*/R* wheel labels)
|
|
so we can see WHY the solver chose what it chose, and whether
|
|
SHELL_YAW_OVERRIDE actually took effect on this car.
|
|
]]
|
|
|
|
local out = {}
|
|
local function log(...)
|
|
local parts = {}
|
|
for i = 1, select("#", ...) do
|
|
parts[#parts + 1] = tostring((select(i, ...)))
|
|
end
|
|
table.insert(out, table.concat(parts, " "))
|
|
end
|
|
|
|
local function yawBetween(a: Vector3, b: Vector3): number
|
|
local av = Vector3.new(a.X, 0, a.Z)
|
|
local bv = Vector3.new(b.X, 0, b.Z)
|
|
if av.Magnitude < 1e-4 or bv.Magnitude < 1e-4 then
|
|
return 0 / 0
|
|
end
|
|
av, bv = av.Unit, bv.Unit
|
|
local dot = math.clamp(av:Dot(bv), -1, 1)
|
|
local ang = math.deg(math.acos(dot))
|
|
local cross = av:Cross(bv).Y
|
|
return (cross < 0) and -ang or ang
|
|
end
|
|
|
|
-- Find live cars anywhere in workspace (folder name varies).
|
|
local cars = {}
|
|
for _, d in ipairs(workspace:GetDescendants()) do
|
|
if d:IsA("Model") and d.Name:match("^Car_") then
|
|
cars[#cars + 1] = d
|
|
end
|
|
end
|
|
|
|
log(string.format("live Car_* models found: %d", #cars))
|
|
if #cars == 0 then
|
|
log("")
|
|
log("No spawned cars. Drive/spawn a car, or this is the wrong datamodel.")
|
|
return table.concat(out, "\n")
|
|
end
|
|
|
|
for _, model in ipairs(cars) do
|
|
log("")
|
|
log("=== " .. model.Name .. " ===")
|
|
|
|
local body = model:FindFirstChild("Body")
|
|
if not body or not body:IsA("BasePart") then
|
|
log(" no Body part -- cannot establish chassis frame")
|
|
continue
|
|
end
|
|
|
|
local bodyLook = body.CFrame.LookVector
|
|
log(string.format(" bodyLook = (%.2f, %.2f, %.2f)", bodyLook.X, bodyLook.Y, bodyLook.Z))
|
|
|
|
-- The shell: a Model child that is not the chassis scaffolding.
|
|
local shell = nil
|
|
for _, c in ipairs(model:GetChildren()) do
|
|
if c:IsA("Model") then
|
|
shell = c
|
|
break
|
|
end
|
|
end
|
|
|
|
if not shell then
|
|
log(" NO SHELL MODEL as a child -- cosmetic mesh may be parented elsewhere")
|
|
local kids = {}
|
|
for _, c in ipairs(model:GetChildren()) do
|
|
kids[#kids + 1] = c.ClassName .. ":" .. c.Name
|
|
end
|
|
log(" children: " .. table.concat(kids, ", "))
|
|
continue
|
|
end
|
|
|
|
local shellLook = shell:GetPivot().LookVector
|
|
log(string.format(" shell = %s", shell.Name))
|
|
log(string.format(" shellLook = (%.2f, %.2f, %.2f)", shellLook.X, shellLook.Y, shellLook.Z))
|
|
|
|
local delta = yawBetween(bodyLook, shellLook)
|
|
log(
|
|
string.format(
|
|
" yawDelta = %.1f deg --> %s",
|
|
delta,
|
|
(math.abs(delta) < 30) and "ALIGNED"
|
|
or (math.abs(math.abs(delta) - 180) < 30) and "BACKWARDS (180 flip)"
|
|
or "SKEWED"
|
|
)
|
|
)
|
|
|
|
-- Why: the shell's own wheel-label cue, in the shell's local frame.
|
|
local wf = shell:FindFirstChild("Wheels")
|
|
if wf then
|
|
local pivotInv = shell:GetPivot():Inverse()
|
|
local fS, fN, rS, rN = 0, 0, 0, 0
|
|
for _, d in ipairs(wf:GetDescendants()) do
|
|
if d:IsA("BasePart") then
|
|
local nm = d.Name:upper()
|
|
local z = (pivotInv * d.CFrame).Z
|
|
if nm:match("^F") then
|
|
fS, fN = fS + z, fN + 1
|
|
elseif nm:match("^R") then
|
|
rS, rN = rS + z, rN + 1
|
|
end
|
|
end
|
|
end
|
|
if fN > 0 and rN > 0 then
|
|
log(
|
|
string.format(
|
|
" shell wheel cue: frontZ=%.2f rearZ=%.2f (labels say nose at %s)",
|
|
fS / fN,
|
|
rS / rN,
|
|
(fS / fN < rS / rN) and "-Z" or "+Z"
|
|
)
|
|
)
|
|
else
|
|
log(string.format(" shell wheel cue: unusable (F parts=%d, R parts=%d)", fN, rN))
|
|
end
|
|
else
|
|
log(" shell has no Wheels folder -- no cue available")
|
|
end
|
|
end
|
|
|
|
return table.concat(out, "\n")
|