--!strict --[[ CarManager.luau (SERVER) Spawns the player's equipped car as a CONSTRAINT CHASSIS vehicle. M1 upgrade: the old single-Part + VehicleSeat arcade car is replaced with a proper physical chassis (A-Chassis style, built from Roblox constraints): Body ──Prismatic(vertical)+Spring──> Hub ──[front only: Servo hinge]──> Knuckle ──Motor hinge──> Wheel - Springs give real suspension; prismatics constrain travel to vertical. - Front knuckles yaw on Servo hinges (steering). - Every wheel spins on a Motor hinge (drive/brake). - The VehicleSeat is kept ONLY as the input device: its Throttle/Steer are read by the client ChassisController, which sets motor/servo targets locally (the driver owns the physics, so it replicates). The real RX-7 mesh swap (docs/MESH_SWAP.md) welds a MeshPart onto the Body and hides the placeholder blocks — the chassis itself doesn't change. ]] local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Shared = ReplicatedStorage.Shared 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 local CarManager = {} local activeCars: { [Player]: Model } = {} local carsFolder: Folder -- Helpers --------------------------------------------------------------------- local function makeAttachment(parent: BasePart, name: string, cf: CFrame): Attachment local att = Instance.new("Attachment") att.Name = name att.CFrame = cf att.Parent = parent return att end -- Attachment whose primary (X) axis points straight up — used for vertical -- prismatic travel and for the steering servo's yaw axis. local VERTICAL_AXIS = CFrame.Angles(0, 0, math.rad(90)) local function makePart(props: { Name: string, Size: Vector3, CFrame: CFrame, Color: Color3?, Material: Enum.Material?, CanCollide: boolean, Parent: Instance, Shape: Enum.PartType?, Friction: number?, }): Part local part = Instance.new("Part") part.Name = props.Name part.Size = props.Size part.CFrame = props.CFrame if props.Shape then part.Shape = props.Shape end part.Color = props.Color or Color3.new(0.2, 0.2, 0.2) part.Material = props.Material or Enum.Material.SmoothPlastic part.CanCollide = props.CanCollide if props.Friction then part.CustomPhysicalProperties = PhysicalProperties.new(0.9, props.Friction, 0.2, 1, 1) end part.Parent = props.Parent return part end -- Build one corner of the suspension. `isFront` corners get a steering servo. -- All positions are in BODY-LOCAL space, converted via body.CFrame. local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: number, zSign: number) local isFront = zSign < 0 -- car forward is -Z local cornerName = (isFront and "F" or "R") .. (xSign < 0 and "L" or "R") local wheelLocal = Vector3.new(xSign * Chassis.TRACK_HALF, -Chassis.SUSPENSION_DROP, zSign * Chassis.WHEELBASE_HALF) local wheelWorld = body.CFrame * CFrame.new(wheelLocal) -- Hub: rides the prismatic+spring below the body corner --------------------- local hub = makePart({ Name = "Hub_" .. cornerName, Size = Vector3.new(0.6, 0.6, 0.6), CFrame = wheelWorld, CanCollide = false, Parent = model, }) local bodyAtt = makeAttachment(body, "SuspAtt_" .. cornerName, CFrame.new(wheelLocal) * VERTICAL_AXIS) local hubAtt = makeAttachment(hub, "HubAtt", VERTICAL_AXIS) local prismatic = Instance.new("PrismaticConstraint") prismatic.Name = "SuspSlide_" .. cornerName prismatic.Attachment0 = bodyAtt prismatic.Attachment1 = hubAtt prismatic.LimitsEnabled = true prismatic.LowerLimit = -Chassis.SUSPENSION_TRAVEL / 2 prismatic.UpperLimit = Chassis.SUSPENSION_TRAVEL / 2 prismatic.Parent = body local spring = Instance.new("SpringConstraint") spring.Name = "SuspSpring_" .. cornerName spring.Attachment0 = bodyAtt spring.Attachment1 = hubAtt spring.FreeLength = 0.2 spring.Stiffness = tuning.SpringStiffness spring.Damping = tuning.SpringDamping spring.LimitsEnabled = false spring.Parent = body -- Front corners: a knuckle that yaws on a servo hinge (steering) ------------ local wheelParent: Part = hub if isFront then local knuckle = makePart({ Name = "Knuckle_" .. cornerName, Size = Vector3.new(0.5, 0.5, 0.5), CFrame = wheelWorld, CanCollide = false, Parent = model, }) local hubSteerAtt = makeAttachment(hub, "SteerAtt", VERTICAL_AXIS) local knuckleAtt = makeAttachment(knuckle, "KnuckleAtt", VERTICAL_AXIS) local servo = Instance.new("HingeConstraint") servo.Name = Chassis.STEER_SERVO_NAME servo.Attachment0 = hubSteerAtt servo.Attachment1 = knuckleAtt servo.ActuatorType = Enum.ActuatorType.Servo servo.ServoMaxTorque = 1e6 servo.AngularSpeed = tuning.SteerSpeed servo.TargetAngle = 0 servo.LimitsEnabled = true servo.LowerAngle = -tuning.MaxSteerAngle servo.UpperAngle = tuning.MaxSteerAngle servo.Parent = hub wheelParent = knuckle end -- Wheel: cylinder spinning on a motor hinge about the lateral (X) axis ------ local wheel = makePart({ Name = "Wheel_" .. cornerName, Size = Vector3.new(Chassis.WHEEL_WIDTH, Chassis.WHEEL_RADIUS * 2, Chassis.WHEEL_RADIUS * 2), CFrame = wheelWorld, Shape = Enum.PartType.Cylinder, -- cylinder axis = X = lateral. correct spin axis Color = Color3.new(0.08, 0.08, 0.09), Material = Enum.Material.Rubber, CanCollide = true, Friction = tuning.WheelFriction, Parent = model, }) local parentAtt = makeAttachment(wheelParent, "WheelAxleAtt", CFrame.new()) local wheelAtt = makeAttachment(wheel, "WheelAtt", CFrame.new()) local motor = Instance.new("HingeConstraint") motor.Name = Chassis.WHEEL_MOTOR_NAME motor.Attachment0 = parentAtt motor.Attachment1 = wheelAtt motor.ActuatorType = Enum.ActuatorType.Motor motor.MotorMaxTorque = 0 -- freewheel until the ChassisController takes over motor.AngularVelocity = 0 motor.Parent = wheelParent end -- Build the full constraint-chassis car model. local function buildCarModel(car: Car, tuning: Tuning): Model local model = Instance.new("Model") model.Name = "Car_" .. car.Id local origin = CFrame.new() -- built at origin; PivotTo() places it later -- Body (PrimaryPart / mesh anchor point) ------------------------------------ local body = makePart({ Name = "Body", Size = Chassis.BODY_SIZE, CFrame = origin, Color = Color3.new(car.Color[1], car.Color[2], car.Color[3]), CanCollide = true, Parent = model, }) -- Ballast: dense low block welded under the body — drops the centre of mass -- so the car corners flat instead of rolling over. local ballast = makePart({ Name = "Ballast", Size = Vector3.new(4, 0.4, 8), CFrame = origin * CFrame.new(0, -0.9, 0), CanCollide = false, Parent = model, }) ballast.Transparency = 1 ballast.CustomPhysicalProperties = PhysicalProperties.new(8, 0.3, 0.5, 1, 1) local ballastWeld = Instance.new("WeldConstraint") ballastWeld.Part0 = body ballastWeld.Part1 = ballast ballastWeld.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") seat.Name = Chassis.SEAT_NAME seat.Size = Vector3.new(4, 0.5, 3) seat.MaxSpeed = 0 -- neutralise built-in drive; constraints do the work seat.Torque = 0 seat.TurnSpeed = 0 seat.HeadsUpDisplay = false seat.CanCollide = false seat.Color = Color3.new(0.1, 0.1, 0.1) seat.CFrame = origin * CFrame.new(0, 1.2, -0.5) seat.Parent = model local seatWeld = Instance.new("WeldConstraint") seatWeld.Part0 = body seatWeld.Part1 = seat seatWeld.Parent = body -- Four suspension corners --------------------------------------------------- for _, xSign in ipairs({ -1, 1 }) do for _, zSign in ipairs({ -1, 1 }) do buildCorner(model, body, tuning, xSign, zSign) end end -- Sculpted visual body kit (per-car styling; all parts Massless + welded) CarBody.build(model, body, car) model.PrimaryPart = body return model end -- Find a reasonable spawn CFrame (a SpawnPad in the map, else origin) -------- local function spawnCFrame(): CFrame local map = Workspace:FindFirstChild("Map") if map then local pad = map:FindFirstChild("SpawnPad", true) if pad and pad:IsA("BasePart") then return pad.CFrame * CFrame.new(0, 5, 0) end end return CFrame.new(0, 8, 0) end --- Despawn the player's current car if any. function CarManager.despawn(player: Player) local existing = activeCars[player] if existing then existing:Destroy() activeCars[player] = nil end end --- Spawn (or replace) the player's equipped car and seat them in it. function CarManager.spawn(player: Player, carId: string?) local data = DataManager.get(player) if not data then return end local id = carId or data.EquippedCarId local car = Cars.Get(id) if not car then return end -- Restricted car (RX-7) can only be driven by allowlisted UserIds. if not GameConfig.canDrive(player.UserId, car.Id, car.Restricted) then return end CarManager.despawn(player) local tuning = Chassis.tuningFor(car) local model = buildCarModel(car, tuning) model:PivotTo(spawnCFrame()) model.Parent = carsFolder model:SetAttribute("OwnerUserId", player.UserId) model:SetAttribute("CarId", id) -- Give the driver network ownership of EVERY assembly in the car. With a -- constraint chassis the body, hubs, knuckles, and wheels are SEPARATE -- assemblies (constraints don't merge them like welds do) — missing one -- causes stuttery mixed-authority physics. for _, desc in ipairs(model:GetDescendants()) do if desc:IsA("BasePart") and not desc.Anchored then desc:SetNetworkOwner(player) end end local character = player.Character if character then local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat if seat then task.defer(function() seat:Sit(character:FindFirstChildOfClass("Humanoid")) end) end end activeCars[player] = model end function CarManager.init() carsFolder = Instance.new("Folder") carsFolder.Name = "ActiveCars" carsFolder.Parent = Workspace Players.PlayerRemoving:Connect(function(player) CarManager.despawn(player) end) end return CarManager