-- ========================================================== -- SweetC Cremiosis - Automated Racing & NPC Bridge -- ServerScriptService -> Script (RunContext: Server) -- Target: https://live.sweetc.my.id -- ========================================================== -- FITUR SISTEM BALAPAN OTOMATIS: -- 1. Round State: WAITING -> COUNTDOWN -> RACING -> FINISHED -> RESETTING -- 2. Line-up otomatis di Starting Grid (StartLine) saat NPC spawn -- 3. Checkpoint sequential (CP1 -> CP2 -> CP3 -> CP4 -> FinishLine) -- 4. Deteksi lap anti-duplikasi, perhitungan progress %, dan penentuan pemenang -- 5. Sinkronisasi leaderboard dan progress real-time ke Dashboard VPS -- 6. Dukungan auto-create lintasan jika map masih kosong -- ========================================================== local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local RunService = game:GetService("RunService") local CONFIG = { API_URL = "https://live.sweetc.my.id/api/roblox", API_KEY = script:GetAttribute("ApiKey") or "roblox-sweetc-bridge-secure-key", POLL_INTERVAL = 1.5, -- Polling ke backend setiap 1.5 detik TELEMETRY_INTERVAL = 5.0, -- Heartbeat status setiap 5 detik RACE_SYNC_INTERVAL = 1.2, -- Sync progress balapan ke dashboard setiap 1.2 detik RUNNER_NAME = "SpeedyMarb", SPAWN_FOLDER_NAME = "SweetC_Participants", TRACK_FOLDER_NAME = "SweetC_Track", AUTO_CREATE_TRACK = true, -- Buat track & checkpoint otomatis jika belum ada CHECKPOINT_RADIUS = 12.0, -- Jarak radius deteksi checkpoint (studs) DEFAULT_TOTAL_LAPS = 3, DEFAULT_BASE_SPEED = 16, MAX_NPCS = 50, } print("=========================================================") print("๐Ÿ [SweetC Bridge] Automated Racing Engine Initializing...") print("๐ŸŒ Target API: " .. CONFIG.API_URL) print("=========================================================") -- In-game state local isBridgeActive = true local roundStatus = "WAITING" -- WAITING | COUNTDOWN | RACING | FINISHED | RESETTING local currentRoundId = "rnd_init" local targetTotalLaps = CONFIG.DEFAULT_TOTAL_LAPS local raceBaseSpeed = CONFIG.DEFAULT_BASE_SPEED local raceStartTime = 0 -- Tracking peserta & NPC local spawnedUserIds = {} -- Set UserId -> true local processedEventIds = {} -- Set EventId -> true local runnerList = {} -- Array of runnerState local leaderboard = {} -- Array of { rank, robloxId, username, time } -- Folder peserta di Workspace local participantsFolder = Workspace:FindFirstChild(CONFIG.SPAWN_FOLDER_NAME) if not participantsFolder then participantsFolder = Instance.new("Folder") participantsFolder.Name = CONFIG.SPAWN_FOLDER_NAME participantsFolder.Parent = Workspace end -- ========================================================== -- 1. SETUP LINTASAN & CHECKPOINT (AUTO-BUILD JIKA BELUM ADA) -- ========================================================== local trackFolder = Workspace:FindFirstChild(CONFIG.TRACK_FOLDER_NAME) local function ensureTrackSetup() if not trackFolder and CONFIG.AUTO_CREATE_TRACK then print("[SweetC Track] Membuat arena lintasan balap default...") trackFolder = Instance.new("Folder") trackFolder.Name = CONFIG.TRACK_FOLDER_NAME trackFolder.Parent = Workspace -- Start Line local startLine = Instance.new("Part") startLine.Name = "SweetC_StartLine" startLine.Size = Vector3.new(36, 1, 8) startLine.Position = Vector3.new(0, 0.5, 0) startLine.Anchored = true startLine.CanCollide = false startLine.Material = Enum.Material.Neon startLine.Color = Color3.fromRGB(34, 197, 94) startLine.Parent = trackFolder -- Finish Line local finishLine = Instance.new("Part") finishLine.Name = "SweetC_FinishLine" finishLine.Size = Vector3.new(36, 1, 6) finishLine.Position = Vector3.new(0, 0.5, -4) finishLine.Anchored = true finishLine.CanCollide = false finishLine.Material = Enum.Material.Neon finishLine.Color = Color3.fromRGB(234, 179, 8) finishLine.Parent = trackFolder -- Checkpoints Folder local cpFolder = Instance.new("Folder") cpFolder.Name = "Checkpoints" cpFolder.Parent = trackFolder local cpCoords = { { name = "CP1", pos = Vector3.new(0, 1.5, 75), size = Vector3.new(36, 8, 4), color = Color3.fromRGB(56, 189, 248) }, { name = "CP2", pos = Vector3.new(80, 1.5, 75), size = Vector3.new(4, 8, 36), color = Color3.fromRGB(168, 85, 247) }, { name = "CP3", pos = Vector3.new(80, 1.5, -75), size = Vector3.new(36, 8, 4), color = Color3.fromRGB(56, 189, 248) }, { name = "CP4", pos = Vector3.new(0, 1.5, -75), size = Vector3.new(4, 8, 36), color = Color3.fromRGB(168, 85, 247) } } for _, info in ipairs(cpCoords) do local cp = Instance.new("Part") cp.Name = info.name cp.Size = info.size cp.Position = info.pos cp.Anchored = true cp.CanCollide = false cp.Transparency = 0.65 cp.Material = Enum.Material.Neon cp.Color = info.color cp.Parent = cpFolder end print("[SweetC Track] โœ… Lintasan dan 4 Checkpoint berhasil dipasang.") end end ensureTrackSetup() -- Helper: Ambil referensi Checkpoints terurut local function getCheckpoints() local list = {} if trackFolder then local cpFolder = trackFolder:FindFirstChild("Checkpoints") if cpFolder then for i = 1, 20 do local cp = cpFolder:FindFirstChild("CP" .. i) or cpFolder:FindFirstChild(tostring(i)) if cp and cp:IsA("BasePart") then table.insert(list, cp) end end end end -- Fallback: Cari di Workspace jika dibuat manual oleh developer if #list == 0 then for i = 1, 20 do local cp = Workspace:FindFirstChild("SweetC_Checkpoint" .. i, true) or Workspace:FindFirstChild("CP" .. i, true) if cp and cp:IsA("BasePart") then table.insert(list, cp) end end end return list end local function getStartLine() if trackFolder and trackFolder:FindFirstChild("SweetC_StartLine") then return trackFolder.SweetC_StartLine end return Workspace:FindFirstChild("SweetC_StartLine", true) or Workspace:FindFirstChild("SweetC_SpawnArea", true) end local function getFinishLine() if trackFolder and trackFolder:FindFirstChild("SweetC_FinishLine") then return trackFolder.SweetC_FinishLine end return Workspace:FindFirstChild("SweetC_FinishLine", true) or getStartLine() end -- Posisi Starting Grid untuk barisan peserta local function getGridCFrame(index) local startPart = getStartLine() local basePos = startPart and startPart.Position or Vector3.new(0, 0.5, 0) local row = math.floor(index / 8) local col = (index % 8) - 3.5 local offset = Vector3.new(col * 4.2, 3, -row * 5.0) return CFrame.new(basePos + offset) end -- ========================================================== -- 2. OVERHEAD GUI NAMETAG & STATUS -- ========================================================== local function createOverheadGui(head, robloxUsername, tiktokUser, isReal) if not head then return end local existing = head:FindFirstChild("SweetCNametag") if existing then existing:Destroy() end local bbg = Instance.new("BillboardGui") bbg.Name = "SweetCNametag" bbg.Adornee = head bbg.Size = UDim2.new(0, 190, 0, 60) bbg.StudsOffset = Vector3.new(0, 3.0, 0) bbg.AlwaysOnTop = true bbg.MaxDistance = 180 local frame = Instance.new("Frame") frame.Size = UDim2.new(1, 0, 1, 0) frame.BackgroundColor3 = Color3.fromRGB(15, 23, 42) frame.BackgroundTransparency = 0.2 frame.BorderSizePixel = 0 frame.Parent = bbg local corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(0, 8) corner.Parent = frame local stroke = Instance.new("UIStroke") stroke.Name = "TagStroke" stroke.Color = isReal and Color3.fromRGB(0, 242, 254) or Color3.fromRGB(245, 158, 11) stroke.Thickness = 1.5 stroke.Parent = frame local nameLabel = Instance.new("TextLabel") nameLabel.Name = "NameLabel" nameLabel.Size = UDim2.new(1, 0, 0, 20) nameLabel.Position = UDim2.new(0, 0, 0, 4) nameLabel.BackgroundTransparency = 1 nameLabel.Text = (isReal and "" or "[DEMO] ") .. "@" .. tostring(robloxUsername) nameLabel.TextColor3 = isReal and Color3.fromRGB(255, 255, 255) or Color3.fromRGB(254, 240, 138) nameLabel.TextSize = 13 nameLabel.Font = Enum.Font.GothamBold nameLabel.Parent = frame local tiktokLabel = Instance.new("TextLabel") tiktokLabel.Name = "TikTokLabel" tiktokLabel.Size = UDim2.new(1, 0, 0, 16) tiktokLabel.Position = UDim2.new(0, 0, 0, 22) tiktokLabel.BackgroundTransparency = 1 tiktokLabel.Text = (isReal and "LIVE: @" or "DEMO: @") .. tostring(tiktokUser) tiktokLabel.TextColor3 = isReal and Color3.fromRGB(56, 189, 248) or Color3.fromRGB(251, 191, 36) tiktokLabel.TextSize = 10 tiktokLabel.Font = Enum.Font.Gotham tiktokLabel.Parent = frame local statusLabel = Instance.new("TextLabel") statusLabel.Name = "StatusLabel" statusLabel.Size = UDim2.new(1, 0, 0, 16) statusLabel.Position = UDim2.new(0, 0, 0, 38) statusLabel.BackgroundTransparency = 1 statusLabel.Text = "WAITING AT START" statusLabel.TextColor3 = Color3.fromRGB(148, 163, 184) statusLabel.TextSize = 10 statusLabel.Font = Enum.Font.GothamBold statusLabel.Parent = frame bbg.Parent = head end local function updateRunnerNametag(runner, text, color) if runner and runner.model and runner.model:FindFirstChild("Head") then local bbg = runner.model.Head:FindFirstChild("SweetCNametag") if bbg and bbg:FindFirstChild("Frame") and bbg.Frame:FindFirstChild("StatusLabel") then bbg.Frame.StatusLabel.Text = text if color then bbg.Frame.StatusLabel.TextColor3 = color end end end end -- ========================================================== -- 3. SPAWNING NPC KE STARTING GRID -- ========================================================== local function spawnParticipantNpc(action) local userId = tonumber(action.userId or action.robloxId) local username = tostring(action.robloxUsername or ("User_" .. tostring(userId))) local eventId = tostring(action.id or action.eventId or userId) local tiktokUser = tostring(action.tiktokUser or "TikTokUser") local isReal = action.isReal ~= false local isDemo = (action.isDemo == true) or (not isReal) local demoRunId = tostring(action.demoRunId or "") if not userId or userId <= 0 then return false, "INVALID_USER_ID" end -- Deduplikasi if spawnedUserIds[userId] then return true, "ALREADY_SPAWNED", "NPC_" .. username end if #runnerList >= CONFIG.MAX_NPCS then return false, "ARENA_FULL" end local createOk, npcModel = pcall(function() return Players:CreateHumanoidModelFromUserId(userId) end) if not createOk or not npcModel then warn("[SweetC Bridge] Gagal memuat karakter UserId " .. userId .. ": " .. tostring(npcModel)) return false, "LOAD_AVATAR_FAILED" end npcModel.Name = "NPC_" .. username npcModel:SetAttribute("RobloxUserId", userId) npcModel:SetAttribute("IsDemo", isDemo) npcModel:SetAttribute("DemoRunId", demoRunId) local hrp = npcModel:FindFirstChild("HumanoidRootPart") local humanoid = npcModel:FindFirstChildOfClass("Humanoid") local head = npcModel:FindFirstChild("Head") local gridPos = getGridCFrame(#runnerList) if hrp then hrp.CFrame = gridPos hrp.Anchored = (roundStatus ~= "RACING") end if humanoid then humanoid.DisplayName = (isDemo and "[DEMO] " or "") .. username humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None humanoid.WalkSpeed = raceBaseSpeed humanoid.MaxHealth = 100 humanoid.Health = 100 end if head then createOverheadGui(head, username, tiktokUser, isReal) end npcModel.Parent = participantsFolder -- Simpan runner state local runner = { model = npcModel, userId = userId, username = username, tiktokUser = tiktokUser, isReal = isReal, isDemo = isDemo, demoRunId = demoRunId, currentLap = 1, nextCheckpoint = 1, progressPercent = 0, speed = raceBaseSpeed, speedMultiplier = 1.0, finished = false, finishTime = nil, rank = nil, lastPosition = hrp and hrp.Position or Vector3.zero, stuckCount = 0 } spawnedUserIds[userId] = true processedEventIds[eventId] = true table.insert(runnerList, runner) print(string.format("[SweetC Bridge] ๐Ÿ‘ค Peserta di Grid: @%s (UserId: %d). Total di Starting Line: %d", username, userId, #runnerList)) return true, "SPAWNED", npcModel.Name end -- ========================================================== -- 4. ROUND CONTROLS (WAITING -> COUNTDOWN -> RACING -> FINISHED -> RESETTING) -- ========================================================== local function setRoundState(newState, params) params = params or {} print(string.format("[SweetC Race] ๐Ÿ”„ Transisi Status: %s -> %s", roundStatus, newState)) roundStatus = newState if newState == "WAITING" then -- Reposisi ke Starting Grid for idx, runner in ipairs(runnerList) do runner.currentLap = 1 runner.nextCheckpoint = 1 runner.progressPercent = 0 runner.finished = false runner.finishTime = nil runner.rank = nil runner.speed = raceBaseSpeed * (runner.speedMultiplier or 1.0) if runner.model and runner.model:FindFirstChild("HumanoidRootPart") then runner.model.HumanoidRootPart.CFrame = getGridCFrame(idx - 1) runner.model.HumanoidRootPart.Anchored = true local hum = runner.model:FindFirstChildOfClass("Humanoid") if hum then hum.WalkSpeed = runner.speed end end updateRunnerNametag(runner, "WAITING AT START", Color3.fromRGB(148, 163, 184)) end leaderboard = {} elseif newState == "COUNTDOWN" then for _, runner in ipairs(runnerList) do if runner.model and runner.model:FindFirstChild("HumanoidRootPart") then runner.model.HumanoidRootPart.Anchored = true end updateRunnerNametag(runner, "READY... SET...", Color3.fromRGB(245, 158, 11)) end elseif newState == "RACING" then raceStartTime = os.clock() targetTotalLaps = params.totalLaps or targetTotalLaps or CONFIG.DEFAULT_TOTAL_LAPS raceBaseSpeed = params.baseSpeed or raceBaseSpeed or CONFIG.DEFAULT_BASE_SPEED print(string.format("[SweetC Race] ๐ŸŸข BALAPAN DIMULAI! Target: %d Laps, Runners: %d", targetTotalLaps, #runnerList)) for _, runner in ipairs(runnerList) do runner.currentLap = 1 runner.nextCheckpoint = 1 runner.finished = false runner.rank = nil runner.speed = raceBaseSpeed * (runner.speedMultiplier or 1.0) if runner.model and runner.model:FindFirstChild("HumanoidRootPart") then runner.model.HumanoidRootPart.Anchored = false local hum = runner.model:FindFirstChildOfClass("Humanoid") if hum then hum.WalkSpeed = runner.speed end end updateRunnerNametag(runner, "LAP 1/" .. targetTotalLaps, Color3.fromRGB(34, 197, 94)) end elseif newState == "FINISHED" then print("[SweetC Race] ๐Ÿ BALAPAN SELESAI!") for _, runner in ipairs(runnerList) do if runner.model and runner.model:FindFirstChild("HumanoidRootPart") then runner.model.HumanoidRootPart.Anchored = true local hum = runner.model:FindFirstChildOfClass("Humanoid") if hum then hum.WalkSpeed = 0 end end if runner.rank then updateRunnerNametag(runner, "๐Ÿ† RANK #" .. runner.rank, Color3.fromRGB(234, 179, 8)) else updateRunnerNametag(runner, "DNF (FINISHED)", Color3.fromRGB(148, 163, 184)) end end end end -- ========================================================== -- 5. AUTONOMOUS MOVEMENT & CHECKPOINT VALIDATION (RACING LOOP) -- ========================================================== local function updateRaceStep() if roundStatus ~= "RACING" then return end local checkpoints = getCheckpoints() local finishLine = getFinishLine() local allFinished = (#runnerList > 0) for _, runner in ipairs(runnerList) do if not runner.finished and runner.model and runner.model.Parent then allFinished = false local hrp = runner.model:FindFirstChild("HumanoidRootPart") local humanoid = runner.model:FindFirstChildOfClass("Humanoid") if hrp and humanoid and humanoid.Health > 0 then -- 1. Tentukan target checkpoint atau garis finis local targetPart = nil local isFinishTarget = false if #checkpoints > 0 and runner.nextCheckpoint <= #checkpoints then targetPart = checkpoints[runner.nextCheckpoint] else targetPart = finishLine isFinishTarget = true end if targetPart then humanoid:MoveTo(targetPart.Position) -- 2. Deteksi jarak ke checkpoint/finis local dist = (hrp.Position - targetPart.Position).Magnitude if dist <= CONFIG.CHECKPOINT_RADIUS then if not isFinishTarget then -- Berhasil melewati checkpoint urut runner.nextCheckpoint = runner.nextCheckpoint + 1 else -- Menyelesaikan 1 lap setelah semua checkpoint dilalui runner.currentLap = runner.currentLap + 1 runner.nextCheckpoint = 1 if runner.currentLap > targetTotalLaps then -- Runner Menyelesaikan Balapan! runner.finished = true runner.finishTime = os.clock() - raceStartTime table.insert(leaderboard, { rank = #leaderboard + 1, robloxId = runner.userId, username = runner.username, time = string.format("%.2fs", runner.finishTime) }) runner.rank = #leaderboard hrp.Anchored = true humanoid.WalkSpeed = 0 updateRunnerNametag(runner, "๐Ÿ† RANK #" .. runner.rank, Color3.fromRGB(234, 179, 8)) print(string.format("[SweetC Race] ๐Ÿฅ‡ Runner @%s FINISHED di Posisi #%d (Waktu: %.2fs)!", runner.username, runner.rank, runner.finishTime)) else updateRunnerNametag(runner, string.format("LAP %d/%d", runner.currentLap, targetTotalLaps), Color3.fromRGB(34, 197, 94)) end end end -- 3. Hitung progress persen lap & checkpoint local totalCpCount = math.max(1, #checkpoints + 1) local currentCpStep = (runner.nextCheckpoint - 1) local lapProgress = (runner.currentLap - 1) / targetTotalLaps local cpProgress = (currentCpStep / totalCpCount) / targetTotalLaps runner.progressPercent = math.clamp(math.round((lapProgress + cpProgress) * 100), 0, 100) -- 4. Anti-stuck: Jika tersangkut rintangan, lakukan jump local movedDist = (hrp.Position - runner.lastPosition).Magnitude if movedDist < 0.6 then runner.stuckCount = runner.stuckCount + 1 if runner.stuckCount >= 2 then humanoid.Jump = true runner.stuckCount = 0 end else runner.stuckCount = 0 end runner.lastPosition = hrp.Position end end end end -- Jika seluruh peserta selesai, tutup balapan if allFinished and #runnerList > 0 then setRoundState("FINISHED") end end -- ========================================================== -- 6. SINKRONISASI PROGRESS KE DASHBOARD VPS -- ========================================================== local function sendRaceProgress() if #runnerList == 0 then return end local participantsPayload = {} for _, r in ipairs(runnerList) do table.insert(participantsPayload, { robloxId = r.userId, username = r.username, tiktokUser = r.tiktokUser, lap = math.min(r.currentLap, targetTotalLaps), totalLaps = targetTotalLaps, checkpoint = r.nextCheckpoint, progressPercent = r.progressPercent, speed = r.speed, rank = r.rank, finished = r.finished }) end local payload = { roundStatus = roundStatus, roundId = currentRoundId, participants = participantsPayload, leaderboard = leaderboard } pcall(function() HttpService:RequestAsync({ Url = CONFIG.API_URL .. "/race/progress", Method = "POST", Headers = { ["Content-Type"] = "application/json", ["X-Roblox-Key"] = CONFIG.API_KEY }, Body = HttpService:JSONEncode(payload) }) end) end -- Telemetry status reguler local function sendTelemetry() local playerNames = {} for _, p in ipairs(Players:GetPlayers()) do table.insert(playerNames, p.Name) end local payload = { placeId = tostring(game.PlaceId), jobId = game.JobId or "studio-local", playerCount = #playerNames, players = playerNames, spawnedNpcCount = #runnerList, roundStatus = roundStatus, status = "BRIDGE_CONNECTED", serverFps = 60 } pcall(function() HttpService:RequestAsync({ Url = CONFIG.API_URL .. "/telemetry", Method = "POST", Headers = { ["Content-Type"] = "application/json", ["X-Roblox-Key"] = CONFIG.API_KEY }, Body = HttpService:JSONEncode(payload) }) end) end -- ========================================================== -- 7. POWER-UPS APPLICATION -- ========================================================== local function executePowerUp(action) local runnerTarget = action.runner local payload = action.payload or {} local mult = payload.multiplier or 1.5 local duration = payload.durationSec or 5 for _, r in ipairs(runnerList) do if r.username == runnerTarget or runnerTarget == CONFIG.RUNNER_NAME then r.speedMultiplier = mult r.speed = raceBaseSpeed * mult if r.model and r.model:FindFirstChildOfClass("Humanoid") then r.model.Humanoid.WalkSpeed = r.speed end task.delay(duration, function() if r and r.model and r.model:FindFirstChildOfClass("Humanoid") then r.speedMultiplier = 1.0 r.speed = raceBaseSpeed if roundStatus == "RACING" then r.model.Humanoid.WalkSpeed = raceBaseSpeed end end end) end end end -- ========================================================== -- 8. POLLING LOOP & BACKEND EVENT HANDLER -- ========================================================== local function pollBridgeEvents() local success, response = pcall(function() return HttpService:RequestAsync({ Url = CONFIG.API_URL .. "/poll?limit=10", Method = "GET", Headers = { ["X-Roblox-Key"] = CONFIG.API_KEY } }) end) if not success or not response or not response.Success then return end local data = nil pcall(function() data = HttpService:JSONDecode(response.Body) end) if not data then return end -- A. Handle Round Control Commands dari Dashboard if data.roundControl then local rc = data.roundControl local action = rc.action currentRoundId = rc.roundId or currentRoundId targetTotalLaps = rc.totalLaps or targetTotalLaps raceBaseSpeed = rc.baseSpeed or raceBaseSpeed if action == "START_RACE" and roundStatus ~= "RACING" and roundStatus ~= "COUNTDOWN" then setRoundState("RACING", { totalLaps = targetTotalLaps, baseSpeed = raceBaseSpeed }) elseif action == "START_COUNTDOWN" and roundStatus ~= "COUNTDOWN" and roundStatus ~= "RACING" then setRoundState("COUNTDOWN") elseif action == "STOP_RACE" and (roundStatus == "RACING" or roundStatus == "COUNTDOWN") then setRoundState("FINISHED") elseif action == "RESET_ROUND" then setRoundState("WAITING") end end -- B. Handle Actions & Spawning if data.actions and #data.actions > 0 then local ackActionIds = {} local ackSpawned = {} for _, act in ipairs(data.actions) do if act.type == "SPAWN_PARTICIPANT_NPC" then local ok, code, modelName = spawnParticipantNpc(act) if ok then table.insert(ackActionIds, act.id) table.insert(ackSpawned, { eventId = act.id, robloxId = act.robloxId or act.userId, characterName = modelName or ("NPC_" .. tostring(act.robloxUsername)), success = true }) end elseif act.type == "SPEED_BOOST" or act.type == "MEGA_NITRO" or act.type == "EXECUTE_POWERUP" then executePowerUp(act) table.insert(ackActionIds, act.id) elseif act.type == "CLEANUP_DEMO_NPCS" then local targetRunId = act.payload and act.payload.demoRunId local removedCount = 0 for i = #runnerList, 1, -1 do local r = runnerList[i] local isDemoRunner = (r.isDemo == true) or (r.isReal == false) local matchRunId = (not targetRunId or targetRunId == "ALL_DEMO" or (r.demoRunId == targetRunId)) if isDemoRunner and matchRunId then if r.model and r.model.Parent then r.model:Destroy() end spawnedUserIds[r.userId] = nil table.remove(runnerList, i) removedCount = removedCount + 1 end end print(string.format("[SweetC Bridge] ๐Ÿงน Cleaned up %d DEMO NPC models (Run: %s).", removedCount, tostring(targetRunId or "ALL"))) table.insert(ackActionIds, act.id) end end if #ackActionIds > 0 or #ackSpawned > 0 then pcall(function() HttpService:RequestAsync({ Url = CONFIG.API_URL .. "/ack", Method = "POST", Headers = { ["Content-Type"] = "application/json", ["X-Roblox-Key"] = CONFIG.API_KEY }, Body = HttpService:JSONEncode({ actionIds = ackActionIds, spawned = ackSpawned }) }) end) end end end -- Heartbeat Thread (Polling & Telemetry) task.spawn(function() local telemetryTick = 0 local progressTick = 0 while isBridgeActive do task.wait(CONFIG.POLL_INTERVAL) pollBridgeEvents() telemetryTick = telemetryTick + CONFIG.POLL_INTERVAL if telemetryTick >= CONFIG.TELEMETRY_INTERVAL then telemetryTick = 0 sendTelemetry() end if roundStatus == "RACING" or #leaderboard > 0 then progressTick = progressTick + CONFIG.POLL_INTERVAL if progressTick >= CONFIG.RACE_SYNC_INTERVAL then progressTick = 0 sendRaceProgress() end end end end) -- Physics / Movement Stepper Thread RunService.Heartbeat:Connect(function(dt) updateRaceStep() end) print("[SweetC Bridge] โœ… Automated Racing System Active on " .. CONFIG.API_URL)