Initial MVP: drivable RX-7 vertical slice + server-authoritative systems
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:
2026-07-21 23:30:53 -04:00
commit 9a2f2d4dc9
24 changed files with 1986 additions and 0 deletions
+178
View File
@@ -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