CarBody: sculpted per-car visual body kits (cars-first priority)
CI / quality (push) Successful in 1m5s
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:
@@ -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
|
||||
@@ -29,6 +29,7 @@ local Cars = require(Shared.Cars)
|
||||
local Chassis = require(Shared.Chassis)
|
||||
local GameConfig = require(Shared.GameConfig)
|
||||
local DataManager = require(script.Parent.DataManager)
|
||||
local CarBody = require(script.Parent.CarBody)
|
||||
|
||||
type Car = Cars.Car
|
||||
type Tuning = Chassis.Tuning
|
||||
@@ -208,20 +209,9 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
||||
ballastWeld.Part1 = ballast
|
||||
ballastWeld.Parent = body
|
||||
|
||||
-- Cabin (visual only) -------------------------------------------------------
|
||||
local cabin = makePart({
|
||||
Name = "Cabin",
|
||||
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
|
||||
-- Visual bodywork (nose, glass, spoilers, lights, underglow, rims) is added
|
||||
-- by CarBody.build() after assembly. All CarBody parts are Massless, so
|
||||
-- they never alter the chassis physics.
|
||||
|
||||
-- Seat: input device only (Throttle/Steer). It drives nothing directly. -----
|
||||
local seat = Instance.new("VehicleSeat")
|
||||
@@ -247,6 +237,9 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
||||
end
|
||||
end
|
||||
|
||||
-- Sculpted visual body kit (per-car styling; all parts Massless + welded)
|
||||
CarBody.build(model, body, car)
|
||||
|
||||
model.PrimaryPart = body
|
||||
return model
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user