diff --git a/src/server/CarBody.luau b/src/server/CarBody.luau new file mode 100644 index 0000000..e092d85 --- /dev/null +++ b/src/server/CarBody.luau @@ -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 diff --git a/src/server/CarManager.luau b/src/server/CarManager.luau index 7fad62c..e47b02c 100644 --- a/src/server/CarManager.luau +++ b/src/server/CarManager.luau @@ -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