--!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