Initial MVP: drivable RX-7 vertical slice + server-authoritative systems
CI / quality (push) Failing after 5m53s
CI / quality (push) Failing after 5m53s
- Rojo + Wally project; ProfileStore (loleris) for safe saves - Shared: GameConfig, Cars (RX-7 free starter), ProfileTemplate, Remotes - Server: DataManager, Economy, CarManager, RaceManager, MapBuilder, AntiCheat - Client: neon HUD + Garage/Dealership UI (buy/equip/upgrade), race/daily - Procedural Tokyo expressway (spawn pad, finish line, neon pylons, kill walls) - CI workflow (Gitea/GitHub), Makefile, selene+stylua clean, rojo builds - Docs: README, GDD, realistic solo ROADMAP
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
--!strict
|
||||
--[[
|
||||
CarManager.luau (SERVER)
|
||||
Spawns the player's equipped car as a simple, PROVEN drivable vehicle.
|
||||
|
||||
Design choice (per "don't reinvent the wheel"): instead of a hand-rolled
|
||||
physics chassis, the MVP car is a single driving Part + a VehicleSeat.
|
||||
Roblox's VehicleSeat drives the assembly via its built-in Throttle/Steer —
|
||||
the most reliable, least-buggy way to get a car moving on day one. When you
|
||||
later import the real RX-7 mesh + a constraint chassis (A-Chassis style),
|
||||
you swap out `buildCarModel` and keep everything else.
|
||||
|
||||
Upgrades scale MaxSpeed / Torque / TurnSpeed so the upgrade economy is real.
|
||||
]]
|
||||
|
||||
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 ProfileTemplate = require(Shared.ProfileTemplate)
|
||||
local DataManager = require(script.Parent.DataManager)
|
||||
|
||||
type ProfileData = ProfileTemplate.ProfileData
|
||||
type Car = Cars.Car
|
||||
|
||||
local CarManager = {}
|
||||
|
||||
local activeCars: { [Player]: Model } = {}
|
||||
local carsFolder: Folder
|
||||
|
||||
-- Convert a car's 0-100 stats + upgrade levels into VehicleSeat tuning --------
|
||||
local function computeTuning(car: Car, upgrades: { Engine: number, Tires: number, Nitro: number })
|
||||
local engineLvl = upgrades.Engine or 0
|
||||
local tiresLvl = upgrades.Tires or 0
|
||||
-- Base values derived from stats; upgrades add a percentage on top.
|
||||
local maxSpeed = 60 + car.Stats.Speed * 1.6 -- studs/sec-ish target
|
||||
maxSpeed *= (1 + engineLvl * 0.08) -- +8% top speed per engine level
|
||||
local torque = 8000 + car.Stats.Acceleration * 220
|
||||
torque *= (1 + engineLvl * 0.10)
|
||||
local turnSpeed = 20 + car.Stats.Handling * 0.35
|
||||
turnSpeed *= (1 + tiresLvl * 0.06) -- tires help turn-in
|
||||
return maxSpeed, torque, turnSpeed
|
||||
end
|
||||
|
||||
-- Build the placeholder car model. Returns the Model with a VehicleSeat.
|
||||
local function buildCarModel(car: Car, maxSpeed: number, torque: number, turnSpeed: number): Model
|
||||
local model = Instance.new("Model")
|
||||
model.Name = "Car_" .. car.Id
|
||||
|
||||
-- Body (the driving part) --------------------------------------------------
|
||||
local body = Instance.new("Part")
|
||||
body.Name = "Body"
|
||||
body.Size = Vector3.new(6, 2, 12)
|
||||
body.Color = Color3.new(car.Color[1], car.Color[2], car.Color[3])
|
||||
body.Material = Enum.Material.SmoothPlastic
|
||||
body.CanCollide = true
|
||||
body.Parent = model
|
||||
|
||||
-- A cabin block so it reads as a car, welded to the body -------------------
|
||||
local cabin = Instance.new("Part")
|
||||
cabin.Name = "Cabin"
|
||||
cabin.Size = Vector3.new(5, 1.6, 5)
|
||||
cabin.Color = Color3.new(car.Color[1] * 0.7, car.Color[2] * 0.7, car.Color[3] * 0.7)
|
||||
cabin.Material = Enum.Material.Glass
|
||||
cabin.CanCollide = false
|
||||
cabin.Parent = model
|
||||
local cabinWeld = Instance.new("WeldConstraint")
|
||||
cabinWeld.Part0 = body
|
||||
cabinWeld.Part1 = cabin
|
||||
cabinWeld.Parent = body
|
||||
cabin.CFrame = body.CFrame * CFrame.new(0, 1.8, 0.5)
|
||||
|
||||
-- VehicleSeat: Roblox's built-in, reliable driver ------------------------
|
||||
local seat = Instance.new("VehicleSeat")
|
||||
seat.Name = "DriveSeat"
|
||||
seat.Size = Vector3.new(4, 0.5, 3)
|
||||
seat.MaxSpeed = maxSpeed
|
||||
seat.Torque = torque
|
||||
seat.TurnSpeed = turnSpeed
|
||||
seat.HeadsUpDisplay = false
|
||||
seat.CanCollide = false
|
||||
seat.Color = Color3.new(0.1, 0.1, 0.1)
|
||||
seat.Parent = model
|
||||
local seatWeld = Instance.new("WeldConstraint")
|
||||
seatWeld.Part0 = body
|
||||
seatWeld.Part1 = seat
|
||||
seatWeld.Parent = body
|
||||
seat.CFrame = body.CFrame * CFrame.new(0, 1.6, -1)
|
||||
|
||||
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
|
||||
-- Only spawn cars the player owns (anti-cheat: no spawning unowned exotics).
|
||||
local owned
|
||||
for _, o in ipairs(data.OwnedCars) do
|
||||
if o.Id == id then
|
||||
owned = o
|
||||
break
|
||||
end
|
||||
end
|
||||
if not owned then
|
||||
return
|
||||
end
|
||||
|
||||
CarManager.despawn(player)
|
||||
|
||||
local maxSpeed, torque, turnSpeed = computeTuning(car, owned.Upgrades)
|
||||
local model = buildCarModel(car, maxSpeed, torque, turnSpeed)
|
||||
model:PivotTo(spawnCFrame())
|
||||
model.Parent = carsFolder
|
||||
model:SetAttribute("OwnerUserId", player.UserId)
|
||||
model:SetAttribute("CarId", id)
|
||||
|
||||
-- Sit the player down. Give the assembly network ownership to the driver so
|
||||
-- physics are smooth on their client (standard Roblox vehicle practice).
|
||||
local character = player.Character
|
||||
if character then
|
||||
local seat = model:FindFirstChild("DriveSeat") :: VehicleSeat
|
||||
local body = model.PrimaryPart
|
||||
if body then
|
||||
body:SetNetworkOwner(player)
|
||||
end
|
||||
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
|
||||
Reference in New Issue
Block a user