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
108 lines
3.1 KiB
Luau
108 lines
3.1 KiB
Luau
--!strict
|
|
--[[
|
|
RaceManager.luau (SERVER)
|
|
MVP race: a solo time-trial down the expressway. Player starts a race, drives
|
|
to the FinishLine, and the SERVER validates the finish (touch of the real
|
|
finish part) before paying out. This keeps rewards server-authoritative — the
|
|
client can SAY it won, but it only gets paid if the server saw the touch.
|
|
|
|
This is intentionally the simplest thing that proves the race->reward->save
|
|
loop. Head-to-head, checkpoints, and drag mode come later.
|
|
]]
|
|
|
|
local Players = game:GetService("Players")
|
|
local Workspace = game:GetService("Workspace")
|
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
|
|
local Shared = ReplicatedStorage.Shared
|
|
local GameConfig = require(Shared.GameConfig)
|
|
local Economy = require(script.Parent.Economy)
|
|
local DataManager = require(script.Parent.DataManager)
|
|
|
|
local RaceManager = {}
|
|
|
|
type ActiveRace = {
|
|
startTime: number,
|
|
connection: RBXScriptConnection,
|
|
}
|
|
|
|
local active: { [Player]: ActiveRace } = {}
|
|
local RACE_TIMEOUT = 180 -- auto-cancel a race after 3 min
|
|
|
|
local function finishFor(player: Player, won: boolean)
|
|
local race = active[player]
|
|
if not race then
|
|
return
|
|
end
|
|
race.connection:Disconnect()
|
|
active[player] = nil
|
|
|
|
local data = DataManager.get(player)
|
|
if not data then
|
|
return
|
|
end
|
|
data.TotalRaces += 1
|
|
|
|
local payout = GameConfig.RaceBaseReward
|
|
if won then
|
|
payout += GameConfig.RaceWinBonus
|
|
data.Wins += 1
|
|
end
|
|
Economy.addCash(player, payout)
|
|
Economy.addRep(player, GameConfig.RaceRepReward)
|
|
-- addCash/addRep already push updates; TotalRaces/Wins ride along on next push.
|
|
Economy.pushUpdate(player, data)
|
|
end
|
|
|
|
--- Start a race for the player. Returns a Result.
|
|
function RaceManager.start(player: Player, _raceId: string?): { ok: boolean, reason: string? }
|
|
if active[player] then
|
|
return { ok = false, reason = "Already racing" }
|
|
end
|
|
local map = Workspace:FindFirstChild("Map")
|
|
local finish = map and map:FindFirstChild("FinishLine")
|
|
if not (finish and finish:IsA("BasePart")) then
|
|
return { ok = false, reason = "No finish line in map" }
|
|
end
|
|
|
|
local conn
|
|
conn = finish.Touched:Connect(function(hit)
|
|
-- Was this the racing player's car/character that touched the line?
|
|
local character = player.Character
|
|
local carModel = hit:FindFirstAncestorOfClass("Model")
|
|
local isOwnCar = carModel and carModel:GetAttribute("OwnerUserId") == player.UserId
|
|
local isSelf = character and hit:IsDescendantOf(character)
|
|
if isOwnCar or isSelf then
|
|
finishFor(player, true)
|
|
end
|
|
end)
|
|
|
|
active[player] = { startTime = os.clock(), connection = conn }
|
|
|
|
-- Timeout guard
|
|
task.delay(RACE_TIMEOUT, function()
|
|
if active[player] and os.clock() - active[player].startTime >= RACE_TIMEOUT then
|
|
-- cancel with no reward (won = false but we just clean up)
|
|
local race = active[player]
|
|
if race then
|
|
race.connection:Disconnect()
|
|
active[player] = nil
|
|
end
|
|
end
|
|
end)
|
|
|
|
return { ok = true }
|
|
end
|
|
|
|
function RaceManager.init()
|
|
Players.PlayerRemoving:Connect(function(player)
|
|
local race = active[player]
|
|
if race then
|
|
race.connection:Disconnect()
|
|
active[player] = nil
|
|
end
|
|
end)
|
|
end
|
|
|
|
return RaceManager
|