CarBody: sculpted per-car visual body kits (cars-first priority)
CI / quality (push) Successful in 1m5s

- New src/server/CarBody.luau: wedge noses, raked glass, pop-up lights (RX-7),
  spoilers (lip/wing/GT), tail styles, exhaust tips, underglow, accent neon,
  alloy rims. All parts Massless+welded: zero physics impact.
- CarManager: replaced the old glass-cube cabin with CarBody.build().
This commit is contained in:
2026-07-22 08:50:19 -04:00
parent 0fc0d0ae22
commit e0bee80795
2 changed files with 451 additions and 14 deletions
+444
View File
@@ -0,0 +1,444 @@
--!strict
--[[
CarBody.luau (SERVER)
Procedural car BODYWORK — the visual layer on top of the constraint chassis.
The user's #1 priority: the cars must look the best of anything in the game.
Until real meshes are imported (docs/MESH_SWAP.md), this module builds each
car a sculpted, recognisable silhouette out of parts/wedges:
- low wedge nose + windshield/rear glass raked with WedgeParts
- per-car style: pop-up era lights, spoilers/wings, hatch vs coupe tail
- neon underglow, light bars, twin/quad exhausts, styled rims
EVERY part here is visual-only: Massless, CanCollide=false, welded to the
Body. Physics/tuning are 100% unchanged by anything in this file — deleting
the whole visual set leaves the same drivable chassis.
Contract with CarManager: CarBody.build(model, body, car) after the body
part exists. The mesh swap later replaces THIS module's output only.
]]
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Shared = ReplicatedStorage.Shared
local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis)
type Car = Cars.Car
local CarBody = {}
-- Per-car styling table --------------------------------------------------------
-- Anything absent falls back to Default. Colors here override car.Color-derived
-- accents only; the main paint always comes from car.Color so the catalogue
-- stays the single source of truth.
type Style = {
NoseKind: "wedge" | "flat", -- wedge = sports car nose
PopUpLights: boolean, -- 90s pop-up style light pods (RX-7!)
Spoiler: "none" | "lip" | "wing" | "gt", -- rear aero
TailKind: "coupe" | "hatch" | "kammback",
ExhaustCount: number, -- 1, 2 or 4 tips
Underglow: Color3?, -- nil = no underglow
AccentNeon: Color3?, -- rim/trim neon accent
RoofScoop: boolean,
}
local DEFAULT_STYLE: Style = {
NoseKind = "flat",
PopUpLights = false,
Spoiler = "none",
TailKind = "coupe",
ExhaustCount = 1,
Underglow = nil,
AccentNeon = nil,
RoofScoop = false,
}
local STYLES: { [string]: Style } = {
rx7_fc = {
NoseKind = "wedge",
PopUpLights = true, -- the FD's signature
Spoiler = "lip",
TailKind = "coupe",
ExhaustCount = 2,
Underglow = Color3.fromRGB(255, 60, 60),
AccentNeon = Color3.fromRGB(255, 90, 90),
RoofScoop = false,
},
hatch_junk = {
NoseKind = "flat",
PopUpLights = false,
Spoiler = "none",
TailKind = "hatch",
ExhaustCount = 1,
Underglow = nil,
AccentNeon = nil,
RoofScoop = false,
},
silv_s15 = {
NoseKind = "wedge",
PopUpLights = false,
Spoiler = "wing",
TailKind = "coupe",
ExhaustCount = 1,
Underglow = Color3.fromRGB(70, 140, 255),
AccentNeon = Color3.fromRGB(90, 160, 255),
RoofScoop = false,
},
gtr_r34 = {
NoseKind = "flat",
PopUpLights = false,
Spoiler = "gt",
TailKind = "kammback",
ExhaustCount = 2,
Underglow = Color3.fromRGB(80, 200, 255),
AccentNeon = Color3.fromRGB(120, 220, 255),
RoofScoop = false,
},
hyper_x = {
NoseKind = "wedge",
PopUpLights = false,
Spoiler = "gt",
TailKind = "kammback",
ExhaustCount = 4,
Underglow = Color3.fromRGB(190, 90, 255),
AccentNeon = Color3.fromRGB(210, 120, 255),
RoofScoop = true,
},
}
-- Helpers ----------------------------------------------------------------------
local function weldTo(body: BasePart, part: BasePart)
local weld = Instance.new("WeldConstraint")
weld.Part0 = body
weld.Part1 = part
weld.Parent = body
end
local function vis(props: {
Name: string,
Size: Vector3,
CFrame: CFrame,
Color: Color3,
Material: Enum.Material?,
Parent: Instance,
Wedge: boolean?,
Transparency: number?,
}): BasePart
local part: BasePart
if props.Wedge then
part = Instance.new("WedgePart")
else
part = Instance.new("Part")
end
part.Name = props.Name
part.Size = props.Size
part.CFrame = props.CFrame
part.Color = props.Color
part.Material = props.Material or Enum.Material.SmoothPlastic
part.Transparency = props.Transparency or 0
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
part.Massless = true -- CRITICAL: zero effect on the chassis physics
part.Parent = props.Parent
return part
end
-- Body geometry constants (derived from the chassis so proportions track) ------
local L = Chassis.BODY_SIZE.Z -- 12 length
local W = Chassis.BODY_SIZE.X -- 6 width
local H = Chassis.BODY_SIZE.Y -- 1.4 base slab height
-- Car forward is -Z (matches CarManager/buildCorner).
-- Section builders --------------------------------------------------------------
local function buildNose(folder: Folder, body: BasePart, paint: Color3, dark: Color3, style: Style)
local origin = body.CFrame
if style.NoseKind == "wedge" then
-- Sloped hood: wedge on top of the front slab, pointing forward-down.
vis({
Name = "Hood",
Size = Vector3.new(W - 0.4, 1.0, L * 0.32),
-- WedgePart slope faces +Z; rotate 180° yaw so it slopes toward -Z (front).
CFrame = origin * CFrame.new(0, H / 2 + 0.5, -L * 0.31) * CFrame.Angles(0, math.pi, 0),
Color = paint,
Parent = folder,
Wedge = true,
})
else
vis({
Name = "Hood",
Size = Vector3.new(W - 0.4, 0.8, L * 0.3),
CFrame = origin * CFrame.new(0, H / 2 + 0.4, -L * 0.3),
Color = paint,
Parent = folder,
})
end
-- Front bumper lip
vis({
Name = "FrontBumper",
Size = Vector3.new(W, 0.7, 0.6),
CFrame = origin * CFrame.new(0, -0.1, -L / 2 + 0.1),
Color = dark,
Parent = folder,
})
-- Headlights
if style.PopUpLights then
for _, xSign in ipairs({ -1, 1 }) do
-- Raised pod
vis({
Name = "LightPod",
Size = Vector3.new(1.2, 0.35, 1.1),
CFrame = origin * CFrame.new(xSign * (W / 2 - 1.1), H / 2 + 0.55, -L * 0.36),
Color = paint,
Parent = folder,
})
-- Lens (neon so they glow at night)
vis({
Name = "Headlight",
Size = Vector3.new(1.0, 0.25, 0.3),
CFrame = origin * CFrame.new(xSign * (W / 2 - 1.1), H / 2 + 0.55, -L * 0.36 - 0.55),
Color = Color3.fromRGB(255, 245, 210),
Material = Enum.Material.Neon,
Parent = folder,
})
end
else
for _, xSign in ipairs({ -1, 1 }) do
vis({
Name = "Headlight",
Size = Vector3.new(1.4, 0.4, 0.25),
CFrame = origin * CFrame.new(xSign * (W / 2 - 1.2), 0.35, -L / 2 + 0.05),
Color = Color3.fromRGB(255, 245, 210),
Material = Enum.Material.Neon,
Parent = folder,
})
end
end
end
local function buildGreenhouse(folder: Folder, body: BasePart, paint: Color3, glass: Color3, style: Style)
local origin = body.CFrame
local roofY = H / 2 + 1.5
local roofLen = L * 0.26
-- Windshield: raked wedge (slope toward front)
vis({
Name = "Windshield",
Size = Vector3.new(W - 1.0, 1.3, 1.9),
CFrame = origin * CFrame.new(0, H / 2 + 0.85 + 0.5, -L * 0.09) * CFrame.Angles(0, math.pi, 0),
Color = glass,
Material = Enum.Material.Glass,
Transparency = 0.25,
Parent = folder,
Wedge = true,
})
-- Roof
vis({
Name = "Roof",
Size = Vector3.new(W - 1.2, 0.25, roofLen),
CFrame = origin * CFrame.new(0, roofY + 0.3, L * 0.08),
Color = paint,
Parent = folder,
})
if style.RoofScoop then
vis({
Name = "RoofScoop",
Size = Vector3.new(1.2, 0.4, 1.4),
CFrame = origin * CFrame.new(0, roofY + 0.6, L * 0.05),
Color = Color3.fromRGB(20, 20, 22),
Parent = folder,
})
end
-- Rear glass: wedge sloping back
local rearGlassLen = if style.TailKind == "hatch" then 2.4 else 1.7
vis({
Name = "RearGlass",
Size = Vector3.new(W - 1.2, 1.3, rearGlassLen),
CFrame = origin * CFrame.new(0, H / 2 + 0.85 + 0.5, L * 0.235),
Color = glass,
Material = Enum.Material.Glass,
Transparency = 0.25,
Parent = folder,
Wedge = true,
})
-- Side skirts
for _, xSign in ipairs({ -1, 1 }) do
vis({
Name = "SideSkirt",
Size = Vector3.new(0.3, 0.5, L * 0.55),
CFrame = origin * CFrame.new(xSign * (W / 2 - 0.1), -0.45, 0),
Color = paint,
Parent = folder,
})
end
end
local function buildTail(folder: Folder, body: BasePart, paint: Color3, dark: Color3, style: Style, accent: Color3?)
local origin = body.CFrame
-- Trunk deck
local deckLen = L * 0.18
vis({
Name = "TrunkDeck",
Size = Vector3.new(W - 0.4, 0.6, deckLen),
CFrame = origin * CFrame.new(0, H / 2 + 0.3, L * 0.41),
Color = paint,
Parent = folder,
})
-- Rear bumper
vis({
Name = "RearBumper",
Size = Vector3.new(W, 0.7, 0.6),
CFrame = origin * CFrame.new(0, -0.1, L / 2 - 0.1),
Color = dark,
Parent = folder,
})
-- Full-width taillight bar (modern JDM night look, reads great with bloom)
vis({
Name = "TaillightBar",
Size = Vector3.new(W - 0.8, 0.32, 0.2),
CFrame = origin * CFrame.new(0, H / 2 + 0.35, L / 2 - 0.02),
Color = Color3.fromRGB(255, 40, 40),
Material = Enum.Material.Neon,
Parent = folder,
})
-- Spoiler
if style.Spoiler == "lip" then
vis({
Name = "Spoiler",
Size = Vector3.new(W - 1.0, 0.22, 0.7),
CFrame = origin * CFrame.new(0, H / 2 + 0.72, L * 0.46),
Color = paint,
Parent = folder,
})
elseif style.Spoiler == "wing" or style.Spoiler == "gt" then
local wingH = if style.Spoiler == "gt" then 1.15 else 0.85
for _, xSign in ipairs({ -1, 1 }) do
vis({
Name = "WingStrut",
Size = Vector3.new(0.2, wingH, 0.5),
CFrame = origin * CFrame.new(xSign * (W / 2 - 1.0), H / 2 + wingH / 2 + 0.3, L * 0.44),
Color = dark,
Parent = folder,
})
end
vis({
Name = "WingBlade",
Size = Vector3.new(W - 0.6, 0.18, 1.1),
CFrame = origin * CFrame.new(0, H / 2 + wingH + 0.4, L * 0.44) * CFrame.Angles(math.rad(-8), 0, 0),
Color = if accent and style.Spoiler == "gt" then dark else paint,
Parent = folder,
})
end
-- Exhaust tips
local tips = style.ExhaustCount
local xs: { number } = if tips == 1 then { -1.6 } elseif tips == 2 then { -1.8, 1.8 } else { -2.1, -1.5, 1.5, 2.1 }
for _, x in ipairs(xs) do
vis({
Name = "Exhaust",
Size = Vector3.new(0.45, 0.45, 0.5),
CFrame = origin * CFrame.new(x, -0.42, L / 2 + 0.15),
Color = Color3.fromRGB(210, 210, 215),
Material = Enum.Material.Metal,
Parent = folder,
})
end
end
local function buildTrim(folder: Folder, body: BasePart, style: Style)
local origin = body.CFrame
-- Underglow: one long neon strip under the floor pan. Night-map candy.
if style.Underglow then
vis({
Name = "Underglow",
Size = Vector3.new(W - 1.6, 0.12, L * 0.6),
CFrame = origin * CFrame.new(0, -H / 2 - 0.25, 0),
Color = style.Underglow,
Material = Enum.Material.Neon,
Transparency = 0.35,
Parent = folder,
})
end
-- Accent neon trim line along each flank
if style.AccentNeon then
for _, xSign in ipairs({ -1, 1 }) do
vis({
Name = "AccentLine",
Size = Vector3.new(0.08, 0.1, L * 0.5),
CFrame = origin * CFrame.new(xSign * (W / 2 + 0.02), 0.1, -L * 0.02),
Color = style.AccentNeon,
Material = Enum.Material.Neon,
Parent = folder,
})
end
end
end
-- Rims: a contrasting massless disc welded into each wheel so they read as
-- alloys instead of raw black cylinders. Welded to the WHEEL (they must spin).
local function buildRims(model: Model, accent: Color3?)
for _, desc in ipairs(model:GetChildren()) do
if desc:IsA("BasePart") and desc.Name:match("^Wheel_") then
local rim = Instance.new("Part")
rim.Name = "Rim"
rim.Shape = Enum.PartType.Cylinder
rim.Size = Vector3.new(Chassis.WHEEL_WIDTH + 0.1, Chassis.WHEEL_RADIUS * 1.2, Chassis.WHEEL_RADIUS * 1.2)
rim.CFrame = desc.CFrame
rim.Color = accent or Color3.fromRGB(200, 200, 205)
rim.Material = Enum.Material.Metal
rim.CanCollide = false
rim.CanQuery = false
rim.CanTouch = false
rim.Massless = true
rim.Parent = desc
local weld = Instance.new("WeldConstraint")
weld.Part0 = desc
weld.Part1 = rim
weld.Parent = desc
end
end
end
--- Build the full visual body kit onto an existing chassis.
--- `body` is the physics slab (stays as the collision hull, painted to match).
function CarBody.build(model: Model, body: BasePart, car: Car)
local style = STYLES[car.Id] or DEFAULT_STYLE
local paint = Color3.new(car.Color[1], car.Color[2], car.Color[3])
local dark = Color3.new(car.Color[1] * 0.25, car.Color[2] * 0.25, car.Color[3] * 0.25)
local glass = Color3.fromRGB(25, 30, 40)
local folder = Instance.new("Folder")
folder.Name = "BodyKit"
folder.Parent = model
buildNose(folder, body, paint, dark, style)
buildGreenhouse(folder, body, paint, glass, style)
buildTail(folder, body, paint, dark, style, style.AccentNeon)
buildTrim(folder, body, style)
buildRims(model, style.AccentNeon)
-- Weld every kit part to the body
for _, part in ipairs(folder:GetChildren()) do
if part:IsA("BasePart") then
weldTo(body, part)
end
end
end
return CarBody
+7 -14
View File
@@ -29,6 +29,7 @@ local Cars = require(Shared.Cars)
local Chassis = require(Shared.Chassis) local Chassis = require(Shared.Chassis)
local GameConfig = require(Shared.GameConfig) local GameConfig = require(Shared.GameConfig)
local DataManager = require(script.Parent.DataManager) local DataManager = require(script.Parent.DataManager)
local CarBody = require(script.Parent.CarBody)
type Car = Cars.Car type Car = Cars.Car
type Tuning = Chassis.Tuning type Tuning = Chassis.Tuning
@@ -208,20 +209,9 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
ballastWeld.Part1 = ballast ballastWeld.Part1 = ballast
ballastWeld.Parent = body ballastWeld.Parent = body
-- Cabin (visual only) ------------------------------------------------------- -- Visual bodywork (nose, glass, spoilers, lights, underglow, rims) is added
local cabin = makePart({ -- by CarBody.build() after assembly. All CarBody parts are Massless, so
Name = "Cabin", -- they never alter the chassis physics.
Size = Vector3.new(5, 1.6, 5),
CFrame = origin * CFrame.new(0, 1.5, 0.5),
Color = Color3.new(car.Color[1] * 0.7, car.Color[2] * 0.7, car.Color[3] * 0.7),
Material = Enum.Material.Glass,
CanCollide = false,
Parent = model,
})
local cabinWeld = Instance.new("WeldConstraint")
cabinWeld.Part0 = body
cabinWeld.Part1 = cabin
cabinWeld.Parent = body
-- Seat: input device only (Throttle/Steer). It drives nothing directly. ----- -- Seat: input device only (Throttle/Steer). It drives nothing directly. -----
local seat = Instance.new("VehicleSeat") local seat = Instance.new("VehicleSeat")
@@ -247,6 +237,9 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
end end
end end
-- Sculpted visual body kit (per-car styling; all parts Massless + welded)
CarBody.build(model, body, car)
model.PrimaryPart = body model.PrimaryPart = body
return model return model
end end