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.
183 lines
5.0 KiB
Luau
183 lines
5.0 KiB
Luau
--[[
|
|
diag_forward.luau -- settle which end of the car is the NOSE.
|
|
|
|
Contradiction to resolve: the chassis places its FRONT axle at local -Z
|
|
(WHEELBASE_HALF = 5.9, frontZ = -5.90) but Body.LookVector is +Z. Those
|
|
disagree: the part the code calls "front" is BEHIND the direction the body
|
|
looks. One of the two conventions is wrong, and the shell is being aligned
|
|
to the wrong one -- which is why the car renders backwards even though
|
|
shell and body agree (yawDelta 0).
|
|
|
|
Ground truth used here, in order of reliability:
|
|
|
|
1. VehicleSeat.LookVector -- in Roblox a seated driver faces the seat's
|
|
+Z look direction and positive throttle drives that way. This is the
|
|
direction the car ACTUALLY moves, so it defines "forward" for gameplay.
|
|
2. Steering: which wheels have a steering constraint. Only the FRONT
|
|
wheels steer, so a steered wheel's local Z tells us where the front is.
|
|
3. Shell art extents: the visual mesh bounding box relative to its pivot.
|
|
|
|
Reports each independently so a disagreement is visible rather than assumed.
|
|
]]
|
|
|
|
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 cars = {}
|
|
for _, d in ipairs(workspace:GetDescendants()) do
|
|
if d:IsA("Model") and d.Name:match("^Car_") then
|
|
cars[#cars + 1] = d
|
|
end
|
|
end
|
|
|
|
if #cars == 0 then
|
|
return "No live Car_* models."
|
|
end
|
|
|
|
for _, model in ipairs(cars) do
|
|
log("=== " .. model.Name .. " ===")
|
|
|
|
local body = model:FindFirstChild("Body")
|
|
if not (body and body:IsA("BasePart")) then
|
|
log(" no Body part")
|
|
continue
|
|
end
|
|
local bodyCF = body.CFrame
|
|
local inv = bodyCF:Inverse()
|
|
|
|
log(
|
|
string.format(
|
|
" bodyLook (world) = (%.2f, %.2f, %.2f)",
|
|
bodyCF.LookVector.X,
|
|
bodyCF.LookVector.Y,
|
|
bodyCF.LookVector.Z
|
|
)
|
|
)
|
|
|
|
-- 1. VehicleSeat: the direction the car actually drives.
|
|
local seat = nil
|
|
for _, d in ipairs(model:GetDescendants()) do
|
|
if d:IsA("VehicleSeat") then
|
|
seat = d
|
|
break
|
|
end
|
|
end
|
|
if seat then
|
|
local seatLocalZ = (inv * seat.CFrame).Z
|
|
local dot = bodyCF.LookVector:Dot(seat.CFrame.LookVector)
|
|
log(
|
|
string.format(
|
|
" [1] VehicleSeat: localZ=%.2f, seatLook.dot(bodyLook)=%.2f -> seat faces %s body",
|
|
seatLocalZ,
|
|
dot,
|
|
(dot > 0) and "SAME as" or "OPPOSITE to"
|
|
)
|
|
)
|
|
log(string.format(" => car drives toward body %s Z", (dot > 0) and "+" or "-"))
|
|
else
|
|
log(" [1] no VehicleSeat found")
|
|
end
|
|
|
|
-- 2. Which wheels steer? Only the front axle has a steering servo.
|
|
local steered, plain = {}, {}
|
|
for _, d in ipairs(model:GetDescendants()) do
|
|
if d:IsA("BasePart") and d.Name:match("^Wheel_") then
|
|
local z = (inv * d.CFrame).Z
|
|
local hasServo = false
|
|
for _, c in ipairs(d:GetDescendants()) do
|
|
if c:IsA("HingeConstraint") and c.ActuatorType == Enum.ActuatorType.Servo then
|
|
hasServo = true
|
|
break
|
|
end
|
|
end
|
|
-- servo may live on the attachment's parent chain instead
|
|
if not hasServo and d.Parent then
|
|
for _, c in ipairs(d.Parent:GetChildren()) do
|
|
if c:IsA("HingeConstraint") and c.ActuatorType == Enum.ActuatorType.Servo then
|
|
local a1, a0 = c.Attachment1, c.Attachment0
|
|
if (a1 and a1:IsDescendantOf(d)) or (a0 and a0:IsDescendantOf(d)) then
|
|
hasServo = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
table.insert(hasServo and steered or plain, { name = d.Name, z = z })
|
|
end
|
|
end
|
|
|
|
local function meanZ(t)
|
|
if #t == 0 then
|
|
return 0 / 0
|
|
end
|
|
local s = 0
|
|
for _, v in ipairs(t) do
|
|
s = s + v.z
|
|
end
|
|
return s / #t
|
|
end
|
|
|
|
local sz, pz = meanZ(steered), meanZ(plain)
|
|
log(string.format(" [2] steered wheels=%d meanZ=%.2f | unsteered=%d meanZ=%.2f", #steered, sz, #plain, pz))
|
|
if #steered > 0 and #plain > 0 then
|
|
log(string.format(" => FRONT (steered) axle is at body %s Z", (sz < pz) and "-" or "+"))
|
|
end
|
|
for _, v in ipairs(steered) do
|
|
log(string.format(" steered: %-14s z=%.2f", v.name, v.z))
|
|
end
|
|
for _, v in ipairs(plain) do
|
|
log(string.format(" plain : %-14s z=%.2f", v.name, v.z))
|
|
end
|
|
|
|
-- 3. Shell art: where is the visual mass, and is it symmetric?
|
|
local shell = nil
|
|
for _, c in ipairs(model:GetChildren()) do
|
|
if c:IsA("Model") then
|
|
shell = c
|
|
break
|
|
end
|
|
end
|
|
if shell then
|
|
local cf, size = shell:GetBoundingBox()
|
|
local localCentre = (inv * cf.Position)
|
|
log(
|
|
string.format(
|
|
" [3] shell bbox size=(%.1f, %.1f, %.1f) centre localZ=%.2f",
|
|
size.X,
|
|
size.Y,
|
|
size.Z,
|
|
localCentre.Z
|
|
)
|
|
)
|
|
-- named bodywork gives an independent nose/tail cue
|
|
for _, d in ipairs(shell:GetDescendants()) do
|
|
local n = d.Name:lower()
|
|
if
|
|
d:IsA("BasePart")
|
|
and (
|
|
n:match("hood")
|
|
or n:match("bonnet")
|
|
or n:match("boot")
|
|
or n:match("trunk")
|
|
or n:match("spoiler")
|
|
or n:match("windscreen")
|
|
or n:match("bumper")
|
|
)
|
|
then
|
|
log(string.format(" %-16s localZ=%.2f", d.Name, (inv * d.CFrame).Z))
|
|
end
|
|
end
|
|
else
|
|
log(" [3] no shell model child")
|
|
end
|
|
log("")
|
|
end
|
|
|
|
return table.concat(out, "\n")
|