wait(10) local genv = (getgenv and getgenv()) or _G genv.__CTD_BOOTING_AT = os.clock() genv.__CTD_HEARTBEAT = os.clock() local function elevateThread() pcall(function() local setId = setthreadidentity or set_thread_identity or setidentity or (syn and syn.set_thread_identity) if setId then setId(8) end end) end elevateThread() local _cloneref = cloneref or function(o) return o end local function svc(name) local ok, s = pcall(function() return _cloneref(game:GetService(name)) end) return ok and s or game:GetService(name) end local Players = svc("Players") local ReplicatedStorage = svc("ReplicatedStorage") local RunService = svc("RunService") local UserInputService = svc("UserInputService") local VirtualUser = svc("VirtualUser") local HttpService = svc("HttpService") local Lighting = svc("Lighting") local CoreGui = svc("CoreGui") local SoundService = svc("SoundService") local StarterGui = svc("StarterGui") local LogService = svc("LogService") local ScriptContext = svc("ScriptContext") local TeleportService = svc("TeleportService") local GroupService = svc("GroupService") local GuiService = svc("GuiService") local VirtualInputManager = svc("VirtualInputManager") -- Immediately restore 3D rendering and 60 FPS at Line 1 BEFORE waiting for game/player load -- so Roblox's ReplicatedFirst / ModuleLoader never stalls during Lobby <-> Match teleports! pcall(function() RunService:Set3dRenderingEnabled(true) end) pcall(function() local capFn = setfpscap or set_fps_cap if capFn then capFn(60) end end) -- Safe non-hanging startup poll: updates __CTD_BOOTING_AT & __CTD_HEARTBEAT every 0.1s so ctd-watchdog NEVER double-spawns! do local t0 = os.clock() while not game:IsLoaded() and (os.clock() - t0 < 15) do genv.__CTD_BOOTING_AT = os.clock() genv.__CTD_HEARTBEAT = os.clock() if Players.LocalPlayer and workspace.CurrentCamera and ReplicatedStorage:FindFirstChild("ModuleLoader") then break end task.wait(0.1) end end local LocalPlayer = Players.LocalPlayer do local t0 = os.clock() while not LocalPlayer and (os.clock() - t0 < 20) do genv.__CTD_BOOTING_AT = os.clock() genv.__CTD_HEARTBEAT = os.clock() task.wait(0.1) LocalPlayer = Players.LocalPlayer end end if not LocalPlayer then return false end do local t0 = os.clock() while (not game.PlaceId or game.PlaceId == 0) and (os.clock() - t0 < 10) do genv.__CTD_BOOTING_AT = os.clock() genv.__CTD_HEARTBEAT = os.clock() task.wait(0.1) end end -- Wait up to 4.0s for the game's own ModuleLoader LocalScript to populate shared.Modules -- (NEVER call require() on game ModuleScripts from executor thread, which taints them with 'Plugin' capability!) do local t0 = os.clock() while os.clock() - t0 < 4.0 do genv.__CTD_BOOTING_AT = os.clock() genv.__CTD_HEARTBEAT = os.clock() local renv = (getrenv and getrenv()) or _G local sh = (renv and renv.shared) or shared if sh and type(sh.Modules) == "table" and sh.Modules.Network and sh.Modules.Config then break end task.wait(0.1) end end -- ============================================================================ -- 1. EPOCH GUARD & CLEANUP (SAFE RE-EXECUTION - ZERO YIELDS BEFORE createGUI) -- ============================================================================ pcall(function() if not genv.__MCP and isfile and loadfile and isfile("mcp/client.luau") then task.spawn(function() pcall(function() local fn = loadfile("mcp/client.luau") if fn then fn() end end) end) end end) genv.__CTD_EPOCH = (genv.__CTD_EPOCH or 0) + 1 local MY_EPOCH = genv.__CTD_EPOCH genv.__CTD_HEARTBEAT = os.clock() if genv.CTD_Bot and genv.CTD_Bot.MacroState then genv.CTD_Bot.MacroState.IsRecording = false genv.CTD_Bot.MacroState.IsPlaying = false end if genv.__CTD_CONNS then for _, conn in ipairs(genv.__CTD_CONNS) do pcall(function() conn:Disconnect() end) end end genv.__CTD_CONNS = {} local function trackConn(conn) if conn then table.insert(genv.__CTD_CONNS, conn) end return conn end local function isAlive() return genv.__CTD_EPOCH == MY_EPOCH end -- Always restore 3D rendering on script reload unless Headless is enabled pcall(function() RunService:Set3dRenderingEnabled(true) end) -- Clean up old UI in gethui(), CoreGui, PlayerGui & 3D Macro Markers pcall(function() if gethui then local hui = gethui() local old0 = hui and hui:FindFirstChild("CTD_AutoHub") if old0 then old0:Destroy() end end end) pcall(function() local old1 = CoreGui:FindFirstChild("CTD_AutoHub") if old1 then old1:Destroy() end end) pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local old2 = pg and pg:FindFirstChild("CTD_AutoHub") if old2 then old2:Destroy() end end) pcall(function() local oldVis = workspace:FindFirstChild("CTD_MacroVisuals") if oldVis then oldVis:Destroy() end end) -- ============================================================================ -- 2. PLACE IDS & PERSISTENT CONFIG -- ============================================================================ local PLACE_LOBBY = 132929771812494 local PLACE_MATCH = 114068177100380 local PLACE_TRADING = 85725785704440 local PLACE_AFK = 121259443358356 local CURRENT_PLACE = game.PlaceId local function refreshCurrentPlace() local pid = tonumber(game.PlaceId) or 0 if pid == PLACE_MATCH or workspace:GetAttribute("IsVoting") ~= nil or workspace:GetAttribute("Wave") ~= nil or (workspace:FindFirstChild("Game") ~= nil and workspace:FindFirstChild("Elevators") == nil) then CURRENT_PLACE = PLACE_MATCH elseif pid == PLACE_LOBBY or workspace:FindFirstChild("Elevators") ~= nil then CURRENT_PLACE = PLACE_LOBBY elseif pid ~= 0 then CURRENT_PLACE = pid end genv.__CTD_ACTIVE_PLACE = CURRENT_PLACE return CURRENT_PLACE end refreshCurrentPlace() local DEFAULT_CFG = { -- Master & Anti-Macro Enabled = true, BypassAntiMacro = true, LockTopDownCamera = false, ShowMacroVisuals = false, -- Super Max Low / True Delete Everything Optimization (Lightweight & Crash-Free) SuperMaxLow = true, DeleteEverything = true, -- Xoá sạch Map, Đường, Nhà Chính, Quái, Tướng (Giữ lại 1 SafetyPart dưới chân) DeleteGameUI = false, -- Chỉ ẩn (Enabled=false) GameUI khi cần, không can thiệp sâu gây crash SkipAllErrors = false, SafetyPlatform = true, -- Tạo 1 Part cứng dưới chân + tắt FallenPartsDestroyHeight chống rơi vực / die StripModelsToBox = true, HeadlessNo3D = true, FPSCap = "10 FPS", -- "Unlimited", "60 FPS", "30 FPS", "15 FPS", "10 FPS" -- Macro Settings SelectedMacro = "cac", PlayMacro = true, MacroTimingMode = "Cash & Wave", -- "Cash & Wave", "Cash Only", "Exact Time" RecordWaveSkips = true, QuickTowerTarget = "Latest Placed", -- Match Aux Automation AutoStarterCameraman = true, -- 100% Auto Smart Step Pipeline: Tự đọc tướng theo từng Macro Step -> Săn Banner -> Equip -> Đổi Macro/Mode/Map động! StepChainText = "~cac(Easy) > ~cac2(Easy) | ~cac3(Easy) | ~cac4(Easy) > ~cac5(Normal) | ~cac6(Normal) > cac7(Hard) > cac8(Hard):Titan Cinemaman,Glitch Cameraman", AutoVote = true, VoteMode = "Easy", -- "Easy", "Normal", "Hard", "Insane", "Nightmare", "Macro" AutoSkipWave = true, AutoSpeed2x = true, AutoAbility = true, AutoMatchEnd = true, MatchEndAction = "Smart", -- "PlayAgain", "Smart", "ReturnLobby" ReturnMoneyThreshold = 900, ReturnCandyThreshold = 500, WebhookOnMatchEnd = true, WebhookUrl = "https://discord.com/api/webhooks/1565177606343630949/VqJ5kL-o2jY9L9lbpwNYfGWqlzmCTmVLkmLkRsyFMupFgUG6U4VICUC3KxA9nA-6qmo1", -- Lobby Automation AutoJoinGroup = false, -- Tạm thời tắt Auto Join Group theo yêu cầu AutoRedeemCodes = true, AutoEquipBest = false, AutoSummon = true, SummonMinMoney = 900, AutoBuyHalloweenCrate = false, AutoOpenCrates = false, AutoClaimQuests = true, AutoClaimBattlepass = true, AutoSellDuplicates = false, KeepCopies = 2, AutoUseBoosts = false, AutoJoinElevator = true, BannerWatcherOnly = false, -- Chế độ Acc Treo Check Banner 24/7 ở Lobby -> Đẩy lên VPS 160.191.242.43:8787 PreferredMap = "Toilet City", -- "Toilet City", "Desert", "Cameraman HQ", "Any" } genv.__CTD_CONFIG = genv.__CTD_CONFIG or {} local Cfg = genv.__CTD_CONFIG for k, v in pairs(DEFAULT_CFG) do if Cfg[k] == nil then Cfg[k] = v end end -- Ensure DeleteEverything defaults to true when upgrading from old config if Cfg.DeleteEverything == nil then Cfg.DeleteEverything = true end local CONFIG_FILE = "CTD_AutoConfig_v5.json" local MACRO_FOLDER = "CTD_Macros" local function ensureMacroFolder() pcall(function() if makefolder and isfolder and not isfolder(MACRO_FOLDER) then makefolder(MACRO_FOLDER) end end) end ensureMacroFolder() local saveConfig, loadConfig do local LastSavedConfigJson = nil saveConfig = function() pcall(function() if writefile then local encoded = HttpService:JSONEncode(Cfg) if encoded ~= LastSavedConfigJson then LastSavedConfigJson = encoded writefile(CONFIG_FILE, encoded) end end end) end loadConfig = function() pcall(function() if isfile and readfile and isfile(CONFIG_FILE) then local raw = readfile(CONFIG_FILE) LastSavedConfigJson = raw local decoded = HttpService:JSONDecode(raw) if type(decoded) == "table" then for k, v in pairs(decoded) do if DEFAULT_CFG[k] ~= nil then Cfg[k] = v end end end end end) end end loadConfig() Cfg.AutoJoinGroup = false -- Tạm thời tắt Auto Join Group theo yêu cầu Cfg.AutoSkipWave = true -- Luôn bật Auto Skip Wave để không bị miss skip ở cac/cac2/cac3 Cfg.DeleteGameUI = false Cfg.SkipAllErrors = false if type(Cfg.WebhookUrl) ~= "string" or not Cfg.WebhookUrl:find("discord.com/api/webhooks/", 1, true) then Cfg.WebhookUrl = DEFAULT_CFG.WebhookUrl end if Cfg.WebhookOnMatchEnd == nil then Cfg.WebhookOnMatchEnd = true end if type(Cfg.StepChainText) ~= "string" or not Cfg.StepChainText:match("%S") or Cfg.StepChainText == "cac > cac2 > cac3 > cac3:Titan Cinemaman,Glitch Cameraman" or Cfg.StepChainText == "cac > cac2 > cac4 > cac5 > cac6 > cac7 > cac8" or Cfg.StepChainText == "cac > cac2 | cac4 > cac5 | cac6 > cac7 > cac8" or Cfg.StepChainText == "cac > cac2 | cac3 | cac4 > cac5 | cac6 > cac7 > cac8:Titan Cinemaman,Glitch Cameraman" then Cfg.StepChainText = DEFAULT_CFG.StepChainText end if (tonumber(Cfg.ReturnMoneyThreshold) or 0) < 900 then Cfg.ReturnMoneyThreshold = 900 end if (tonumber(Cfg.SummonMinMoney) or 0) < 900 then Cfg.SummonMinMoney = 900 end -- Queue on teleport so the script persists across Lobby <-> Match transitions genv.__CTD_ACTIVE_JOB = tostring(game.JobId) local function setupQueueOnTeleport(forceRequeue) if genv.__CTD_TELEPORT_QUEUED_JOB == tostring(game.JobId) and not forceRequeue then return end local qot = queue_on_teleport or queueonteleport or (syn and syn.queue_on_teleport) or (fluxus and fluxus.queue_on_teleport) if not qot then return end pcall(function() local paths = { "CTD.lua", "E:\\Desktop\\savedata\\quanh code dang cap\\savea\\.v.v\\PHIMNHATHAY\\CTD.lua", } for _, p in ipairs(paths) do if isfile and isfile(p) then qot(string.format([[ task.spawn(function() pcall(function() game:GetService("RunService"):Set3dRenderingEnabled(true) local capFn = setfpscap or set_fps_cap if capFn then capFn(60) end end) if not game:IsLoaded() then game.Loaded:Wait() end task.wait(2.5) local g = (getgenv and getgenv()) or _G local hb = tonumber(g.__CTD_HEARTBEAT) or 0 local bootAt = tonumber(g.__CTD_BOOTING_AT) or 0 local curJob = tostring(game.JobId) local isNewJob = (curJob ~= "" and g.__CTD_ACTIVE_JOB ~= curJob) if (isNewJob and (os.clock() - bootAt > 4)) or ((os.clock() - hb > 15) and (os.clock() - bootAt > 15)) then local ok, src = pcall(readfile, %q) if ok and type(src) == "string" and #src > 100 then g.__CTD_BOOTING_AT = os.clock() local fn = loadstring(src) if fn then task.spawn(fn) end end end end) ]], p)) genv.__CTD_TELEPORT_QUEUED_JOB = tostring(game.JobId) break end end end) end setupQueueOnTeleport(false) -- Restore 3D rendering & 60 FPS and re-arm queue_on_teleport right before any place transition local function prepareForTeleport() pcall(function() RunService:Set3dRenderingEnabled(true) end) pcall(function() local capFn = setfpscap or set_fps_cap if capFn then capFn(60) end end) setupQueueOnTeleport(true) end pcall(function() trackConn(LocalPlayer.OnTeleport:Connect(function() prepareForTeleport() task.defer(function() setupQueueOnTeleport(true) end) end)) end) -- Re-arm queue_on_teleport if Roblox fails a teleport attempt so multi-account farming never loses persistence pcall(function() trackConn(TeleportService.TeleportInitFailed:Connect(function(plr, teleportResult, errMsg) if plr == LocalPlayer then genv.__CTD_TELEPORT_QUEUED_JOB = nil setupQueueOnTeleport(true) end end)) end) -- Anti-AFK (All Places) trackConn(LocalPlayer.Idled:Connect(function() pcall(function() VirtualUser:CaptureController() VirtualUser:ClickButton2(Vector2.zero) end) end)) -- ============================================================================ -- 3. COMPLETE ANTI-ANTIMACRO BYPASS SYSTEM -- ============================================================================ local setupAntiMacroBypass do local LastSafeCameraCF = nil local SuppressCameraSpinUntil = 0 local CachedTopDownCenter = nil local AntiMacroBypassInit = false setupAntiMacroBypass = function() refreshCurrentPlace() if CURRENT_PLACE ~= PLACE_MATCH then return end if AntiMacroBypassInit then return end AntiMacroBypassInit = true pcall(function() local ps = LocalPlayer:FindFirstChild("PlayerScripts") if ps then local am = ps:FindFirstChild("AntiMacro") if am then pcall(function() am.Disabled = true end) pcall(function() am:Destroy() end) end trackConn(ps.ChildAdded:Connect(function(ch) if ch.Name == "AntiMacro" then task.defer(function() pcall(function() ch.Disabled = true end) pcall(function() ch:Destroy() end) end) end end)) end end) trackConn(workspace:GetAttributeChangedSignal("IsVoting"):Connect(function() if workspace:GetAttribute("IsVoting") == false then SuppressCameraSpinUntil = os.clock() + 1.5 end end)) trackConn(LocalPlayer.CharacterAdded:Connect(function() SuppressCameraSpinUntil = os.clock() + 1.5 end)) local cam = workspace.CurrentCamera if cam then LastSafeCameraCF = cam.CFrame end trackConn(RunService.RenderStepped:Connect(function() if Cfg.HeadlessNo3D then return end local curCam = workspace.CurrentCamera if not curCam then return end if Cfg.LockTopDownCamera then if not CachedTopDownCenter then local g = workspace:FindFirstChild("Game") local wps = g and g:FindFirstChild("Waypoints") local wp2 = wps and (wps:FindFirstChild("2") or wps:FindFirstChild("1")) if wp2 and wp2:IsA("BasePart") then CachedTopDownCenter = wp2.Position else CachedTopDownCenter = Vector3.new(-15, -48, 448) end end curCam.CameraType = Enum.CameraType.Scriptable curCam.CFrame = CFrame.lookAt(CachedTopDownCenter + Vector3.new(0, 85, 35), CachedTopDownCenter) return end if not Cfg.BypassAntiMacro then return end if curCam.CameraType == Enum.CameraType.Scriptable then curCam.CameraType = Enum.CameraType.Custom local char = LocalPlayer.Character local hum = char and char:FindFirstChildOfClass("Humanoid") if hum then curCam.CameraSubject = hum end if LastSafeCameraCF then curCam.CFrame = LastSafeCameraCF end elseif os.clock() < SuppressCameraSpinUntil and LastSafeCameraCF then local pos = curCam.CFrame.Position curCam.CFrame = CFrame.new(pos) * (LastSafeCameraCF - LastSafeCameraCF.Position) else LastSafeCameraCF = curCam.CFrame end end)) end end setupAntiMacroBypass() -- ============================================================================ -- 4. ACCESS GAME MODULES & REMOTESECURITY SCHEDULER (LAZY & SELF-HEALING) -- ============================================================================ local State = { StatusText = "Initializing...", LastAction = "None", MatchesPlayed = 0, WinsSession = 0, MatchStartClock = os.clock(), CurrentWave = tonumber(workspace:GetAttribute("Wave")) or 0, CurrentWaveStartClock = os.clock(), SpeedSetThisMatch = false, MatchEndHandled = false, DeletedInstancesCount = 0, LastModeVoteClock = 0, } trackConn(workspace:GetAttributeChangedSignal("Wave"):Connect(function() local w = tonumber(workspace:GetAttribute("Wave")) or 0 if w ~= State.CurrentWave then State.CurrentWave = w State.CurrentWaveStartClock = os.clock() end end)) local UIRefs = {} local function setStatus(txt) State.StatusText = tostring(txt) if UIRefs and UIRefs.ActionLabel then pcall(function() elevateThread() UIRefs.ActionLabel.Text = "Status: " .. State.StatusText end) end end local function getModules() local renv = (getrenv and getrenv()) or _G local sh = (renv and renv.shared) or shared if sh and type(sh.Modules) == "table" then return sh.Modules end return nil end local Modules = getModules() local RawNetworkGet = nil local setupAuthoritativeMacroListeners local installNetworkGetProxy local ensureGameModules, getConfigTable do ensureGameModules = function() local m = getModules() if m then Modules = m end local netTbl = Modules and Modules.Network if netTbl and not RawNetworkGet then if not netTbl._CTDRawGet and type(netTbl.Get) == "function" then netTbl._CTDRawGet = netTbl.Get end RawNetworkGet = netTbl._CTDRawGet or netTbl.Get end if setupAuthoritativeMacroListeners and CURRENT_PLACE == PLACE_MATCH then pcall(setupAuthoritativeMacroListeners) end return Modules end getConfigTable = function() ensureGameModules() return Modules and Modules.Config end end -- Run once synchronously with 0 yields so createGUI() executes immediately in the same frame! ensureGameModules() local function getReplicaData() ensureGameModules() if not Modules or not Modules.Replication then return nil end local ok, rep = pcall(function() return Modules.Replication.GetCurrentReplica() end) if ok and rep and rep.Data then return rep.Data end return nil end local function getGameReplicator() ensureGameModules() if not Modules then return nil end return Modules.GameReplicator end local function getRawRemoteInstance(remoteName) ensureGameModules() local ml = ReplicatedStorage:FindFirstChild("ModuleLoader") local sharedFolder = ml and ml:FindFirstChild("Shared") local netFolder = sharedFolder and sharedFolder:FindFirstChild("Network") if netFolder then local reFolder = netFolder:FindFirstChild("RemoteEvent") if reFolder then local r = reFolder:FindFirstChild(remoteName) if r then return r end end local rfFolder = netFolder:FindFirstChild("RemoteFunction") if rfFolder then local r = rfFolder:FindFirstChild(remoteName) if r then return r end end end for _, folderName in ipairs({ "RemoteEvents", "Remotes" }) do local f = ReplicatedStorage:FindFirstChild(folderName) if f then local direct = f:FindFirstChild(remoteName) if direct then return direct end end end if RawNetworkGet then local ok, r = pcall(RawNetworkGet, remoteName) if ok and r then if typeof(r) == "Instance" then return r elseif type(r) == "table" and typeof(r._remote) == "Instance" then return r._remote end end end return nil end local MacroState = { IsRecording = false, IsPlaying = false, RecordingMacroName = nil, ManualMacroLocked = false, CurrentStep = 1, Steps = {}, MapName = "Any", ModeName = "Easy", CustomNames = {}, RecordStartClock = 0, NextTowerOrder = 1, ServerIdToOrder = {}, RecordedTowerSnap = {}, PendingPlaceCFrame = {}, PendingPlaceList = {}, PendingAbility = {}, LastRecordedWaveSkip = -1, OrderToServerId = {}, ClaimedServerIds = {}, SoldOrders = {}, AuthoritativeTowers = {}, AbilityCooldowns = {}, StepRetryCount = 0, SelectedTowerId = nil, SelectedHotbarSlot = nil, QuickTargetSelection = "Latest Placed", VisualFolder = nil, } -- ============================================================================ -- 5. LIGHTWEIGHT & CRASH-FREE DELETE MAP ENGINE (ZERO FREEZE ON JOIN) -- ============================================================================ -- Clean & stable optimization: -- * No cache.invalidate / setfflag / sethiddenproperty / getconnections / debug.getinfo loops -- * No cloning 100+ tower models on startup -- * No destroying ReplicatedStorage.Assets or PlayerGui ScreenGuis -- * Throttled batch deletion of workspace map objects so game join never freezes or crashes! State.OrigModuleFuncs = {} State.StyledHumanoids = setmetatable({}, { __mode = "k" }) State.CachedSafetyPlatform = nil State.IsRunningSuperMaxLow = false State.WatchedGameSubFolders = setmetatable({}, { __mode = "k" }) State.GameFolderWatcherRunning = false State.EconomyLocked = false State.LastRemoteTime = {} State.TowerStatsCache = {} genv.__CTD_SAVED_TEMPLATES = genv.__CTD_SAVED_TEMPLATES or { WaypointPositions = {}, TowerAreaCFrame = nil, TowerAreaSize = nil, TowerModels = {}, } local SavedTemplates = genv.__CTD_SAVED_TEMPLATES local function safeDestroy(inst) if not inst then return end pcall(function() inst:Destroy() end) end local function applyFPSCap() local capFn = setfpscap or set_fps_cap if not capFn then return end pcall(function() local val = tostring(Cfg.FPSCap or "60 FPS") if val == "Unlimited" then capFn(999) else local num = tonumber(val:match("(%d+)")) or 60 capFn(num) end end) end local function applyHeadlessMode() pcall(function() RunService:Set3dRenderingEnabled(not Cfg.HeadlessNo3D) end) end local function ensureSafetyPlatform() if not Cfg.SafetyPlatform then return nil end local plat = State.CachedSafetyPlatform if not plat or plat.Parent ~= workspace then plat = workspace:FindFirstChild("CTD_SafetyPlatform") State.CachedSafetyPlatform = plat end pcall(function() workspace.FallenPartsDestroyHeight = 0 / 0 local char = LocalPlayer.Character local hrp = char and (char:FindFirstChild("HumanoidRootPart") or char.PrimaryPart) local hum = char and char:FindFirstChildOfClass("Humanoid") if hum and not State.StyledHumanoids[hum] then State.StyledHumanoids[hum] = true hum.RequiresNeck = false pcall(function() hum:SetStateEnabled(Enum.HumanoidStateType.Dead, false) end) pcall(function() hum:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false) end) pcall(function() hum:SetStateEnabled(Enum.HumanoidStateType.Ragdoll, false) end) end if not plat or not plat:IsA("BasePart") then if plat then safeDestroy(plat) end local gameFolder = workspace:FindFirstChild("Game") local spawnLoc = gameFolder and gameFolder:FindFirstChild("SpawnLocation") local platPos if hrp and hrp.Position.Y > -50 then platPos = Vector3.new(hrp.Position.X, hrp.Position.Y - 3.5, hrp.Position.Z) elseif spawnLoc and spawnLoc:IsA("BasePart") then platPos = spawnLoc.Position - Vector3.new(0, 2, 0) elseif SavedTemplates.TowerAreaCFrame then platPos = SavedTemplates.TowerAreaCFrame.Position else platPos = Vector3.new(0, 10, 0) end plat = Instance.new("Part") plat.Name = "CTD_SafetyPlatform" plat.Anchored = true plat.CanCollide = true plat.CanTouch = false plat.CanQuery = false plat.CastShadow = false plat.Material = Enum.Material.SmoothPlastic plat.Color = Color3.fromRGB(28, 35, 48) plat.Transparency = 0.35 plat.Size = Vector3.new(2048, 4, 2048) plat.Position = platPos plat.Parent = workspace State.CachedSafetyPlatform = plat else if not plat.Anchored then plat.Anchored = true end if not plat.CanCollide then plat.CanCollide = true end if hrp and math.abs(hrp.Position.Y - (plat.Position.Y + 3.5)) > 25 and hrp.Position.Y > -50 then plat.Position = Vector3.new(hrp.Position.X, hrp.Position.Y - 3.5, hrp.Position.Z) end end if hrp and plat and hrp.Position.Y < (plat.Position.Y - 8) then hrp.AssemblyLinearVelocity = Vector3.zero hrp.AssemblyAngularVelocity = Vector3.zero hrp.CFrame = CFrame.new(plat.Position + Vector3.new(0, 5, 0)) end end) return plat end trackConn(LocalPlayer.CharacterAdded:Connect(function(newChar) task.defer(function() if Cfg.SafetyPlatform then ensureSafetyPlatform() end local hum = newChar and newChar:FindFirstChildOfClass("Humanoid") local t0 = os.clock() while newChar and newChar.Parent and not hum and (os.clock() - t0 < 5) do task.wait(0.1) hum = newChar:FindFirstChildOfClass("Humanoid") end if hum and not State.StyledHumanoids[hum] then State.StyledHumanoids[hum] = true hum.RequiresNeck = false pcall(function() hum:SetStateEnabled(Enum.HumanoidStateType.Dead, false) end) end if Cfg.SafetyPlatform then ensureSafetyPlatform() end end) end)) local function stripModelToRootPart(model, colorOverride, makeInvisible) if not model or not model:IsA("Model") then return 0 end local root = model.PrimaryPart or model:FindFirstChild("RootPart") or model:FindFirstChild("HumanoidRootPart") or model:FindFirstChildWhichIsA("BasePart") if not root then return 0 end pcall(function() model.PrimaryPart = root end) local removed = 0 for _, ch in ipairs(model:GetChildren()) do if ch ~= root then safeDestroy(ch) removed = removed + 1 end end pcall(function() root.Anchored = true root.CanQuery = true root.Material = Enum.Material.SmoothPlastic root.CastShadow = false if colorOverride then root.Color = colorOverride end root.Transparency = makeInvisible and 1 or 0.25 end) return removed end local function backupTemplatesBeforeNuke(gameFolder) pcall(function() if gameFolder then local waypoints = gameFolder:FindFirstChild("Waypoints") if waypoints and #SavedTemplates.WaypointPositions == 0 then local wps = {} for i = 1, 60 do local wp = waypoints:FindFirstChild(tostring(i)) if wp and wp:IsA("BasePart") then table.insert(wps, wp.Position) end end if #wps >= 2 then SavedTemplates.WaypointPositions = wps end end local towerArea = gameFolder:FindFirstChild("TowerArea") if towerArea and not SavedTemplates.TowerAreaCFrame then local part = towerArea:FindFirstChildWhichIsA("BasePart") if part then SavedTemplates.TowerAreaCFrame = part.CFrame SavedTemplates.TowerAreaSize = part.Size end end end local cfgTbl = getConfigTable() if cfgTbl and type(cfgTbl.Towers) == "table" then for tName, tCfg in pairs(cfgTbl.Towers) do if type(tCfg) == "table" and tCfg.Model and not SavedTemplates.TowerModels[tName] then SavedTemplates.TowerModels[tName] = tCfg.Model end end end end) end local function buildMinimalRoadInPaths(pathsFolder, waypointsFolder) if not pathsFolder then return end if pathsFolder:FindFirstChild("CTD_MinimalRoad_1") then return end local wps = {} if waypointsFolder then for i = 1, 60 do local wp = waypointsFolder:FindFirstChild(tostring(i)) if wp and wp:IsA("BasePart") then table.insert(wps, wp.Position) end end end if #wps < 2 and #SavedTemplates.WaypointPositions >= 2 then wps = SavedTemplates.WaypointPositions end if #wps < 2 then return end for i = 1, #wps - 1 do local a = wps[i] local b = wps[i + 1] local mid = (a + b) * 0.5 local dist = (b - a).Magnitude local roadPart = Instance.new("Part") roadPart.Name = "CTD_MinimalRoad_" .. tostring(i) roadPart.Anchored = true roadPart.CanCollide = false roadPart.CanTouch = false roadPart.CanQuery = true roadPart.CastShadow = false roadPart.Material = Enum.Material.SmoothPlastic roadPart.Color = Color3.fromRGB(75, 38, 42) roadPart.Size = Vector3.new(5.2, 0.35, dist + 5.2) roadPart.CFrame = CFrame.lookAt(Vector3.new(mid.X, a.Y - 0.35, mid.Z), Vector3.new(b.X, a.Y - 0.35, b.Z)) roadPart.Parent = pathsFolder end end local syncRecordingTowerHitboxes local function restoreMinimalForRecording() refreshCurrentPlace() if CURRENT_PLACE ~= PLACE_MATCH then return end pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") if pg then local gameUI = pg:FindFirstChild("GameUI") if gameUI and gameUI:IsA("ScreenGui") then gameUI.Enabled = true end end local gameFolder = workspace:FindFirstChild("Game") if not gameFolder then return end local towerArea = gameFolder:FindFirstChild("TowerArea") if towerArea and #towerArea:GetChildren() == 0 and SavedTemplates.TowerAreaCFrame then local p = Instance.new("Part") p.Name = "Part" p.Anchored = true p.CanCollide = true p.CanQuery = true p.CastShadow = false p.Material = Enum.Material.SmoothPlastic p.Color = Color3.fromRGB(28, 33, 44) p.Size = SavedTemplates.TowerAreaSize or Vector3.new(400, 1, 400) p.CFrame = SavedTemplates.TowerAreaCFrame p.Parent = towerArea end local paths = gameFolder:FindFirstChild("Paths") local waypoints = gameFolder:FindFirstChild("Waypoints") if paths then buildMinimalRoadInPaths(paths, waypoints) end local cfgTbl = getConfigTable() if cfgTbl and type(cfgTbl.Towers) == "table" then for tName, tCfg in pairs(cfgTbl.Towers) do if type(tCfg) == "table" and not tCfg.Model and SavedTemplates.TowerModels[tName] then tCfg.Model = SavedTemplates.TowerModels[tName] end end end if syncRecordingTowerHitboxes then syncRecordingTowerHitboxes() end end) end local function deleteAllGameUIAndSeverConns() if not Cfg.DeleteGameUI or MacroState.IsRecording then return 0 end local hidden = 0 pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") if pg then for _, gui in ipairs(pg:GetChildren()) do if gui:IsA("ScreenGui") and gui.Name ~= "CTD_AutoHub" and gui.Enabled then gui.Enabled = false hidden = hidden + 1 end end end end) return hidden end local function suppressAllErrorsAndPreventRAMLeak() if not Cfg.SkipAllErrors then return 0 end pcall(function() local gr = getGameReplicator() if gr and type(gr.Data) == "table" then if type(gr.Data.Enemies) == "table" then table.clear(gr.Data.Enemies) end if type(gr.Data.Units) == "table" then table.clear(gr.Data.Units) end end end) return 0 end local function stepLightLowMaintenance() if not Cfg.SuperMaxLow then return end if Cfg.SafetyPlatform then ensureSafetyPlatform() end if Cfg.DeleteGameUI and not MacroState.IsRecording then deleteAllGameUIAndSeverConns() end end local function applySuperMaxLow() if State.IsRunningSuperMaxLow then return end State.IsRunningSuperMaxLow = true do local t0 = os.clock() while not game:IsLoaded() and (os.clock() - t0 < 5) do task.wait(0.1) end end refreshCurrentPlace() ensureGameModules() -- Always create safety platform first so character stands safely if Cfg.SafetyPlatform then ensureSafetyPlatform() end if not Cfg.SuperMaxLow then applyFPSCap() applyHeadlessMode() State.IsRunningSuperMaxLow = false return end local removed = 0 local batchCounter = 0 local function throttleDelete(inst) if not inst then return end safeDestroy(inst) removed = removed + 1 batchCounter = batchCounter + 1 if batchCounter % 20 == 0 then task.wait() end end -- 1. Basic Lighting & Terrain Cleanup (Once per session, no sethiddenproperty / setfflag) if not State.EngineLowConfiguredOnce then State.EngineLowConfiguredOnce = true pcall(function() Lighting.GlobalShadows = false Lighting.FogEnd = 9e9 Lighting.Brightness = 0 for _, ch in ipairs(Lighting:GetChildren()) do safeDestroy(ch) removed = removed + 1 end end) pcall(function() local terrain = workspace:FindFirstChildOfClass("Terrain") if terrain then terrain.WaterWaveSize = 0 terrain.WaterWaveSpeed = 0 terrain.WaterReflectance = 0 terrain:Clear() end end) end -- 2. Stub out Heavy Client Visual Effect Functions (Simple no-op, no getconnections) pcall(function() if Modules then if Modules.Effects then State.OrigModuleFuncs.ShowEffect = State.OrigModuleFuncs.ShowEffect or Modules.Effects.ShowEffect Modules.Effects.ShowEffect = function() end end if Modules.Animation then State.OrigModuleFuncs.AnimPlay = State.OrigModuleFuncs.AnimPlay or Modules.Animation.Play Modules.Animation.Play = function() return nil end Modules.Animation.Stop = function() end Modules.Animation.StopTrack = function() end end if Modules.HealthUI then Modules.HealthUI.Setup = function() return nil end Modules.HealthUI.UpdateHealth = function() end end end end) -- 3. Place-Specific Map Deletion (Throttled so 0 Freeze! Never destroy Base/Waypoints/Modifiers/SpawnLocation so GameReplicator.Init never hangs!) if CURRENT_PLACE == PLACE_MATCH then local gameFolder = workspace:FindFirstChild("Game") backupTemplatesBeforeNuke(gameFolder) if Cfg.DeleteEverything and not MacroState.IsRecording then if Cfg.SafetyPlatform then ensureSafetyPlatform() end -- Delete existing objects inside workspace.Game folders in throttled batches (preserving Waypoints, Modifiers, Base & SpawnLocation) if gameFolder then for _, sub in ipairs(gameFolder:GetChildren()) do pcall(function() if sub:IsA("Folder") then if sub.Name ~= "Waypoints" and sub.Name ~= "Modifiers" then for _, item in ipairs(sub:GetChildren()) do throttleDelete(item) end end elseif sub.Name == "Base" then local root = (sub:IsA("Model") and sub.PrimaryPart) or sub:FindFirstChildWhichIsA("BasePart") for _, item in ipairs(sub:GetChildren()) do if item ~= root and not item:IsA("Configuration") and not item:IsA("ValueBase") then throttleDelete(item) end end if root and root:IsA("BasePart") then root.Transparency = 1 root.CanCollide = false end elseif sub.Name ~= "SpawnLocation" and sub.Name ~= "Modifiers" then throttleDelete(sub) end end) end end -- Delete decorative map models in workspace for _, ch in ipairs(workspace:GetChildren()) do pcall(function() if ch.Name ~= "Game" and ch.Name ~= "Camera" and ch.Name ~= "Terrain" and ch.Name ~= "CTD_SafetyPlatform" and ch.Name ~= "CTD_MacroVisuals" and ch ~= LocalPlayer.Character and not Players:GetPlayerFromCharacter(ch) then throttleDelete(ch) end end) end elseif gameFolder then local paths = gameFolder:FindFirstChild("Paths") local waypoints = gameFolder:FindFirstChild("Waypoints") if paths then for _, ch in ipairs(paths:GetChildren()) do pcall(function() if not tostring(ch.Name):find("^CTD_MinimalRoad_") then throttleDelete(ch) end end) end buildMinimalRoadInPaths(paths, waypoints) end end elseif CURRENT_PLACE == PLACE_LOBBY then ensureSafetyPlatform() if not workspace:FindFirstChild("CTD_LobbySafetyFloor") then local floor = Instance.new("Part") floor.Name = "CTD_LobbySafetyFloor" floor.Anchored = true floor.CanCollide = true floor.Material = Enum.Material.SmoothPlastic floor.Color = Color3.fromRGB(28, 33, 44) floor.Size = Vector3.new(2048, 4, 2048) local char = LocalPlayer.Character local hrp = char and char:FindFirstChild("HumanoidRootPart") local xPos = hrp and hrp.Position.X or 581 local yPos = math.min(hrp and (hrp.Position.Y - 4) or 302, 302.5) local zPos = hrp and hrp.Position.Z or 597 floor.Position = Vector3.new(xPos, yPos, zPos) floor.Parent = workspace end for _, ch in ipairs(workspace:GetChildren()) do pcall(function() if ch.Name ~= "Elevators" and ch.Name ~= "Zones" and ch.Name ~= "Camera" and ch.Name ~= "Terrain" and ch.Name ~= "CTD_LobbySafetyFloor" and ch.Name ~= "CTD_SafetyPlatform" and ch ~= LocalPlayer.Character and not Players:GetPlayerFromCharacter(ch) then throttleDelete(ch) end end) end end if Cfg.DeleteGameUI then deleteAllGameUIAndSeverConns() end applyFPSCap() applyHeadlessMode() State.DeletedInstancesCount = State.DeletedInstancesCount + removed if removed > 0 then setStatus(string.format("⚡ Clean Delete Map: Removed %d objects (Total: %d)", removed, State.DeletedInstancesCount)) end State.IsRunningSuperMaxLow = false end -- Watch workspace.Game in Match to clean up spawned visual models (with a short delay so GameReplicator finishes synchronous spawn logic cleanly) local function hookGameSubFolder(sub) if not sub or not sub:IsA("Folder") or sub.Name == "Waypoints" or sub.Name == "Modifiers" or State.WatchedGameSubFolders[sub] then return end State.WatchedGameSubFolders[sub] = true trackConn(sub.ChildAdded:Connect(function(ch) if Cfg.SuperMaxLow and Cfg.DeleteEverything and not MacroState.IsRecording then task.delay(0.15, function() if Cfg.SuperMaxLow and Cfg.DeleteEverything and not MacroState.IsRecording then safeDestroy(ch) end end) elseif Cfg.SuperMaxLow and Cfg.StripModelsToBox and (sub.Name == "Towers" or sub.Name == "Enemies" or sub.Name == "Units") then task.delay(0.15, function() stripModelToRootPart(ch, Color3.fromRGB(60, 220, 120), false) end) end end)) end local function setupMatchGameFolderWatcher() refreshCurrentPlace() if CURRENT_PLACE ~= PLACE_MATCH then return end local g = workspace:FindFirstChild("Game") if g then for _, sub in ipairs(g:GetChildren()) do hookGameSubFolder(sub) end if not State.WatchedGameSubFolders[g] then State.WatchedGameSubFolders[g] = true trackConn(g.ChildAdded:Connect(function(sub) hookGameSubFolder(sub) end)) end return end if State.GameFolderWatcherRunning then return end State.GameFolderWatcherRunning = true task.spawn(function() while isAlive() and CURRENT_PLACE == PLACE_MATCH do local gf = workspace:FindFirstChild("Game") if gf then State.GameFolderWatcherRunning = false setupMatchGameFolderWatcher() break end task.wait(0.25) end State.GameFolderWatcherRunning = false end) end setupMatchGameFolderWatcher() local InternalRemoteCall = false local ROUTE_META = { ModeVote = { Func = false, Economy = false, Delay = 0.55 }, WaveSkip = { Func = false, Economy = false, Delay = 0.55 }, ChangeSpeed = { Func = true, Economy = true, Delay = 0.55 }, TowerAdd = { Func = false, Economy = true, Delay = 0.25 }, TowerUpgrade = { Func = false, Economy = true, Delay = 0.20 }, TowerTarget = { Func = false, Economy = true, Delay = 0.20 }, TowerSell = { Func = false, Economy = true, Delay = 0.20 }, TowerAbility = { Func = false, Economy = true, Delay = 0.25 }, PlayAgain = { Func = false, Economy = false, Delay = 1.10 }, OnLeave = { Func = false, Economy = true, Delay = 3.10 }, TeleportBack = { Func = false, Economy = true, Delay = 3.10 }, SummonTowers = { Func = true, Economy = true, Delay = 2.15 }, GetSummonSet = { Func = true, Economy = false, Delay = 0.55 }, AutosellEnable = { Func = false, Economy = true, Delay = 0.30 }, EquipPart = { Func = false, Economy = true, Delay = 0.32 }, SellPart = { Func = false, Economy = true, Delay = 0.85 }, BuyCrate = { Func = false, Economy = true, Delay = 1.10 }, OpenCrate = { Func = true, Economy = true, Delay = 2.15 }, UseBoost = { Func = false, Economy = true, Delay = 0.55 }, RedeemCode = { Func = false, Economy = true, Delay = 1.05 }, RequestQuestData = { Func = false, Economy = true, Delay = 1.15 }, CollectQuest = { Func = true, Economy = true, Delay = 0.60 }, CollectPassQuest = { Func = true, Economy = true, Delay = 0.60 }, ClaimTier = { Func = true, Economy = true, Delay = 0.50 }, ElevatorEnter = { Func = true, Economy = false, Delay = 0.55 }, ElevatorSetModifiers = { Func = true, Economy = false, Delay = 0.55 }, ElevatorStart = { Func = true, Economy = false, Delay = 3.10 }, ElevatorLeave = { Func = true, Economy = false, Delay = 0.55 }, } -- Timeout-protected RemoteFunction:InvokeServer so a lagging/teleporting Roblox server can NEVER freeze the automation loop! local function invokeServerWithTimeout(remote, timeoutSec, ...) local args = { ... } local done = false local callOk, r1, r2 = false, "InvokeTimeout", nil task.spawn(function() local ok, a, b = pcall(function() return remote:InvokeServer(table.unpack(args)) end) if not done then done = true callOk, r1, r2 = ok, a, b end end) local t0 = os.clock() local limit = tonumber(timeoutSec) or 6.0 while not done and isAlive() and (os.clock() - t0 < limit) do task.wait(0.04) end done = true return callOk, r1, r2 end local function callRemote(name, ...) if not isAlive() then return false, "Stopped" end ensureGameModules() if name == "ElevatorStart" or name == "PlayAgain" or name == "TeleportBack" then prepareForTeleport() end local meta = ROUTE_META[name] or { Func = false, Economy = true, Delay = 0.35 } local args = { ... } if meta.Economy then local waitStart = os.clock() while State.EconomyLocked and isAlive() do if os.clock() - waitStart > 6.5 then State.EconomyLocked = false break end task.wait(0.05) end State.EconomyLocked = true end local remote = nil if RawNetworkGet then local ok, r = pcall(function() return RawNetworkGet(name) end) if ok and r then remote = r end end if not remote then remote = getRawRemoteInstance(name) end if not remote then if meta.Economy then State.EconomyLocked = false end return false, "RemoteNotFound" end local isRemoteFunc = meta.Func or (typeof(remote) == "Instance" and remote:IsA("RemoteFunction")) or (type(remote) == "table" and remote.IsA and remote:IsA("RemoteFunction")) local callOk, res1, res2 local maxAttempts = isRemoteFunc and 3 or 1 for attempt = 1, maxAttempts do local now = os.clock() local prev = State.LastRemoteTime[name] or 0 local elapsed = now - prev if elapsed < meta.Delay then task.wait(meta.Delay - elapsed) end State.LastRemoteTime[name] = os.clock() InternalRemoteCall = true if isRemoteFunc then callOk, res1, res2 = invokeServerWithTimeout(remote, 6.0, table.unpack(args)) else callOk, res1, res2 = pcall(function() remote:FireServer(table.unpack(args)) return true end) end InternalRemoteCall = false State.LastRemoteTime[name] = os.clock() if isRemoteFunc and callOk and res1 == false and (res2 == "BUSY" or res2 == "RATE_LIMIT" or res2 == "SERVER_BUSY") and attempt < maxAttempts then task.wait(math.max(meta.Delay, 0.55)) else break end end if meta.Economy then local unlockDelay = isRemoteFunc and 0.12 or 0.42 task.delay(unlockDelay, function() State.EconomyLocked = false end) end if not callOk then return false, res1 end return true, res1, res2 end -- ============================================================================ -- 6. TOWER ANALYTICS & MATCH HELPERS -- ============================================================================ local RARITY_RANK = { Basic = 1, Uncommon = 2, Rare = 3, Epic = 4, Legendary = 5, Mythic = 6, Exclusive = 7, Godly = 8, } local function calcLevelDPS(uEntry) if type(uEntry) ~= "table" then return 0 end local dmg = tonumber(uEntry.Damage) or 0 local cd = tonumber(uEntry.Cooldown) or 1 local dps = (cd > 0) and (dmg / cd) or 0 if type(uEntry.SecondaryAttacks) == "table" then for _, sec in pairs(uEntry.SecondaryAttacks) do if type(sec) == "table" then local sDmg = tonumber(sec.Damage) or 0 local sCd = tonumber(sec.Cooldown) or 1 if sCd > 0 then dps = dps + (sDmg / sCd) end end end end if tonumber(uEntry.AllyDPS) and tonumber(uEntry.AllyDPS) > 0 then dps = dps + tonumber(uEntry.AllyDPS) end return dps end local function getTowerAnalysis(towerName) if State.TowerStatsCache[towerName] then return State.TowerStatsCache[towerName] end local cfgTbl = getConfigTable() local cfgTowers = cfgTbl and cfgTbl.Towers local tCfg = cfgTowers and cfgTowers[towerName] if not tCfg then return nil end local upgrades = tCfg.Upgrades or {} local maxLevel = #upgrades if maxLevel == 0 then for k in pairs(upgrades) do local n = tonumber(k) if n and n > maxLevel then maxLevel = n end end end if maxLevel == 0 then maxLevel = 1 end local u1 = upgrades[1] or upgrades["1"] or tCfg local uMax = upgrades[maxLevel] or upgrades[tostring(maxLevel)] or u1 local priceL1 = tonumber(u1.Price) or tonumber(tCfg.Price) or 100 local rangeL1 = tonumber(u1.Range) or tonumber(tCfg.Range) or 6 local rangeMax = tonumber(uMax.Range) or rangeL1 local totalCost = 0 local maxCash = 0 local isMedic = (tCfg.IsMedic == true) local isSpawner = (tCfg.SpawnUnit ~= nil) or (tCfg.Tags and tCfg.Tags.Spawner ~= nil) local isAOE = (tCfg.AttackType == "AOE") or (tCfg.Tags and tCfg.Tags.AOE ~= nil) local hideDet = (tCfg.HideDetection == true) for lvl = 1, maxLevel do local upg = upgrades[lvl] or upgrades[tostring(lvl)] if type(upg) == "table" then totalCost = totalCost + (tonumber(upg.Price) or 0) if tonumber(upg.CashPerWave) and tonumber(upg.CashPerWave) > maxCash then maxCash = tonumber(upg.CashPerWave) end if upg.IsMedic then isMedic = true end if upg.Unit or upg.SpawnUnit then isSpawner = true end if upg.AOE or upg.AttackType == "AOE" or upg.SplashRadius then isAOE = true end if upg.HideDetection then hideDet = true end end end local dpsL1 = calcLevelDPS(u1) local totalMaxDPS = calcLevelDPS(uMax) local isFarm = (maxCash > 0) or (tCfg.Tags and tCfg.Tags.Farm ~= nil) local rarity = tCfg.Rarity or "Basic" local rarityRank = RARITY_RANK[rarity] or 1 local combatScore = totalMaxDPS if isAOE then combatScore = combatScore * 1.25 end if isSpawner then combatScore = math.max(combatScore, rarityRank * 650) end if not hideDet then combatScore = combatScore * 1.05 end local info = { Name = towerName, Rarity = rarity, RarityRank = rarityRank, PriceL1 = priceL1, RangeL1 = rangeL1, TotalCost = totalCost, MaxLevel = maxLevel, DPS_L1 = dpsL1, MaxDPS = totalMaxDPS, MaxRange = rangeMax, IsFarm = isFarm, MaxCashPerWave = maxCash, IsMedic = isMedic, IsSpawner = isSpawner, IsAOE = isAOE, HideDetection = hideDet, PlacementLimit = tonumber(tCfg.PlacementLimit) or 15, HasAbility = (tCfg.Ability ~= nil) or isMedic, CombatScore = combatScore, Config = tCfg, } State.TowerStatsCache[towerName] = info return info end local function getNextUpgradeCost(towerName, currentLevel) local cfgTbl = getConfigTable() local tCfg = cfgTbl and cfgTbl.Towers and cfgTbl.Towers[towerName] if not tCfg or type(tCfg.Upgrades) ~= "table" then return nil end local nextLvl = (tonumber(currentLevel) or 1) + 1 local upg = tCfg.Upgrades[nextLvl] or tCfg.Upgrades[tostring(nextLvl)] if not upg then return nil end return tonumber(upg.Price) end -- Luôn tìm UUID động theo Tên Tướng (TowerName) từ kho của chính tài khoản đang chạy! -- Tuyệt đối KHÔNG dùng UnitId lưu cứng trong file macro để 1 macro chạy chuẩn 100% trên mọi tài khoản khác nhau. local function findEquippedUUIDByName(towerName) local data = getReplicaData() if data and type(data.Towers) == "table" then -- Ưu tiên 1: Tìm đúng tướng đang được Equip trong Hotbar của tài khoản hiện tại for uuid, unit in pairs(data.Towers) do if type(unit) == "table" and unit.Name == towerName and (unit.Equipped == true or (tonumber(unit.Equipped) and tonumber(unit.Equipped) > 0)) then return tostring(uuid) end end -- Ưu tiên 2: Tìm bất kỳ bản sao nào của tướng đó trong kho của tài khoản hiện tại for uuid, unit in pairs(data.Towers) do if type(unit) == "table" and unit.Name == towerName then return tostring(uuid) end end end return nil end local function getMyMatchCash() local ls = LocalPlayer:FindFirstChild("leaderstats") local lsMoney = ls and ls:FindFirstChild("Money") if lsMoney and lsMoney:IsA("ValueBase") then return tonumber(lsMoney.Value) or 0 end local moneyObj = LocalPlayer:FindFirstChild("Money") if moneyObj and moneyObj:IsA("ValueBase") then return tonumber(moneyObj.Value) or 0 end return 0 end local function getMyPlacedTowerCount() local ptObj = LocalPlayer:FindFirstChild("PlacedTowers") if ptObj and ptObj:IsA("ValueBase") then return tonumber(ptObj.Value) or 0 end return 0 end local function isUserInJsonAttr(attrName) local raw = workspace:GetAttribute(attrName) if type(raw) == "string" and raw ~= "" then local ok, list = pcall(function() return HttpService:JSONDecode(raw) end) if ok and type(list) == "table" then for _, uid in ipairs(list) do if tonumber(uid) == LocalPlayer.UserId then return true end end end end return false end local function isMyTowerData(tData) if type(tData) ~= "table" then return false end if tData.OwnerUserId == LocalPlayer.UserId or tData.OwnerName == LocalPlayer.Name or tData.Owner == LocalPlayer or tData.Owner == LocalPlayer.Name or tData.OwnerId == LocalPlayer.UserId then return true end -- In solo matches or delta updates where Owner fields are omitted, verify via AuthoritativeTowers local id = tData.Id and tostring(tData.Id) or (tData.Index and tostring(tData.Index)) if id and MacroState.AuthoritativeTowers[id] then return true end return false end local CachedLiveTowers = {} local CachedLiveTowersClock = 0 local function invalidateLiveTowersCache() CachedLiveTowersClock = 0 end local function getLivePlacedTowers() local now = os.clock() if now - CachedLiveTowersClock < 0.03 and CachedLiveTowers then return CachedLiveTowers end local myTowers = {} local merged = {} local gr = getGameReplicator() local grTowers = gr and gr.Data and gr.Data.Towers local hasGRTable = (type(grTowers) == "table") if hasGRTable then for id, tData in pairs(grTowers) do if type(tData) == "table" then local tid = tostring(tData.Id or id) merged[tid] = tData -- Keep AuthoritativeTowers synced with latest server replication if isMyTowerData(tData) then local prev = MacroState.AuthoritativeTowers[tid] or {} MacroState.AuthoritativeTowers[tid] = { Id = tid, Name = tData.Name or prev.Name, UnitId = tData.UnitId or prev.UnitId, Level = tonumber(tData.Level) or prev.Level or 1, Index = tonumber(tData.Index) or prev.Index or 999, TargetMode = tostring(tData.TargetMode or prev.TargetMode or "First"), Pos = tData.HomePos or tData.Pos or tData.Position or prev.Pos, CFrame = tData.HomeCFrame or tData.CFrame or prev.CFrame, OwnerUserId = tData.OwnerUserId or prev.OwnerUserId or LocalPlayer.UserId, OwnerName = tData.OwnerName or prev.OwnerName or LocalPlayer.Name, Stunned = tData.Stunned == true, AddedClock = prev.AddedClock or now, SeenInGR = true, } end end end end -- Include any brand-new towers tracked by AuthoritativeTowers (< 1.5s old) that haven't replicated to gr.Data.Towers yet, -- while purging any tower that was previously in gr.Data.Towers and has now been removed/sold! for tid, authData in pairs(MacroState.AuthoritativeTowers) do if not merged[tid] then if hasGRTable and (authData.SeenInGR or (now - (authData.AddedClock or 0) > 1.5)) then MacroState.AuthoritativeTowers[tid] = nil else merged[tid] = authData end end end for id, tData in pairs(merged) do if type(tData) == "table" and isMyTowerData(tData) then local tid = tostring(tData.Id or id) local auth = MacroState.AuthoritativeTowers[tid] local pos = tData.HomePos or tData.Pos or tData.Position or (auth and auth.Pos) if typeof(pos) ~= "Vector3" and typeof(tData.CFrame) == "CFrame" then pos = tData.CFrame.Position end local cf = tData.HomeCFrame or tData.CFrame or (auth and auth.CFrame) or (typeof(pos) == "Vector3" and CFrame.new(pos)) table.insert(myTowers, { Id = tid, Name = tData.Name or (auth and auth.Name), UnitId = tData.UnitId or (auth and auth.UnitId), Level = tonumber(tData.Level) or (auth and auth.Level) or 1, Index = tonumber(tData.Index) or (auth and auth.Index) or 999, TargetMode = tostring(tData.TargetMode or (auth and auth.TargetMode) or "First"), Pos = pos, CFrame = cf, Stunned = tData.Stunned == true, Raw = tData, }) end end table.sort(myTowers, function(a, b) if a.Index ~= b.Index then return a.Index < b.Index end return a.Id < b.Id end) CachedLiveTowers = myTowers CachedLiveTowersClock = now return myTowers end -- Keep clickable 1-part hitbox Models named in workspace.Game.Towers during Macro Recording -- so native UI.GameUI's GetClickedTower() raycast AND our fallback click selector can select & sell (Q) / upgrade (E) any placed tower! syncRecordingTowerHitboxes = function() if CURRENT_PLACE ~= PLACE_MATCH then return end if Cfg.SuperMaxLow and Cfg.DeleteEverything and not MacroState.IsRecording then return end pcall(function() local gameFolder = workspace:FindFirstChild("Game") local towersFolder = gameFolder and gameFolder:FindFirstChild("Towers") if not towersFolder then return end local liveTowers = getLivePlacedTowers() local liveById = {} for idx, t in ipairs(liveTowers) do local tid = tostring(t.Id) liveById[tid] = t local ord = MacroState.ServerIdToOrder[tid] or MacroState.ClaimedServerIds[tid] or idx local m = towersFolder:FindFirstChild(tid) if not m or not m:IsA("Model") then if m then safeDestroy(m) end m = Instance.new("Model") m.Name = tid local box = Instance.new("Part") box.Name = "HumanoidRootPart" box.Anchored = true box.CanCollide = false box.CanTouch = false box.CanQuery = true box.CastShadow = false box.Material = Enum.Material.SmoothPlastic box.Color = (MacroState.SelectedTowerId == tid) and Color3.fromRGB(255, 215, 60) or Color3.fromRGB(60, 220, 120) box.Transparency = 0.25 box.Size = Vector3.new(3.2, 5.0, 3.2) box.CFrame = t.CFrame or CFrame.new(t.Pos) box.Parent = m local bb = Instance.new("BillboardGui") bb.Name = "CTD_TowerTag" bb.Size = UDim2.new(0, 130, 0, 28) bb.StudsOffset = Vector3.new(0, 3.5, 0) bb.AlwaysOnTop = true bb.Parent = box local lbl = Instance.new("TextLabel") lbl.Name = "Label" lbl.Size = UDim2.new(1, 0, 1, 0) lbl.BackgroundTransparency = 0.25 lbl.BackgroundColor3 = Color3.fromRGB(16, 20, 30) lbl.TextColor3 = Color3.fromRGB(120, 255, 160) lbl.Font = Enum.Font.GothamBold lbl.TextSize = 11 lbl.Text = string.format("#%s %s (L%d) [%s]", tostring(ord), tostring(t.Name), tonumber(t.Level) or 1, tostring(t.TargetMode or "First")) lbl.Parent = bb local uc = Instance.new("UICorner") uc.CornerRadius = UDim.new(0, 5) uc.Parent = lbl m.PrimaryPart = box m.Parent = towersFolder else local root = m.PrimaryPart or m:FindFirstChildWhichIsA("BasePart") if root then root.CanQuery = true if typeof(t.Pos) == "Vector3" and (root.Position - t.Pos).Magnitude > 0.2 then root.CFrame = t.CFrame or CFrame.new(t.Pos) end root.Color = (MacroState.SelectedTowerId == tid) and Color3.fromRGB(255, 215, 60) or Color3.fromRGB(60, 220, 120) local bb = root:FindFirstChild("CTD_TowerTag") local lbl = bb and bb:FindFirstChild("Label") if lbl and lbl:IsA("TextLabel") then lbl.Text = string.format("#%s %s (L%d) [%s]", tostring(ord), tostring(t.Name), tonumber(t.Level) or 1, tostring(t.TargetMode or "First")) end end end end -- Immediately remove any hitbox model whose tower was sold/removed! for _, ch in ipairs(towersFolder:GetChildren()) do if not liveById[ch.Name] then safeDestroy(ch) end end end) end -- ============================================================================ -- 7. PRECISION MACRO RECORDER & PLAYER SYSTEM (SERVER-VERIFIED & SPAM-PROOF) -- ============================================================================ -- Key Architecture Improvements: -- 1. Server-Confirmed Recording (OnClientEvent): -- * NEVER records unverified client :FireServer spam (e.g. clicking Upgrade/Place -- 50 times without enough cash, or clicking Ability while on cooldown). -- * Records TowerAdd, TowerUpgrade, TowerTarget, TowerSell, TowerAbility ONLY when -- the server confirms execution via OnClientEvent / state change! -- * Coalesces rapid TargetMode cycling (First -> Last -> Closest -> Strong) on the -- same tower into a single clean TowerTarget step! -- 2. 1-to-1 Claimed Tower Binding (ClaimedServerIds + OrderToServerId): -- * Every placed tower (#1..#15) claims its unique Server UUID in ClaimedServerIds[id] = order. -- * Prevents neighbor towers placed < 3.2 studs apart from ever colliding or stealing each other's IDs! -- 3. Strict Action Completion Verification Before Advancing CurrentStep: -- * TowerAdd: Waits until a brand-new unclaimed server tower ID actually spawns at Pos. -- * TowerUpgrade: Waits until server confirms targetTower.Level >= toLevel. -- * TowerTarget: Cycles one verified server response at a time until TargetMode == desiredMode. -- * TowerSell: Waits until server confirms the tower ID is removed from getLivePlacedTowers(). -- * Self-Healing: If a tower for TowerUpgrade/TowerTarget is missing on the map, automatically -- looks up its recorded TowerAdd step and places it first! local function vec3ToTable(v) if typeof(v) == "Vector3" then return { math.floor(v.X * 1000 + 0.5) / 1000, math.floor(v.Y * 1000 + 0.5) / 1000, math.floor(v.Z * 1000 + 0.5) / 1000 } elseif type(v) == "table" then return { tonumber(v[1]) or 0, tonumber(v[2]) or 0, tonumber(v[3]) or 0 } end return { 0, 0, 0 } end local function tableToVec3(t) if type(t) == "table" then return Vector3.new(tonumber(t[1]) or 0, tonumber(t[2]) or 0, tonumber(t[3]) or 0) end return Vector3.zero end local function cfToTable(cf) if typeof(cf) == "CFrame" then local comps = { cf:GetComponents() } for i = 1, #comps do comps[i] = math.floor(comps[i] * 10000 + 0.5) / 10000 end return comps elseif type(cf) == "table" then return cf end return nil end local function tableToCF(t, fallbackPos) if type(t) == "table" and #t >= 12 then return CFrame.new(table.unpack(t, 1, 12)) elseif typeof(fallbackPos) == "Vector3" then return CFrame.new(fallbackPos) end return CFrame.new() end local function dist2D(a, b) if typeof(a) ~= "Vector3" or typeof(b) ~= "Vector3" then return 999999 end return (Vector2.new(a.X, a.Z) - Vector2.new(b.X, b.Z)).Magnitude end local function resetMacroPlaybackMappings() MacroState.CurrentStep = 1 MacroState.OrderToServerId = {} MacroState.ClaimedServerIds = {} MacroState.SoldOrders = {} MacroState.StepRetryCount = 0 end local function getMacroFilePath(macroName) ensureMacroFolder() local clean = tostring(macroName or "Default"):gsub("[^%w_%-]", "_") if clean == "" then clean = "Default" end return MACRO_FOLDER .. "/" .. clean .. ".json" end local function listSavedMacros() ensureMacroFolder() local names = { "Default", "cac", "cac2", "cac3", "cac4", "cac5", "cac6", "cac7", "cac8", "Macro_1", "Macro_2", "Macro_3", "ToiletCity", "Desert", "CameramanHQ" } local seen = {} for _, n in ipairs(names) do seen[n] = true end for _, n in ipairs(MacroState.CustomNames) do if not seen[n] then seen[n] = true table.insert(names, n) end end pcall(function() if listfiles and isfolder and isfolder(MACRO_FOLDER) then for _, f in ipairs(listfiles(MACRO_FOLDER)) do local base = tostring(f):match("([^/\\]+)%.json$") if base and not seen[base] then seen[base] = true table.insert(names, base) end end end end) return names end local function refreshMacroVisuals() pcall(function() local old = workspace:FindFirstChild("CTD_MacroVisuals") if old then safeDestroy(old) end end) if not Cfg.ShowMacroVisuals or CURRENT_PLACE ~= PLACE_MATCH then return end if Cfg.SuperMaxLow and Cfg.DeleteEverything and not MacroState.IsRecording then return end if #MacroState.Steps == 0 then return end local folder = Instance.new("Folder") folder.Name = "CTD_MacroVisuals" folder.Parent = workspace MacroState.VisualFolder = folder local towerSummary = {} for idx, step in ipairs(MacroState.Steps) do if step.Action == "TowerAdd" and step.Pos then local ord = step.TowerOrder or idx towerSummary[ord] = { Order = ord, Name = step.TowerName or "Tower", Pos = tableToVec3(step.Pos), MaxLvl = 1, Sold = false, } elseif step.Action == "TowerUpgrade" and step.TowerOrder and towerSummary[step.TowerOrder] then towerSummary[step.TowerOrder].MaxLvl = math.max(towerSummary[step.TowerOrder].MaxLvl, tonumber(step.ToLevel) or 2) elseif step.Action == "TowerSell" and step.TowerOrder and towerSummary[step.TowerOrder] then towerSummary[step.TowerOrder].Sold = true end end for ord, info in pairs(towerSummary) do local marker = Instance.new("Part") marker.Name = "MacroSpot_" .. tostring(ord) marker.Shape = Enum.PartType.Cylinder marker.Anchored = true marker.CanCollide = false marker.CanQuery = false marker.CanTouch = false marker.CastShadow = false marker.Material = Enum.Material.Neon marker.Size = Vector3.new(0.25, 2.6, 2.6) marker.Color = info.Sold and Color3.fromRGB(255, 140, 60) or Color3.fromRGB(0, 225, 255) marker.Transparency = 0.35 marker.CFrame = CFrame.new(info.Pos) * CFrame.Angles(0, 0, math.rad(90)) marker.Parent = folder local bb = Instance.new("BillboardGui") bb.Size = UDim2.new(0, 120, 0, 34) bb.StudsOffset = Vector3.new(0, 3.2, 0) bb.AlwaysOnTop = true bb.Parent = marker local txt = Instance.new("TextLabel") txt.Size = UDim2.new(1, 0, 1, 0) txt.BackgroundTransparency = 0.25 txt.BackgroundColor3 = Color3.fromRGB(16, 20, 30) txt.TextColor3 = Color3.fromRGB(100, 235, 255) txt.Font = Enum.Font.GothamBold txt.TextSize = 11 txt.Text = string.format("#%d %s (L%d)", ord, info.Name, info.MaxLvl) txt.Parent = bb local c = Instance.new("UICorner") c.CornerRadius = UDim.new(0, 5) c.Parent = txt end end local function saveMacroToDisk(macroName, silentStatus) local targetName = tostring(macroName or (MacroState.IsRecording and MacroState.RecordingMacroName) or Cfg.SelectedMacro or "Default") local path = getMacroFilePath(targetName) local payload = { Name = targetName, Map = MacroState.MapName or tostring(workspace:GetAttribute("Map") or "Any"), Mode = MacroState.ModeName or tostring(workspace:GetAttribute("Mode") or "Easy"), StepCount = #MacroState.Steps, SavedAt = os.time(), Steps = MacroState.Steps, } local ok, err = pcall(function() if writefile then writefile(path, HttpService:JSONEncode(payload)) end end) if not silentStatus then if ok then setStatus(string.format("💾 Saved Macro '%s' (%d steps)", tostring(payload.Name), #MacroState.Steps)) else setStatus("Save Macro Error: " .. tostring(err)) end end refreshMacroVisuals() return ok end local function loadMacroFromDisk(macroName) local targetName = tostring(macroName or Cfg.SelectedMacro or "Default") Cfg.SelectedMacro = targetName -- NOTE: Do NOT call saveConfig() here so 50 accounts on the same PC at different starter stages -- (cac vs cac2 vs cac3) never overwrite each other's shared CTD_AutoConfig_v5.json! if MacroState.IsRecording then -- If the user selects a new macro while recording, switch the recording target to the newly selected macro! MacroState.RecordingMacroName = targetName end local path = getMacroFilePath(targetName) local loaded = false pcall(function() if isfile and readfile and isfile(path) then local decoded = HttpService:JSONDecode(readfile(path)) if type(decoded) == "table" and type(decoded.Steps) == "table" then for _, s in ipairs(decoded.Steps) do if type(s) == "table" then s.UnitId = nil -- Luôn xoá UnitId để macro dùng chung cho mọi tài khoản theo TowerName! end end MacroState.Steps = decoded.Steps MacroState.MapName = decoded.Map or "Any" MacroState.ModeName = decoded.Mode or "Easy" MacroState.LoadedSavedAt = tonumber(decoded.SavedAt) or 0 MacroState.LoadedMacroName = targetName resetMacroPlaybackMappings() loaded = true end end end) if loaded then setStatus(string.format("📂 Loaded Macro '%s' (%d steps)", targetName, #MacroState.Steps)) else MacroState.Steps = {} MacroState.LoadedSavedAt = 0 MacroState.LoadedMacroName = targetName resetMacroPlaybackMappings() setStatus(string.format("📂 Selected Macro '%s' (0 steps - Ready to Record)", targetName)) end refreshMacroVisuals() return loaded end local function deleteMacroFileFromDisk(macroName) local target = tostring(macroName or Cfg.SelectedMacro or "Default") local path = getMacroFilePath(target) local deleted = false pcall(function() if isfile and delfile and isfile(path) then delfile(path) deleted = true end end) for i = #MacroState.CustomNames, 1, -1 do if MacroState.CustomNames[i] == target then table.remove(MacroState.CustomNames, i) end end MacroState.Steps = {} resetMacroPlaybackMappings() refreshMacroVisuals() if deleted then setStatus(string.format("🗑️ Deleted Macro File '%s' from disk (delfile)", target)) else setStatus(string.format("🗑️ Cleared Macro '%s' (no disk file found)", target)) end return deleted end -- ============================================================================ -- 7B. CLOUD MACRO SYNC & AUTO-DOWNLOAD / AUTO-UPDATE (VPS 160.191.242.43:8787) -- ============================================================================ local MACROS_VPS_URL = "http://160.191.242.43:8787/macros" local MACRO_SYNC_META_FILE = MACRO_FOLDER .. "/_sync_meta.json" local LastMacroVPSSync = 0 -- Timeout-protected HTTP request wrapper (8s max) so no network call can ever freeze a thread! local function httpRequestSafe(opts, timeoutSec) local reqFn = request or http_request or (syn and syn.request) or (http and http.request) local done = false local resObj = nil task.spawn(function() local ok, r = pcall(function() if reqFn then return reqFn(opts) elseif opts.Method == "GET" and game.HttpGet then local body = game:HttpGet(opts.Url) return { StatusCode = 200, Body = body } end return nil end) if not done and ok then done = true resObj = r else done = true end end) local t0 = os.clock() local limit = tonumber(timeoutSec) or 8.0 while not done and isAlive() and (os.clock() - t0 < limit) do task.wait(0.05) end done = true return resObj end local function checkAndReloadSelectedMacroFromDiskIfNewer() if MacroState.IsRecording then return false end local sel = tostring(Cfg.SelectedMacro or "Default") local path = getMacroFilePath(sel) local diskNewer = false pcall(function() if isfile and readfile and isfile(path) then local loc = HttpService:JSONDecode(readfile(path)) if type(loc) == "table" and type(loc.Steps) == "table" and #loc.Steps > 0 then local diskSavedAt = tonumber(loc.SavedAt) or 0 if MacroState.LoadedMacroName ~= sel or #MacroState.Steps == 0 or (diskSavedAt > (MacroState.LoadedSavedAt or 0)) then diskNewer = true end end end end) if diskNewer then loadMacroFromDisk(sel) return true end return false end local function uploadMacroToVPS(macroName, silentStatus) local targetName = tostring(macroName or Cfg.SelectedMacro or "Default"):gsub("[^%w_%-]", "_") if targetName == "" then targetName = "Default" end local payload = nil local path = getMacroFilePath(targetName) pcall(function() if isfile and readfile and isfile(path) then local decoded = HttpService:JSONDecode(readfile(path)) if type(decoded) == "table" and type(decoded.Steps) == "table" then payload = decoded end end end) if not payload and tostring(Cfg.SelectedMacro) == targetName and #MacroState.Steps > 0 then payload = { Name = targetName, Map = MacroState.MapName or tostring(workspace:GetAttribute("Map") or "Toilet City"), Mode = MacroState.ModeName or tostring(workspace:GetAttribute("Mode") or "Easy"), StepCount = #MacroState.Steps, SavedAt = os.time(), Steps = MacroState.Steps, } end if not payload or type(payload.Steps) ~= "table" or #payload.Steps == 0 then if not silentStatus then setStatus(string.format("⚠️ Không thể Upload Macro '%s' (0 steps / rỗng)!", targetName)) end return false end for _, s in ipairs(payload.Steps) do if type(s) == "table" then s.UnitId = nil end end payload.Name = targetName payload.StepCount = #payload.Steps payload.SavedAt = tonumber(payload.SavedAt) or os.time() local reqFn = request or http_request or (syn and syn.request) or (http and http.request) if not reqFn then if not silentStatus then setStatus("⚠️ Executor không hỗ trợ HTTP POST để upload macro!") end return false end local res = httpRequestSafe({ Url = MACROS_VPS_URL, Method = "POST", Headers = { ["Content-Type"] = "application/json", ["User-Agent"] = "CTD-Client-MacroUpload/1.0", }, Body = HttpService:JSONEncode({ Name = targetName, Macro = payload, }), }, 8.0) if res and (res.StatusCode == 200 or res.StatusCode == nil) then if not silentStatus then setStatus(string.format("☁️ Đã Upload Macro '%s' (%d steps) lên VPS Server!", targetName, #payload.Steps)) end return true else if not silentStatus then setStatus(string.format("⚠️ Lỗi Upload Macro '%s' lên VPS!", targetName)) end return false end end local function syncMacrosFromVPS(forceOverwrite, silentIfNoChange) ensureMacroFolder() LastMacroVPSSync = os.clock() -- Multi-Account Deduplication on the same PC: -- If another account on this PC already synced with the VPS within the last 90 seconds (and all core macros exist locally), -- simply check if the local disk file was updated and skip redundant HTTP downloads! if not forceOverwrite then local allCoreExist = true for _, coreName in ipairs({ "Default", "cac", "cac2", "cac3", "cac4", "cac5", "cac6", "cac7", "cac8" }) do local p = getMacroFilePath(coreName) if not (isfile and isfile(p)) then allCoreExist = false break end end if allCoreExist and isfile and readfile and isfile(MACRO_SYNC_META_FILE) then local recentSync = false pcall(function() local meta = HttpService:JSONDecode(readfile(MACRO_SYNC_META_FILE)) if type(meta) == "table" and (os.time() - (tonumber(meta.LastSyncAt) or 0) < 90) then recentSync = true end end) if recentSync then checkAndReloadSelectedMacroFromDiskIfNewer() return 0, 0, 0 end end end -- First, if not forceOverwrite, query lightweight ?summary=1 (~300 bytes from VPS RAM cache) to check if any macro changed! local needFullPull = forceOverwrite == true if not needFullPull then local sumRes = httpRequestSafe({ Url = MACROS_VPS_URL .. "?summary=1", Method = "GET", Headers = { ["User-Agent"] = "CTD-Client-MacroSummary/1.0" }, }, 6.0) if sumRes and (sumRes.StatusCode == 200 or sumRes.StatusCode == nil) and type(sumRes.Body) == "string" then local okSum, sumDecoded = pcall(function() return HttpService:JSONDecode(sumRes.Body) end) if okSum and type(sumDecoded) == "table" and type(sumDecoded.macros) == "table" then if type(sumDecoded.StepConfig) == "table" and type(State.ApplyRemoteStepConfig) == "function" then pcall(State.ApplyRemoteStepConfig, sumDecoded.StepConfig) end for rawName, meta in pairs(sumDecoded.macros) do if type(meta) == "table" and (tonumber(meta.StepCount) or 0) > 0 then local mName = tostring(meta.Name or rawName):gsub("[^%w_%-]", "_") local path = getMacroFilePath(mName) local localSavedAt = 0 local localSteps = 0 pcall(function() if isfile and readfile and isfile(path) then local loc = HttpService:JSONDecode(readfile(path)) if type(loc) == "table" then localSavedAt = tonumber(loc.SavedAt) or 0 localSteps = (type(loc.Steps) == "table" and #loc.Steps) or 0 end end end) if localSteps == 0 or ((tonumber(meta.SavedAt) or 0) > localSavedAt) then needFullPull = true break end end end if not needFullPull then pcall(function() if writefile then writefile(MACRO_SYNC_META_FILE, HttpService:JSONEncode({ LastSyncAt = os.time() })) end end) checkAndReloadSelectedMacroFromDiskIfNewer() return 0, 0, tonumber(sumDecoded.count) or 0 end else needFullPull = true end else needFullPull = true end end local r = httpRequestSafe({ Url = MACROS_VPS_URL, Method = "GET", Headers = { ["User-Agent"] = "CTD-Client-MacroSync/1.0" }, }, 8.0) local rawBody = (r and (r.StatusCode == 200 or r.StatusCode == nil) and type(r.Body) == "string") and r.Body or nil if type(rawBody) ~= "string" or #rawBody < 10 then if not silentIfNoChange then setStatus("⚠️ Không thể kết nối VPS Macro Server (160.191.242.43:8787)!") end return 0, 0, 0 end local ok, decoded = pcall(function() return HttpService:JSONDecode(rawBody) end) if not ok or type(decoded) ~= "table" or type(decoded.macros) ~= "table" then if not silentIfNoChange then setStatus("⚠️ Dữ liệu Macro từ VPS không hợp lệ!") end return 0, 0, 0 end if type(decoded.StepConfig) == "table" and type(State.ApplyRemoteStepConfig) == "function" then pcall(State.ApplyRemoteStepConfig, decoded.StepConfig) end local downloadedCount = 0 local updatedCount = 0 local totalServerMacros = 0 local touchedSelected = false for rawName, srvMacro in pairs(decoded.macros) do if type(srvMacro) == "table" and type(srvMacro.Steps) == "table" and #srvMacro.Steps > 0 then totalServerMacros = totalServerMacros + 1 local mName = tostring(srvMacro.Name or rawName):gsub("[^%w_%-]", "_") if mName ~= "" then local isRecordingThis = (MacroState.IsRecording and tostring(MacroState.RecordingMacroName) == mName) if not isRecordingThis then local path = getMacroFilePath(mName) local exists = false local localSavedAt = 0 local localStepsCount = 0 pcall(function() if isfile and readfile and isfile(path) then exists = true local loc = HttpService:JSONDecode(readfile(path)) if type(loc) == "table" then localSavedAt = tonumber(loc.SavedAt) or 0 localStepsCount = (type(loc.Steps) == "table" and #loc.Steps) or 0 end end end) local srvSavedAt = tonumber(srvMacro.SavedAt) or 0 local shouldWrite = false local isNewDownload = false if not exists then shouldWrite = true isNewDownload = true elseif forceOverwrite or localStepsCount == 0 or (srvSavedAt > localSavedAt) then shouldWrite = true isNewDownload = false end if shouldWrite then for _, s in ipairs(srvMacro.Steps) do if type(s) == "table" then s.UnitId = nil end end srvMacro.Name = mName srvMacro.StepCount = #srvMacro.Steps local wrote = false pcall(function() if writefile then writefile(path, HttpService:JSONEncode(srvMacro)) wrote = true end end) if wrote then local seenCustom = false for _, cn in ipairs(MacroState.CustomNames) do if cn == mName then seenCustom = true break end end if not seenCustom then table.insert(MacroState.CustomNames, mName) end if isNewDownload then downloadedCount = downloadedCount + 1 else updatedCount = updatedCount + 1 end if tostring(Cfg.SelectedMacro) == mName then touchedSelected = true end end end end end end end pcall(function() if writefile then writefile(MACRO_SYNC_META_FILE, HttpService:JSONEncode({ LastSyncAt = os.time() })) end end) if touchedSelected and not MacroState.IsRecording then loadMacroFromDisk(Cfg.SelectedMacro) end if downloadedCount > 0 or updatedCount > 0 then setStatus(string.format("☁️ VPS Macro Sync: Tải mới %d, Cập nhật %d (Tổng: %d macros trên Server)!", downloadedCount, updatedCount, totalServerMacros)) elseif not silentIfNoChange then setStatus(string.format("☁️ Toàn bộ %d Macros (cac, cac2, cac3...) đã đồng bộ mới nhất từ VPS!", totalServerMacros)) end return downloadedCount, updatedCount, totalServerMacros end local function getMatchElapsed() return math.max(0, math.floor((os.clock() - State.MatchStartClock) * 10 + 0.5) / 10) end local function getWaveElapsed() local curW = tonumber(workspace:GetAttribute("Wave")) or 0 if curW ~= State.CurrentWave then State.CurrentWave = curW State.CurrentWaveStartClock = os.clock() end return math.max(0, math.floor((os.clock() - (State.CurrentWaveStartClock or State.MatchStartClock)) * 10 + 0.5) / 10) end local function pushMacroStep(step) if not isAlive() or not MacroState.IsRecording then return end step.Wave = tonumber(workspace:GetAttribute("Wave")) or 0 step.WaveProgress = tonumber(workspace:GetAttribute("WaveProgress")) or 0 step.WaveElapsed = getWaveElapsed() step.Time = getMatchElapsed() table.insert(MacroState.Steps, step) local recTarget = tostring(MacroState.RecordingMacroName or Cfg.SelectedMacro or "Default") setStatus(string.format("[REC -> %s #%d] %s", recTarget, #MacroState.Steps, tostring(step.Summary or step.Action))) saveMacroToDisk(recTarget, true) end -- Helper to match a server-confirmed TowerAdd with the closest pending client CFrame (strictly within 1.2 studs) local function consumePendingPlaceCFrame(towerName, serverPos) local bestIdx = nil local bestDist = 1.2 for i = #MacroState.PendingPlaceList, 1, -1 do local entry = MacroState.PendingPlaceList[i] if os.clock() - entry.Time > 8.0 then table.remove(MacroState.PendingPlaceList, i) elseif entry.Name == towerName and typeof(entry.CFrame) == "CFrame" then if typeof(serverPos) == "Vector3" then local d = dist2D(entry.CFrame.Position, serverPos) if d <= bestDist then bestDist = d bestIdx = i end else bestIdx = i break end end end if bestIdx then local cf = MacroState.PendingPlaceList[bestIdx].CFrame table.remove(MacroState.PendingPlaceList, bestIdx) return cf end local fallback = MacroState.PendingPlaceCFrame[towerName] MacroState.PendingPlaceCFrame[towerName] = nil if fallback and typeof(serverPos) == "Vector3" and dist2D(fallback.Position, serverPos) <= 1.2 then return fallback end return nil end -- Record a server-confirmed tower placement (called by TowerAdd.OnClientEvent & diff fallback) local function recordConfirmedTowerAdd(tid, tName, unitId, pos, cf, initialLevel, initialTargetMode) if not MacroState.IsRecording or not tid or not tName or typeof(pos) ~= "Vector3" then return end if MacroState.RecordedTowerSnap[tid] then return end local order = MacroState.NextTowerOrder MacroState.NextTowerOrder = order + 1 MacroState.ServerIdToOrder[tid] = order local rawCF = consumePendingPlaceCFrame(tName, pos) or cf or CFrame.new(pos) local exactCF = CFrame.new(pos) * (rawCF - rawCF.Position) local lvl = tonumber(initialLevel) or 1 local tMode = tostring(initialTargetMode or "First") MacroState.RecordedTowerSnap[tid] = { Order = order, Name = tName, UnitId = unitId, Level = lvl, TargetMode = tMode, Pos = pos, CFrame = exactCF, Sold = false, } local st = getTowerAnalysis(tName) local pCost = (st and st.PriceL1) or 100 pushMacroStep({ Action = "TowerAdd", TowerOrder = order, TowerName = tName, Pos = vec3ToTable(pos), CFrame = cfToTable(exactCF), Cost = pCost, Summary = string.format("Place #%d %s ($%d)", order, tName, pCost), }) for uLvl = 2, lvl do local uCost = getNextUpgradeCost(tName, uLvl - 1) or 150 pushMacroStep({ Action = "TowerUpgrade", TowerOrder = order, TowerName = tName, Pos = vec3ToTable(pos), ToLevel = uLvl, Cost = uCost, Summary = string.format("Upg #%d %s -> L%d ($%d)", order, tName, uLvl, uCost), }) end if tMode ~= "First" then pushMacroStep({ Action = "TowerTarget", TowerOrder = order, TowerName = tName, Pos = vec3ToTable(pos), TargetMode = tMode, Cost = 0, Summary = string.format("Target #%d %s -> %s", order, tName, tMode), }) end end -- Record a server-confirmed tower upgrade (ignores all failed/spammed clicks when cash is insufficient!) local function recordConfirmedTowerUpgrade(tid, newLevel) if not MacroState.IsRecording or not tid then return end local snap = MacroState.RecordedTowerSnap[tid] local targetLvl = tonumber(newLevel) if not snap or not targetLvl or targetLvl <= snap.Level then return end for lvl = snap.Level + 1, targetLvl do local uCost = getNextUpgradeCost(snap.Name, lvl - 1) or 150 pushMacroStep({ Action = "TowerUpgrade", TowerOrder = snap.Order, TowerName = snap.Name, Pos = vec3ToTable(snap.Pos), ToLevel = lvl, Cost = uCost, Summary = string.format("Upg #%d %s -> L%d ($%d)", snap.Order, snap.Name, lvl, uCost), }) end snap.Level = targetLvl end -- Record a server-confirmed TargetMode change (coalesces rapid multi-clicks on the same tower!) local function recordConfirmedTowerTarget(tid, newTargetMode) if not MacroState.IsRecording or not tid or not newTargetMode then return end local snap = MacroState.RecordedTowerSnap[tid] local modeStr = tostring(newTargetMode) if not snap or modeStr == "" or modeStr == snap.TargetMode then return end snap.TargetMode = modeStr -- Smart Coalescing: If the immediately preceding step is already a TowerTarget on this exact tower, -- update it in-place so cycling First -> Last -> Closest -> Strong records only 1 step ("Strong")! local lastStep = MacroState.Steps[#MacroState.Steps] if lastStep and lastStep.Action == "TowerTarget" and lastStep.TowerOrder == snap.Order then lastStep.TargetMode = modeStr lastStep.Wave = tonumber(workspace:GetAttribute("Wave")) or lastStep.Wave lastStep.Time = getMatchElapsed() lastStep.Summary = string.format("Target #%d %s -> %s", snap.Order, snap.Name, modeStr) local recTarget = tostring(MacroState.RecordingMacroName or Cfg.SelectedMacro or "Default") setStatus(string.format("[REC -> %s #%d] %s", recTarget, #MacroState.Steps, lastStep.Summary)) saveMacroToDisk(recTarget, true) return end pushMacroStep({ Action = "TowerTarget", TowerOrder = snap.Order, TowerName = snap.Name, Pos = vec3ToTable(snap.Pos), TargetMode = modeStr, Cost = 0, Summary = string.format("Target #%d %s -> %s", snap.Order, snap.Name, modeStr), }) end -- Record a server-confirmed tower sell/removal local function recordConfirmedTowerSell(tid) if not MacroState.IsRecording or not tid then return end local gameState = tostring(workspace:GetAttribute("GameState") or "") local msg = tostring(workspace:GetAttribute("Message") or "") if gameState == "Lost" or gameState == "Won" or msg:find("Won") or msg:find("Lost") then return end local snap = MacroState.RecordedTowerSnap[tid] if not snap or snap.Sold then return end snap.Sold = true pushMacroStep({ Action = "TowerSell", TowerOrder = snap.Order, TowerName = snap.Name, Pos = vec3ToTable(snap.Pos), Cost = 0, Summary = string.format("Sell #%d %s", snap.Order, snap.Name), }) end -- Record a server-confirmed tower ability activation (ignores spammed clicks while on cooldown!) local function recordConfirmedTowerAbility(tid, overridePos, overrideCF) if not MacroState.IsRecording or not tid then return end local snap = MacroState.RecordedTowerSnap[tid] local pending = MacroState.PendingAbility[tid] if not snap or not pending then return end if os.clock() - pending.Time > 3.0 then MacroState.PendingAbility[tid] = nil return end MacroState.PendingAbility[tid] = nil local tPos = overridePos or pending.TargetPos local tCF = overrideCF or pending.TargetCF pushMacroStep({ Action = "TowerAbility", TowerOrder = snap.Order, TowerName = snap.Name, Pos = vec3ToTable(snap.Pos), TargetPos = tPos and vec3ToTable(tPos) or nil, TargetCF = tCF and cfToTable(tCF) or nil, Cost = 0, Summary = string.format("Ability #%d %s", snap.Order, snap.Name), }) end -- Connect directly to server RemoteEvents in Match Place for instant, 100% server-verified state & recording local BoundMatchRemotes = {} setupAuthoritativeMacroListeners = function() if CURRENT_PLACE ~= PLACE_MATCH and game.PlaceId ~= PLACE_MATCH then return end local function bindRemote(rName, handler) if BoundMatchRemotes[rName] then return end local ml = ReplicatedStorage:FindFirstChild("ModuleLoader") local sharedFolder = ml and ml:FindFirstChild("Shared") local netFolder = sharedFolder and sharedFolder:FindFirstChild("Network") local reFolder = netFolder and netFolder:FindFirstChild("RemoteEvent") local rem = reFolder and reFolder:FindFirstChild(rName) if not rem and RawNetworkGet then local ok, r = pcall(RawNetworkGet, rName) if ok and r then if typeof(r) == "Instance" then rem = r elseif type(r) == "table" and typeof(r._remote) == "Instance" then rem = r._remote end end end if rem and typeof(rem) == "Instance" and rem:IsA("RemoteEvent") then BoundMatchRemotes[rName] = rem trackConn(rem.OnClientEvent:Connect(function(...) invalidateLiveTowersCache() pcall(handler, ...) end)) end end bindRemote("TowerAdd", function(data) if type(data) ~= "table" then return end local tid = (data.Id and tostring(data.Id)) or (data.Index and tostring(data.Index)) if not tid or not data.Name or not data.Pos then return end if data.OwnerUserId == LocalPlayer.UserId or data.OwnerName == LocalPlayer.Name or data.Owner == LocalPlayer then MacroState.AuthoritativeTowers[tid] = { Id = tid, Name = data.Name, UnitId = data.UnitId, Level = tonumber(data.Level) or 1, Index = tonumber(data.Index) or 999, TargetMode = tostring(data.TargetMode or "First"), Pos = data.HomePos or data.Pos, CFrame = data.HomeCFrame or data.CFrame or CFrame.new(data.Pos), OwnerUserId = LocalPlayer.UserId, OwnerName = LocalPlayer.Name, Stunned = data.Stunned == true, AddedClock = os.clock(), SeenInGR = false, } MacroState.SelectedTowerId = tid recordConfirmedTowerAdd(tid, data.Name, data.UnitId, data.HomePos or data.Pos, data.HomeCFrame or data.CFrame, data.Level, data.TargetMode) if MacroState.IsRecording then task.defer(syncRecordingTowerHitboxes) end end end) bindRemote("TowerUpgrade", function(data) if type(data) ~= "table" then return end local tid = (data.Id and tostring(data.Id)) or (data.Index and tostring(data.Index)) if not tid then return end local auth = MacroState.AuthoritativeTowers[tid] if auth and data.Level then auth.Level = tonumber(data.Level) or auth.Level end if data.Level then recordConfirmedTowerUpgrade(tid, data.Level) if MacroState.IsRecording then task.defer(syncRecordingTowerHitboxes) end end end) bindRemote("TowerTarget", function(data) if type(data) ~= "table" then return end local tid = (data.Id and tostring(data.Id)) or (data.Index and tostring(data.Index)) if not tid or not data.TargetMode then return end local auth = MacroState.AuthoritativeTowers[tid] if auth then auth.TargetMode = tostring(data.TargetMode) end recordConfirmedTowerTarget(tid, data.TargetMode) if MacroState.IsRecording then task.defer(syncRecordingTowerHitboxes) end end) bindRemote("TowerMove", function(data) if type(data) ~= "table" then return end local tid = (data.Id and tostring(data.Id)) or (data.Index and tostring(data.Index)) if not tid or not data.Pos then return end local auth = MacroState.AuthoritativeTowers[tid] if auth then auth.Pos = data.Pos auth.CFrame = data.CFrame or auth.CFrame end local snap = MacroState.RecordedTowerSnap[tid] if snap then snap.Pos = data.Pos snap.CFrame = data.CFrame or snap.CFrame end recordConfirmedTowerAbility(tid, data.Pos, data.CFrame) if MacroState.IsRecording then task.defer(syncRecordingTowerHitboxes) end end) bindRemote("TowerAbilityUpdate", function(data) if type(data) ~= "table" then return end local tid = (data.Id and tostring(data.Id)) or (data.Index and tostring(data.Index)) if not tid then return end local cdEnd = tonumber(data.CooldownEndsAt) if cdEnd then MacroState.AbilityCooldowns[tid] = cdEnd recordConfirmedTowerAbility(tid, nil, nil) else MacroState.AbilityCooldowns[tid] = nil if data.HeadsActive ~= nil then recordConfirmedTowerAbility(tid, nil, nil) end end end) bindRemote("TowerRemove", function(data) if type(data) ~= "table" then return end local function handleRemovedId(remId) if not remId then return end local tid = tostring(remId) MacroState.AuthoritativeTowers[tid] = nil MacroState.AbilityCooldowns[tid] = nil if MacroState.SelectedTowerId == tid then MacroState.SelectedTowerId = nil end local claimedOrder = MacroState.ClaimedServerIds[tid] if claimedOrder then MacroState.ClaimedServerIds[tid] = nil MacroState.SoldOrders[claimedOrder] = true if MacroState.OrderToServerId[claimedOrder] == tid then MacroState.OrderToServerId[claimedOrder] = nil end end pcall(function() local g = workspace:FindFirstChild("Game") local tf = g and g:FindFirstChild("Towers") local m = tf and tf:FindFirstChild(tid) if m then safeDestroy(m) end end) recordConfirmedTowerSell(tid) end if data.Id then handleRemovedId(data.Id) end if data.Index and tostring(data.Index) ~= tostring(data.Id) then handleRemovedId(data.Index) end -- Also check data.Towers dictionary (sent by TowerRemove.OnClientEvent) so any missing tower is purged & recorded! if type(data.Towers) == "table" then local aliveIds = {} for k, v in pairs(data.Towers) do aliveIds[tostring(k)] = true if type(v) == "table" and v.Id then aliveIds[tostring(v.Id)] = true end end for oldId, _ in pairs(MacroState.AuthoritativeTowers) do if not aliveIds[oldId] then handleRemovedId(oldId) end end for oldId, snap in pairs(MacroState.RecordedTowerSnap) do if not aliveIds[oldId] and not snap.Sold then handleRemovedId(oldId) end end end if MacroState.IsRecording then task.defer(syncRecordingTowerHitboxes) end end) end setupAuthoritativeMacroListeners() local function startMacroRecording(clearExisting, targetMacroOverride) local targetMacro = tostring(targetMacroOverride or Cfg.SelectedMacro or "Default") Cfg.SelectedMacro = targetMacro MacroState.RecordingMacroName = targetMacro MacroState.IsPlaying = false Cfg.PlayMacro = false MacroState.IsRecording = true saveConfig() if installNetworkGetProxy then pcall(installNetworkGetProxy) end -- Temporarily restore minimal ground/road/models if DeleteEverything was active so manual mouse placement works! restoreMinimalForRecording() if clearExisting ~= false then MacroState.Steps = {} MacroState.NextTowerOrder = 1 MacroState.ServerIdToOrder = {} MacroState.RecordedTowerSnap = {} MacroState.PendingPlaceCFrame = {} MacroState.PendingPlaceList = {} MacroState.PendingAbility = {} MacroState.LastRecordedWaveSkip = -1 end MacroState.RecordStartClock = os.clock() State.MatchStartClock = os.clock() MacroState.MapName = tostring(workspace:GetAttribute("Map") or "Any") local curMode = tostring(workspace:GetAttribute("Mode") or "") if curMode ~= "" and curMode ~= "Voting" then MacroState.ModeName = curMode end local existingTowers = getLivePlacedTowers() if #existingTowers > 0 and #MacroState.Steps == 0 then for _, t in ipairs(existingTowers) do recordConfirmedTowerAdd(t.Id, t.Name, t.UnitId, t.Pos, t.CFrame, t.Level, t.TargetMode) end end saveMacroToDisk(targetMacro, true) setStatus(string.format("🔴 RECORDING MACRO -> '%s' (%d initial steps)", targetMacro, #MacroState.Steps)) end local function stopMacroRecording(andSave) local targetMacro = tostring(MacroState.RecordingMacroName or Cfg.SelectedMacro or "Default") MacroState.IsRecording = false MacroState.RecordingMacroName = nil if andSave ~= false then saveMacroToDisk(targetMacro, false) if #MacroState.Steps > 0 then task.spawn(function() uploadMacroToVPS(targetMacro, false) end) end else setStatus("Recording Stopped") end if Cfg.SuperMaxLow and Cfg.DeleteEverything then task.spawn(applySuperMaxLow) end end -- Client :FireServer hook ONLY captures intent metadata (CFrame for TowerAdd, TargetPos for TowerAbility) -- and NEVER directly pushes unverified TowerAdd / TowerUpgrade / TowerSell / TowerTarget / TowerAbility steps! local function onPlayerManualRemoteCall(remoteName, method, arg1) if not MacroState.IsRecording or InternalRemoteCall then return end if remoteName == "ModeVote" and type(arg1) == "string" then if workspace:GetAttribute("IsVoting") ~= true then return end MacroState.ModeName = arg1 for _, s in ipairs(MacroState.Steps) do if s.Action == "ModeVote" then s.Mode = arg1 s.Summary = "Vote Mode: " .. arg1 return end end pushMacroStep({ Action = "ModeVote", Mode = arg1, Cost = 0, Summary = "Vote Mode: " .. arg1, }) elseif remoteName == "WaveSkip" and Cfg.RecordWaveSkips then local curWave = tonumber(workspace:GetAttribute("Wave")) or 0 if workspace:GetAttribute("CanSkip") == true and curWave > 0 and MacroState.LastRecordedWaveSkip ~= curWave then MacroState.LastRecordedWaveSkip = curWave pushMacroStep({ Action = "WaveSkip", Cost = 0, Summary = string.format("Skip Wave %d", curWave), }) end elseif remoteName == "TowerAdd" and type(arg1) == "table" and type(arg1.Name) == "string" then if typeof(arg1.CFrame) == "CFrame" then MacroState.PendingPlaceCFrame[arg1.Name] = arg1.CFrame table.insert(MacroState.PendingPlaceList, { Name = arg1.Name, CFrame = arg1.CFrame, Time = os.clock(), }) end elseif remoteName == "TowerAbility" and type(arg1) == "table" and arg1.Id then local tid = tostring(arg1.Id) local argsTbl = type(arg1.Args) == "table" and arg1.Args or {} MacroState.PendingAbility[tid] = { Time = os.clock(), TargetPos = argsTbl.TargetPosition, TargetCF = argsTbl.TargetCFrame, } end end installNetworkGetProxy = function() if State.NetworkProxyInstalled then return end if Modules and Modules.Network and RawNetworkGet then State.NetworkProxyInstalled = true local proxyCache = {} Modules.Network.Get = function(remoteName) local realRemote = RawNetworkGet(remoteName) if not realRemote then return nil end if proxyCache[remoteName] then return proxyCache[remoteName] end local proxy = setmetatable({}, { __index = function(_, k) if k == "FireServer" then return function(_, ...) if MacroState.IsRecording and not InternalRemoteCall then local a1 = select(1, ...) task.defer(function() elevateThread() pcall(onPlayerManualRemoteCall, remoteName, "FireServer", a1) end) end return realRemote:FireServer(...) end elseif k == "InvokeServer" then return function(_, ...) if MacroState.IsRecording and not InternalRemoteCall then local a1 = select(1, ...) task.defer(function() elevateThread() pcall(onPlayerManualRemoteCall, remoteName, "InvokeServer", a1) end) end return realRemote:InvokeServer(...) end elseif k == "IsA" then return function(_, className) return realRemote:IsA(className) end end local val = realRemote[k] if type(val) == "function" then return function(_, ...) return val(realRemote, ...) end end return val end, }) proxyCache[remoteName] = proxy return proxy end end end -- Backup diff synchronization in case a packet arrived via full Replication snapshot local function stepMacroRecorderDiff() if not MacroState.IsRecording or CURRENT_PLACE ~= PLACE_MATCH then return end local gameState = tostring(workspace:GetAttribute("GameState") or "") local msg = tostring(workspace:GetAttribute("Message") or "") local matchEnded = (gameState == "Lost" or gameState == "Won" or msg == "You Won!" or msg == "You Lost!") local myTowers = getLivePlacedTowers() local currentById = {} for _, t in ipairs(myTowers) do currentById[t.Id] = t local snap = MacroState.RecordedTowerSnap[t.Id] if not snap then recordConfirmedTowerAdd(t.Id, t.Name, t.UnitId, t.Pos, t.CFrame, t.Level, t.TargetMode) else if t.Level > snap.Level then recordConfirmedTowerUpgrade(t.Id, t.Level) end if t.TargetMode ~= snap.TargetMode then recordConfirmedTowerTarget(t.Id, t.TargetMode) end end end if not matchEnded then for oldId, snap in pairs(MacroState.RecordedTowerSnap) do if not currentById[oldId] and not snap.Sold then recordConfirmedTowerSell(oldId) end end end syncRecordingTowerHitboxes() end -- Helper to get the player's equipped towers sorted by Price for fallback hotkeys (1..6) & Quick Micro local function getEquippedLoadoutList() local list = {} local data = getReplicaData() if data and type(data.Towers) == "table" then for uuid, unit in pairs(data.Towers) do if type(unit) == "table" and unit.Equipped and unit.Name then local st = getTowerAnalysis(unit.Name) table.insert(list, { UUID = tostring(uuid), Name = tostring(unit.Name), Price = (st and st.PriceL1) or 100, }) end end end table.sort(list, function(a, b) if a.Price ~= b.Price then return a.Price < b.Price end return a.Name < b.Name end) return list end local function getQuickTowerDropdownOptions() local opts = { "Latest Placed", "Selected On Map" } if CURRENT_PLACE == PLACE_MATCH then local live = getLivePlacedTowers() for idx, t in ipairs(live) do local ord = MacroState.ServerIdToOrder[t.Id] or MacroState.ClaimedServerIds[t.Id] or idx table.insert(opts, string.format("#%s %s (L%d) [%s]", tostring(ord), tostring(t.Name), tonumber(t.Level) or 1, tostring(t.Id):sub(1, 6))) end end return opts end local function resolveQuickSelectedTower() local live = getLivePlacedTowers() if #live == 0 then return nil end local sel = tostring(Cfg.QuickTowerTarget or MacroState.QuickTargetSelection or "Latest Placed") if sel == "Selected On Map" and MacroState.SelectedTowerId then for _, t in ipairs(live) do if t.Id == MacroState.SelectedTowerId then return t end end end local ordMatch = sel:match("^#(%d+)") if ordMatch then local wantOrd = tonumber(ordMatch) for idx, t in ipairs(live) do local ord = MacroState.ServerIdToOrder[t.Id] or MacroState.ClaimedServerIds[t.Id] or idx if ord == wantOrd then return t end end end local idPrefix = sel:match("%[([%w%-]+)%]$") if idPrefix then for _, t in ipairs(live) do if tostring(t.Id):sub(1, #idPrefix) == idPrefix then return t end end end if MacroState.SelectedTowerId then for _, t in ipairs(live) do if t.Id == MacroState.SelectedTowerId then return t end end end return live[#live] end -- Direct Fallback Input Controller (Mouse Raycast Tower Selector + Q Sell / E Upgrade / T Target / 1..6 Place) -- Ensures clicking a tower to Sell (Q) and placing at a new spot ALWAYS works even if GameUI was disabled/deleted! local RecordingInputFallbackInit = false local function setupRecordingInputFallback() refreshCurrentPlace() if CURRENT_PLACE ~= PLACE_MATCH then return end if RecordingInputFallbackInit then return end RecordingInputFallbackInit = true local function isNativeGameUIActive() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local gUI = pg and pg:FindFirstChild("GameUI") return (gUI and gUI:IsA("ScreenGui") and gUI.Enabled == true and #gUI:GetChildren() > 2) or false end local function raycastMouseWorld() local cam = workspace.CurrentCamera if not cam then return nil end local mPos = UserInputService:GetMouseLocation() local ray = cam:ViewportPointToRay(mPos.X, mPos.Y) local params = RaycastParams.new() params.FilterType = Enum.RaycastFilterType.Exclude local ignore = {} if LocalPlayer.Character then table.insert(ignore, LocalPlayer.Character) end local vis = workspace:FindFirstChild("CTD_MacroVisuals") if vis then table.insert(ignore, vis) end params.FilterDescendantsInstances = ignore return workspace:Raycast(ray.Origin, ray.Direction * 1000, params) end trackConn(UserInputService.InputBegan:Connect(function(inp, gameProcessed) if gameProcessed or UserInputService:GetFocusedTextBox() then return end if not MacroState.IsRecording and (Cfg.SuperMaxLow and Cfg.DeleteEverything) then return end if inp.UserInputType == Enum.UserInputType.MouseButton1 then local hit = raycastMouseWorld() if hit and hit.Instance then local g = workspace:FindFirstChild("Game") local tf = g and g:FindFirstChild("Towers") local cur = hit.Instance while cur and cur ~= workspace do if tf and cur:IsA("Model") and cur.Parent == tf then MacroState.SelectedTowerId = cur.Name MacroState.SelectedHotbarSlot = nil local live = getLivePlacedTowers() for idx, t in ipairs(live) do if t.Id == cur.Name then local ord = MacroState.ServerIdToOrder[t.Id] or MacroState.ClaimedServerIds[t.Id] or idx setStatus(string.format("🎯 Selected #%s %s (L%d) | [Q] Sell | [E] Upgrade | [T] Target", tostring(ord), tostring(t.Name), tonumber(t.Level) or 1)) break end end syncRecordingTowerHitboxes() return end cur = cur.Parent end -- If native GameUI is not active and user selected a hotbar slot (1..6), place tower at clicked ground position! if not isNativeGameUIActive() and MacroState.SelectedHotbarSlot then local loadout = getEquippedLoadoutList() local unit = loadout[MacroState.SelectedHotbarSlot] if unit then local placePos = hit.Position local placeCF = CFrame.new(placePos) local net = Modules and Modules.Network and Modules.Network.Get("TowerAdd") if net then net:FireServer({ Name = unit.Name, UnitId = unit.UUID, Pos = placePos, CFrame = placeCF, }) end MacroState.SelectedHotbarSlot = nil end end end elseif inp.UserInputType == Enum.UserInputType.Keyboard then if isNativeGameUIActive() then return end local kc = inp.KeyCode local slotMap = { [Enum.KeyCode.One] = 1, [Enum.KeyCode.Two] = 2, [Enum.KeyCode.Three] = 3, [Enum.KeyCode.Four] = 4, [Enum.KeyCode.Five] = 5, [Enum.KeyCode.Six] = 6, } if slotMap[kc] then local slot = slotMap[kc] local loadout = getEquippedLoadoutList() local unit = loadout[slot] if unit then MacroState.SelectedHotbarSlot = slot setStatus(string.format("📌 Slot [%d] %s ($%d) ready -> Left-Click Ground to Place!", slot, unit.Name, unit.Price)) end elseif kc == Enum.KeyCode.Q then local t = resolveQuickSelectedTower() if t and Modules and Modules.Network then local net = Modules.Network.Get("TowerSell") if net then net:FireServer({ Id = tostring(t.Id) }) end end elseif kc == Enum.KeyCode.E then local t = resolveQuickSelectedTower() if t and Modules and Modules.Network then local net = Modules.Network.Get("TowerUpgrade") if net then net:FireServer({ Id = tostring(t.Id) }) end end elseif kc == Enum.KeyCode.T then local t = resolveQuickSelectedTower() if t and Modules and Modules.Network then local net = Modules.Network.Get("TowerTarget") if net then net:FireServer({ Id = tostring(t.Id) }) end end end end end)) end setupRecordingInputFallback() -- Strict 1-to-1 Tower Resolution: -- 1. Checks exact bound server ID in MacroState.OrderToServerId[towerOrder]. -- 2. If not bound yet, ONLY matches an UNCLAIMED tower (not in ClaimedServerIds) -- of the same name within a tight 0.65-stud radius of posTable! -- 3. NEVER steals or collides with a neighbor tower (#1..#15) placed 1.5-3.0 studs away! local function bindTowerOrderToServerId(towerOrder, serverId) if not towerOrder or not serverId then return end local sid = tostring(serverId) local prevSid = MacroState.OrderToServerId[towerOrder] if prevSid and prevSid ~= sid then MacroState.ClaimedServerIds[prevSid] = nil end MacroState.OrderToServerId[towerOrder] = sid MacroState.ClaimedServerIds[sid] = towerOrder end local function unbindTowerOrder(towerOrder, markSold) if not towerOrder then return end local sid = MacroState.OrderToServerId[towerOrder] if sid then MacroState.ClaimedServerIds[sid] = nil end MacroState.OrderToServerId[towerOrder] = nil if markSold then MacroState.SoldOrders[towerOrder] = true end end local function resolveMacroTower(towerOrder, towerName, posTable, maxUnclaimedDist) local myTowers = getLivePlacedTowers() local byId = {} for _, t in ipairs(myTowers) do byId[t.Id] = t end -- 1. Check exact 1-to-1 bound Server ID first local mappedId = towerOrder and MacroState.OrderToServerId[towerOrder] if mappedId then if byId[mappedId] then MacroState.ClaimedServerIds[mappedId] = towerOrder return byId[mappedId] else -- Bound tower was removed/sold on server MacroState.ClaimedServerIds[mappedId] = nil MacroState.OrderToServerId[towerOrder] = nil end end -- 2. Tight unclaimed position match (default 0.65 studs) — strictly excluding any tower claimed by another TowerOrder! local targetPos = posTable and tableToVec3(posTable) if targetPos then local bestTower = nil local bestDist = tonumber(maxUnclaimedDist) or 0.65 for _, t in ipairs(myTowers) do local claimedBy = MacroState.ClaimedServerIds[t.Id] if (not claimedBy or claimedBy == towerOrder) and (not towerName or t.Name == towerName) and typeof(t.Pos) == "Vector3" then local d = dist2D(t.Pos, targetPos) if d <= bestDist then bestDist = d bestTower = t end end end if bestTower then if towerOrder then bindTowerOrderToServerId(towerOrder, bestTower.Id) end return bestTower end end return nil end -- Find the original TowerAdd step for a given TowerOrder (for Self-Healing if a tower is missing before Upgrade/Target) local function findTowerAddStepForOrder(towerOrder, beforeStepIdx) if not towerOrder then return nil end local foundAdd = nil local maxIdx = math.min(#MacroState.Steps, (beforeStepIdx or #MacroState.Steps) - 1) for i = 1, maxIdx do local s = MacroState.Steps[i] if s and s.TowerOrder == towerOrder then if s.Action == "TowerAdd" then foundAdd = s elseif s.Action == "TowerSell" then foundAdd = nil end end end return foundAdd end -- Execute and strictly verify a TowerAdd placement on the server before returning true local function executeVerifiedTowerAdd(addStep, stepIdx, totalSteps) local existing = resolveMacroTower(addStep.TowerOrder, addStep.TowerName, addStep.Pos, 0.65) if existing then bindTowerOrderToServerId(addStep.TowerOrder, existing.Id) return true, existing end local cash = getMyMatchCash() local st = getTowerAnalysis(addStep.TowerName) local reqCash = (st and st.PriceL1) or tonumber(addStep.Cost) or 100 if cash < reqCash then setStatus(string.format("Macro [%d/%d] Waiting Cash $%d/$%d -> Place #%d %s", stepIdx, totalSteps, cash, reqCash, addStep.TowerOrder or stepIdx, tostring(addStep.TowerName))) return false, "WaitCash" end -- Snapshot all existing tower IDs BEFORE calling TowerAdd so we can identify the exact newly-spawned tower! local beforeTowers = getLivePlacedTowers() local placedCount = math.min(getMyPlacedTowerCount(), #beforeTowers) if placedCount >= 15 then setStatus(string.format("Macro [%d/%d] Tower Limit 15/15 reached -> Waiting to Place #%d %s", stepIdx, totalSteps, addStep.TowerOrder or stepIdx, tostring(addStep.TowerName))) return false, "Limit15" end local repData = getReplicaData() if not repData or type(repData.Towers) ~= "table" then setStatus(string.format("Macro [%d/%d] Waiting for Player ReplicaData to load -> #%d %s...", stepIdx, totalSteps, addStep.TowerOrder or stepIdx, tostring(addStep.TowerName))) return false, "WaitReplica" end local unitUUID = findEquippedUUIDByName(addStep.TowerName) if not unitUUID then setStatus(string.format("⚠️ Macro Error: Tower '%s' not found in Equipped/Inventory!", tostring(addStep.TowerName))) return false, "MissingUnit" end local beforeIds = {} for _, t in ipairs(beforeTowers) do beforeIds[t.Id] = true end -- Clear any prior SoldOrders flag for this TowerOrder since we are actively placing it now if addStep.TowerOrder then MacroState.SoldOrders[addStep.TowerOrder] = nil end local placePos = tableToVec3(addStep.Pos) local rawCF = tableToCF(addStep.CFrame, placePos) local placeCF = CFrame.new(placePos) * (rawCF - rawCF.Position) setStatus(string.format("Macro [%d/%d] Placing #%d %s ($%d)", stepIdx, totalSteps, addStep.TowerOrder or stepIdx, tostring(addStep.TowerName), reqCash)) callRemote("TowerAdd", { Name = addStep.TowerName, UnitId = unitUUID, Pos = placePos, CFrame = placeCF, }) -- Wait up to 1.8s for the server to confirm the new tower via TowerAdd.OnClientEvent / getLivePlacedTowers() local t0 = os.clock() while os.clock() - t0 < 1.8 and isAlive() do local currentTowers = getLivePlacedTowers() local newCandidate = nil local bestDist = 2.0 for _, t in ipairs(currentTowers) do if not beforeIds[t.Id] and not MacroState.ClaimedServerIds[t.Id] and t.Name == addStep.TowerName then local d = dist2D(t.Pos, placePos) if d <= bestDist then bestDist = d newCandidate = t end end end if not newCandidate then newCandidate = resolveMacroTower(addStep.TowerOrder, addStep.TowerName, addStep.Pos, 0.65) end if newCandidate then bindTowerOrderToServerId(addStep.TowerOrder, newCandidate.Id) return true, newCandidate end task.wait(0.06) end -- Not confirmed yet -> do NOT advance CurrentStep! Next loop tick will retry cleanly. setStatus(string.format("Macro [%d/%d] Retrying Place #%d %s (Waiting Server Confirm)...", stepIdx, totalSteps, addStep.TowerOrder or stepIdx, tostring(addStep.TowerName))) return false, "Retry" end local function stepMacroPlayback() if not Cfg.PlayMacro or MacroState.IsRecording then return false end local totalSteps = #MacroState.Steps if totalSteps == 0 then setStatus(string.format("Macro '%s' is empty (0 steps) - Click Record to create!", Cfg.SelectedMacro)) return false end if MacroState.CurrentStep > totalSteps then setStatus(string.format("✅ Macro '%s' Completed (%d/%d steps | Towers: %d/15)", Cfg.SelectedMacro, totalSteps, totalSteps, getMyPlacedTowerCount())) return true end local stepIdx = MacroState.CurrentStep local step = MacroState.Steps[stepIdx] if not step then return true end local wave = tonumber(workspace:GetAttribute("Wave")) or 0 local isVotingAttr = workspace:GetAttribute("IsVoting") local isVoting = (isVotingAttr == true) or (isVotingAttr == nil and wave == 0 and (tostring(workspace:GetAttribute("Mode") or "") == "" or tostring(workspace:GetAttribute("Mode") or "") == "Voting")) local cash = getMyMatchCash() local elapsed = getMatchElapsed() if Cfg.MacroTimingMode == "Exact Time" and not isVoting and step.Action ~= "ModeVote" then local reqTime = tonumber(step.Time) or 0 if elapsed < reqTime then setStatus(string.format("Macro [%d/%d] Waiting Time %.1fs/%.1fs: %s", stepIdx, totalSteps, elapsed, reqTime, tostring(step.Summary or step.Action))) return true end end if step.Action == "ModeVote" then if isVoting then local modeToVote = step.Mode or MacroState.ModeName or Cfg.VoteMode or "Easy" if Cfg.AutoStarterCameraman and State.GetActiveStepMode then modeToVote = State.GetActiveStepMode(getReplicaData()) end if not isUserInJsonAttr(tostring(modeToVote) .. "Voters") then if not Cfg.AutoVote and (os.clock() - (State.LastModeVoteClock or 0) > 1.8) then State.LastModeVoteClock = os.clock() setStatus(string.format("Macro [%d/%d] Voting Mode: %s", stepIdx, totalSteps, modeToVote)) callRemote("ModeVote", modeToVote) else setStatus(string.format("Macro [%d/%d] Voting Mode: %s (Waiting Server Confirm)", stepIdx, totalSteps, modeToVote)) end else setStatus(string.format("Macro [%d/%d] Voted %s (Waiting for Wave 1)", stepIdx, totalSteps, modeToVote)) end else MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 end return true elseif step.Action == "WaveSkip" then if isVoting then return true end local targetWave = tonumber(step.Wave) or 1 if Cfg.AutoSkipWave then if workspace:GetAttribute("CanSkip") == true then callRemote("WaveSkip") end MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end if wave > targetWave then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true elseif wave == targetWave and workspace:GetAttribute("CanSkip") == true then setStatus(string.format("Macro [%d/%d] Skipping Wave %d", stepIdx, totalSteps, wave)) callRemote("WaveSkip") local t0 = os.clock() while os.clock() - t0 < 1.2 and isAlive() do local curW = tonumber(workspace:GetAttribute("Wave")) or 0 if curW > targetWave or workspace:GetAttribute("CanSkip") ~= true then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 break end task.wait(0.08) end return true else setStatus(string.format("Macro [%d/%d] Waiting Wave %d CanSkip", stepIdx, totalSteps, targetWave)) return true end elseif step.Action == "TowerAdd" then if isVoting then setStatus(string.format("Macro [%d/%d] Waiting for Voting to end -> %s", stepIdx, totalSteps, tostring(step.Summary or step.TowerName))) return true end local okAdd, addReason = executeVerifiedTowerAdd(step, stepIdx, totalSteps) if okAdd then MacroState.MissingUnitStreak = 0 MacroState.PlaceFailStreak = 0 MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 else if addReason == "MissingUnit" then MacroState.MissingUnitStreak = (MacroState.MissingUnitStreak or 0) + 1 elseif addReason == "Retry" and getMyPlacedTowerCount() == 0 then MacroState.PlaceFailStreak = (MacroState.PlaceFailStreak or 0) + 1 else MacroState.PlaceFailStreak = 0 end -- Self-Healing Watchdog: If we have 0 towers placed on the map and the required starter tower is missing/unequipped in hotbar, -- automatically return to Lobby so ensureStarterUnitsEquipped() equips it! if getMyPlacedTowerCount() == 0 and ((MacroState.MissingUnitStreak or 0) >= 3 or (MacroState.PlaceFailStreak or 0) >= 12) then MacroState.MissingUnitStreak = 0 MacroState.PlaceFailStreak = 0 setStatus(string.format("⚠️ Tướng '%s' chưa được Equip vào Hotbar -> Tự động về Lobby để Equip!", tostring(step.TowerName))) callRemote("OnLeave") task.wait(1.0) callRemote("TeleportBack") end end return true elseif step.Action == "TowerUpgrade" then if isVoting then return true end local targetTower = resolveMacroTower(step.TowerOrder, step.TowerName, step.Pos, 0.65) if not targetTower then -- Self-Healing: If the tower for this TowerOrder is missing on the map, place it first using its recorded TowerAdd step! local addStep = findTowerAddStepForOrder(step.TowerOrder, stepIdx) if addStep then setStatus(string.format("Macro [%d/%d] Self-Healing: Placing missing Tower #%d %s first...", stepIdx, totalSteps, step.TowerOrder or 0, tostring(step.TowerName))) local okHeal, healedTower = executeVerifiedTowerAdd(addStep, stepIdx, totalSteps) if okHeal and healedTower then targetTower = healedTower else return true end else setStatus(string.format("Macro [%d/%d] Waiting for Tower #%d (%s) to exist...", stepIdx, totalSteps, step.TowerOrder or 0, tostring(step.TowerName))) return true end end local toLevel = tonumber(step.ToLevel) or (targetTower.Level + 1) if targetTower.Level >= toLevel then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end local nextLevel = targetTower.Level + 1 local reqCash = getNextUpgradeCost(targetTower.Name, targetTower.Level) or tonumber(step.Cost) or 150 if cash < reqCash then setStatus(string.format("Macro [%d/%d] Waiting Cash $%d/$%d -> Upg #%d %s L%d->L%d", stepIdx, totalSteps, cash, reqCash, step.TowerOrder or 0, targetTower.Name, targetTower.Level, toLevel)) return true end local prevLevel = targetTower.Level setStatus(string.format("Macro [%d/%d] Upgrading #%d %s L%d->L%d ($%d)", stepIdx, totalSteps, step.TowerOrder or 0, targetTower.Name, prevLevel, nextLevel, reqCash)) callRemote("TowerUpgrade", { Id = tostring(targetTower.Id) }) -- Wait up to 1.5s for the server to confirm the upgrade via TowerUpgrade.OnClientEvent / getLivePlacedTowers() local t0 = os.clock() while os.clock() - t0 < 1.5 and isAlive() do local refreshed = resolveMacroTower(step.TowerOrder, step.TowerName, step.Pos, 0.65) if refreshed and refreshed.Level >= toLevel then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true elseif refreshed and refreshed.Level > prevLevel then -- Multi-level jump: progressed 1 level, will continue to toLevel on next tick return true end task.wait(0.06) end -- Server did not confirm upgrade yet -> stay on stepIdx and retry! setStatus(string.format("Macro [%d/%d] Retrying Upg #%d %s L%d->L%d (Waiting Server Confirm)...", stepIdx, totalSteps, step.TowerOrder or 0, targetTower.Name, prevLevel, toLevel)) return true elseif step.Action == "TowerTarget" then if isVoting then return true end local targetTower = resolveMacroTower(step.TowerOrder, step.TowerName, step.Pos, 0.65) if not targetTower then local addStep = findTowerAddStepForOrder(step.TowerOrder, stepIdx) if addStep then local okHeal, healedTower = executeVerifiedTowerAdd(addStep, stepIdx, totalSteps) if okHeal and healedTower then targetTower = healedTower else return true end else MacroState.CurrentStep = stepIdx + 1 return true end end local desiredMode = tostring(step.TargetMode or "First") if targetTower.TargetMode == desiredMode then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end -- Cycle one verified server click at a time until TargetMode == desiredMode local prevMode = targetTower.TargetMode setStatus(string.format("Macro [%d/%d] Cycling Target #%d %s (%s -> %s)", stepIdx, totalSteps, step.TowerOrder or 0, targetTower.Name, prevMode, desiredMode)) callRemote("TowerTarget", { Id = tostring(targetTower.Id) }) local t0 = os.clock() while os.clock() - t0 < 1.2 and isAlive() do local refreshed = resolveMacroTower(step.TowerOrder, step.TowerName, step.Pos, 0.65) if refreshed then if refreshed.TargetMode == desiredMode then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true elseif refreshed.TargetMode ~= prevMode then -- Server registered 1 cycle step; next loop tick will cycle again until desiredMode is reached! return true end end task.wait(0.06) end return true elseif step.Action == "TowerAbility" then if isVoting then return true end local reqWave = tonumber(step.Wave) or 1 if Cfg.MacroTimingMode == "Cash & Wave" and wave < reqWave then setStatus(string.format("Macro [%d/%d] Waiting Wave %d/%d for Ability #%d %s", stepIdx, totalSteps, wave, reqWave, step.TowerOrder or 0, tostring(step.TowerName))) return true end local targetTower = resolveMacroTower(step.TowerOrder, step.TowerName, step.Pos, 0.65) if not targetTower then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end -- Check if tower meets Ability UnlockLevel local cfgTbl = getConfigTable() local tCfg = cfgTbl and cfgTbl.Towers and cfgTbl.Towers[targetTower.Name] local unlockLvl = (tCfg and tCfg.Ability and tonumber(tCfg.Ability.UnlockLevel)) or 1 if targetTower.Level < unlockLvl then setStatus(string.format("Macro [%d/%d] Ability #%d %s requires L%d (currently L%d) -> Skipping", stepIdx, totalSteps, step.TowerOrder or 0, targetTower.Name, unlockLvl, targetTower.Level)) MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end -- Check if Ability is on server cooldown local cdEnd = MacroState.AbilityCooldowns[targetTower.Id] local nowServer = workspace:GetServerTimeNow() if cdEnd and nowServer < cdEnd then local remCd = math.max(0, cdEnd - nowServer) if remCd <= 6.0 then setStatus(string.format("Macro [%d/%d] Waiting Ability Cooldown (%.1fs) #%d %s", stepIdx, totalSteps, remCd, step.TowerOrder or 0, targetTower.Name)) return true end end local tPos = step.TargetPos and tableToVec3(step.TargetPos) or targetTower.Pos or Vector3.zero local tCF = step.TargetCF and tableToCF(step.TargetCF, tPos) or CFrame.new(tPos) local prevPos = targetTower.Pos local prevCd = MacroState.AbilityCooldowns[targetTower.Id] or 0 setStatus(string.format("Macro [%d/%d] Using Ability #%d %s", stepIdx, totalSteps, step.TowerOrder or 0, targetTower.Name)) callRemote("TowerAbility", { Id = tostring(targetTower.Id), Args = { TargetPosition = tPos, TargetCFrame = tCF, }, }) local t0 = os.clock() local confirmed = false while os.clock() - t0 < 1.2 and isAlive() do local newCd = MacroState.AbilityCooldowns[targetTower.Id] or 0 local refreshed = resolveMacroTower(step.TowerOrder, step.TowerName, step.Pos, 0.65) if newCd > prevCd or (refreshed and prevPos and dist2D(refreshed.Pos, prevPos) > 0.5) then confirmed = true break end task.wait(0.08) end MacroState.StepRetryCount = (MacroState.StepRetryCount or 0) + 1 if confirmed or MacroState.StepRetryCount >= 2 then MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 end return true elseif step.Action == "TowerSell" then if isVoting then return true end local reqWave = tonumber(step.Wave) or 0 -- Always respect recorded Wave on TowerSell (even in Cash Only mode) because Selling costs $0! if reqWave > 0 and Cfg.MacroTimingMode ~= "Exact Time" and wave < reqWave then setStatus(string.format("Macro [%d/%d] Waiting Wave %d/%d to Sell #%d %s", stepIdx, totalSteps, wave, reqWave, step.TowerOrder or 0, tostring(step.TowerName))) return true end -- Mid-wave Boss Micro precision: If the tower was sold mid-wave (WaveProgress > 8% or WaveElapsed > 2.5s), -- wait until the current wave reaches that progress/time before selling so front defense isn't sold at 0% of the wave! if wave == reqWave and Cfg.MacroTimingMode == "Cash & Wave" then local reqProg = tonumber(step.WaveProgress) or 0 local reqWaveElapsed = tonumber(step.WaveElapsed) or 0 if reqProg > 8 or reqWaveElapsed > 2.5 then local curProg = tonumber(workspace:GetAttribute("WaveProgress")) or 0 local curWaveElapsed = getWaveElapsed() local progReady = (reqProg > 8) and (curProg >= (reqProg - 4)) local timeReady = (reqWaveElapsed > 2.5) and (curWaveElapsed >= (reqWaveElapsed - 0.4)) if not progReady and not timeReady then setStatus(string.format("Macro [%d/%d] Waiting W%d Progress %d%%/%d%% (%.1fs/%.1fs) -> Sell #%d %s", stepIdx, totalSteps, wave, curProg, reqProg, curWaveElapsed, reqWaveElapsed, step.TowerOrder or 0, tostring(step.TowerName))) return true end end end local targetTower = resolveMacroTower(step.TowerOrder, step.TowerName, step.Pos, 0.65) if not targetTower then -- Already sold / removed from server! unbindTowerOrder(step.TowerOrder, true) MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end local sellId = tostring(targetTower.Id) local cashBeforeSell = getMyMatchCash() local countBeforeSell = #getLivePlacedTowers() setStatus(string.format("Macro [%d/%d] Selling #%d %s (Freeing Slot & Refunding Cash for Next Place)", stepIdx, totalSteps, step.TowerOrder or 0, targetTower.Name)) callRemote("TowerSell", { Id = sellId }) -- Wait up to 1.8s for: -- 1. sellId to be removed from getLivePlacedTowers() (via TowerRemove.OnClientEvent / GameReplicator), AND -- 2. Server to credit the refund cash (getMyMatchCash() > cashBeforeSell) so the immediate next TowerAdd step at a new spot has the money & slot ready! local t0 = os.clock() local removedConfirmedAt = nil while os.clock() - t0 < 1.8 and isAlive() do local liveNow = getLivePlacedTowers() local stillThere = false for _, t in ipairs(liveNow) do if t.Id == sellId then stillThere = true break end end if not stillThere then removedConfirmedAt = removedConfirmedAt or os.clock() local cashNow = getMyMatchCash() local ptNow = getMyPlacedTowerCount() if cashNow > cashBeforeSell or ptNow < countBeforeSell or (os.clock() - removedConfirmedAt >= 0.30) then unbindTowerOrder(step.TowerOrder, true) MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end end task.wait(0.05) end setStatus(string.format("Macro [%d/%d] Retrying Sell #%d %s (Waiting Server Confirm)...", stepIdx, totalSteps, step.TowerOrder or 0, targetTower.Name)) return true end MacroState.CurrentStep = stepIdx + 1 MacroState.StepRetryCount = 0 return true end -- Load SelectedMacro from local disk immediately (0ms startup); NEVER block GUI creation on VPS HTTP requests! loadMacroFromDisk(Cfg.SelectedMacro) task.spawn(function() if #MacroState.Steps == 0 then pcall(syncMacrosFromVPS, false, true) if #MacroState.Steps == 0 and not MacroState.IsRecording then loadMacroFromDisk(Cfg.SelectedMacro) end else task.wait(1.5 + math.random() * 4.0) if isAlive() and not MacroState.IsRecording then pcall(syncMacrosFromVPS, false, true) end end end) -- Background loop to periodically check VPS / shared disk for newly uploaded/updated macros (jittered 65-95s across multi-accounts) task.spawn(function() while isAlive() do task.wait(65 + math.random(0, 30)) if not MacroState.IsRecording then pcall(syncMacrosFromVPS, false, true) end end end) -- ============================================================================ -- 8. SUMMON BANNER TRACKER (VPS LIVE SERVER 160.191.242.43:8787 + CACHE + 30M TIMER) -- ============================================================================ local BANNER_CACHE_FILE = "CTD_BannerCache.json" local BANNER_VPS_URL = "http://160.191.242.43:8787/banner" local BannerState = { WindowId = 0, UpdatedAt = 0, LastQuery = 0, LastVPSPull = 0, LastVPSPush = 0, Source = "Local Cache", Reporter = "None", BasicCrate = {}, -- { Godly = "...", Mythic = "...", Legendary = "...", Epic = "...", Rare = "...", Uncommon = "...", Basic = "..." } } local function saveBannerCacheToDisk() pcall(function() if writefile then writefile(BANNER_CACHE_FILE, HttpService:JSONEncode({ WindowId = BannerState.WindowId, UpdatedAt = BannerState.UpdatedAt, Source = BannerState.Source, Reporter = BannerState.Reporter, BasicCrate = BannerState.BasicCrate, StepConfig = State.LastCloudStepConfig, })) end end) end local function loadBannerCacheFromDisk() pcall(function() if isfile and readfile and isfile(BANNER_CACHE_FILE) then local decoded = HttpService:JSONDecode(readfile(BANNER_CACHE_FILE)) if type(decoded) == "table" then if type(decoded.StepConfig) == "table" and type(State.ApplyRemoteStepConfig) == "function" then pcall(State.ApplyRemoteStepConfig, decoded.StepConfig) end if type(decoded.BasicCrate) == "table" then BannerState.WindowId = tonumber(decoded.WindowId) or 0 BannerState.UpdatedAt = tonumber(decoded.UpdatedAt) or 0 BannerState.Source = tostring(decoded.Source or "Local Cache") BannerState.Reporter = tostring(decoded.Reporter or "None") BannerState.BasicCrate = decoded.BasicCrate end end end end) end loadBannerCacheFromDisk() local function getBannerTimerInfo() local interval = (Modules and Modules.Config and Modules.Config.GameSettings and tonumber(Modules.Config.GameSettings.SummonInterval)) or 1800 local now = os.time() local windowId = math.floor(now / interval) local timeLeft = (windowId + 1) * interval - now local mins = math.floor(timeLeft / 60) local secs = timeLeft % 60 local hasUnits = (type(BannerState.BasicCrate) == "table" and next(BannerState.BasicCrate) ~= nil) local isCurrent = (BannerState.WindowId == windowId and hasUnits) return { Interval = interval, Now = now, WindowId = windowId, TimeLeft = timeLeft, TimeFormatted = string.format("%02d:%02d", mins, secs), HasUnits = hasUnits, IsCurrent = isCurrent, Source = BannerState.Source or "Local", Reporter = BannerState.Reporter or "None", BasicCrate = BannerState.BasicCrate or {}, } end -- Push (POST) the current BannerState.BasicCrate up to VPS (http://160.191.242.43:8787/banner) local function pushBannerToVPS(force) local info = getBannerTimerInfo() if not info.HasUnits then return false end if not force and not info.IsCurrent then return false end if not force and (os.clock() - BannerState.LastVPSPush < 4) then return true end BannerState.LastVPSPush = os.clock() local reqFn = request or http_request or (syn and syn.request) or (http and http.request) if not reqFn then return false end local res = httpRequestSafe({ Url = BANNER_VPS_URL, Method = "POST", Headers = { ["Content-Type"] = "application/json", ["User-Agent"] = "CTD-Client-BannerSync/1.0", }, Body = HttpService:JSONEncode({ WindowId = info.WindowId, UpdatedAt = BannerState.UpdatedAt > 0 and BannerState.UpdatedAt or os.time(), Reporter = tostring(LocalPlayer and LocalPlayer.Name or "LobbyAccount"), BasicCrate = BannerState.BasicCrate, }), }, 6.0) if res and (res.StatusCode == 200 or res.StatusCode == nil) then BannerState.Source = "Lobby -> VPS" if type(res.Body) == "string" and #res.Body > 10 then pcall(function() local decoded = HttpService:JSONDecode(res.Body) if type(decoded) == "table" and type(decoded.StepConfig) == "table" and type(State.ApplyRemoteStepConfig) == "function" then State.ApplyRemoteStepConfig(decoded.StepConfig) end end) end return true end return false end -- Pull (GET) the live Banner from VPS (http://160.191.242.43:8787/banner) with shared disk cache deduplication for 50+ accounts! local function pullBannerFromVPS(force) if not force then if os.clock() - BannerState.LastVPSPull < 15 then return getBannerTimerInfo().IsCurrent end -- Check if another account on this PC already updated CTD_BannerCache.json for the current 30m window! loadBannerCacheFromDisk() local diskInfo = getBannerTimerInfo() if diskInfo.IsCurrent and (os.time() - (tonumber(BannerState.UpdatedAt) or 0) < 25) then BannerState.LastVPSPull = os.clock() return true end end BannerState.LastVPSPull = os.clock() local r = httpRequestSafe({ Url = BANNER_VPS_URL, Method = "GET", Headers = { ["User-Agent"] = "CTD-Client-BannerPull/1.0" }, }, 6.0) local rawBody = (r and (r.StatusCode == 200 or r.StatusCode == nil) and type(r.Body) == "string") and r.Body or nil if type(rawBody) == "string" and #rawBody > 10 then local ok, decoded = pcall(function() return HttpService:JSONDecode(rawBody) end) if ok and type(decoded) == "table" then if type(decoded.StepConfig) == "table" and type(State.ApplyRemoteStepConfig) == "function" then pcall(State.ApplyRemoteStepConfig, decoded.StepConfig) end if type(decoded.BasicCrate) == "table" then local bc = decoded.BasicCrate if bc.Godly or bc.Mythic or bc.Legendary or bc.Basic then local vpsWin = tonumber(decoded.WindowId) or 0 if vpsWin >= BannerState.WindowId or not getBannerTimerInfo().HasUnits then BannerState.WindowId = vpsWin BannerState.UpdatedAt = tonumber(decoded.UpdatedAt) or os.time() BannerState.Reporter = tostring(decoded.Reporter or "VPS") BannerState.Source = "VPS Live (" .. BannerState.Reporter .. ")" BannerState.BasicCrate = bc saveBannerCacheToDisk() return getBannerTimerInfo().IsCurrent end end end end end return getBannerTimerInfo().IsCurrent end local function applyBannerData(setTable) if type(setTable) ~= "table" then return false end local basic = setTable["Basic Crate"] or setTable if type(basic) == "table" and (basic.Godly or basic.Mythic or basic.Legendary or basic.Basic) then local info = getBannerTimerInfo() BannerState.WindowId = info.WindowId BannerState.UpdatedAt = os.time() BannerState.Reporter = tostring(LocalPlayer and LocalPlayer.Name or "Lobby") BannerState.Source = "Lobby Direct" BannerState.BasicCrate = basic saveBannerCacheToDisk() task.spawn(function() pushBannerToVPS(true) end) return true end return false end local function syncLobbyBanner(force) if CURRENT_PLACE ~= PLACE_LOBBY then local okVPS = pullBannerFromVPS(force) if not okVPS then loadBannerCacheFromDisk() end return getBannerTimerInfo().IsCurrent end local info = getBannerTimerInfo() if not force and info.IsCurrent and (os.clock() - BannerState.LastQuery < 15) then return true end if not force and (os.clock() - BannerState.LastQuery < 5) then return info.IsCurrent end BannerState.LastQuery = os.clock() local ok, res = callRemote("GetSummonSet") if ok and type(res) == "table" then if applyBannerData(res) then local bc = BannerState.BasicCrate setStatus(string.format("🎯 Banner Synced -> VPS: [Godly] %s | [Mythic] %s | [Leg] %s", tostring(bc.Godly or "?"), tostring(bc.Mythic or "?"), tostring(bc.Legendary or "?"))) return true end end return false end -- Background VPS Sync Loop (Multi-Account Optimized): -- * In Lobby: Listens to SummonSetUpdated & pushes current banner to VPS (every 12s if BannerWatcherOnly, or on window rollover) -- * In Match: Pulls live banner on startup & on 30m window rollover (or when Lobby Auto tab is open), avoiding 12s polling across 50 accounts! task.spawn(function() task.wait(1.0 + math.random() * 2.5) if CURRENT_PLACE == PLACE_LOBBY then if getBannerTimerInfo().IsCurrent then pushBannerToVPS(true) end if RawNetworkGet then local ok, rem = pcall(function() return RawNetworkGet("SummonSetUpdated") end) if ok and rem and rem:IsA("RemoteEvent") then trackConn(rem.OnClientEvent:Connect(function(newSet) applyBannerData(newSet) end)) end end syncLobbyBanner(true) else pullBannerFromVPS(false) end local lastWin = getBannerTimerInfo().WindowId while isAlive() do local info = getBannerTimerInfo() local windowRolledOver = (info.WindowId ~= lastWin) or not info.IsCurrent if CURRENT_PLACE == PLACE_LOBBY then if Cfg.BannerWatcherOnly or windowRolledOver then if syncLobbyBanner(true) then lastWin = info.WindowId end end task.wait(windowRolledOver and 5 or 12) else local lobbyTabOpen = (UIRefs and UIRefs.MainFrame and UIRefs.MainFrame.Visible and UIRefs.LobbyPage and UIRefs.LobbyPage.Visible) if windowRolledOver or lobbyTabOpen then if windowRolledOver then task.wait(1 + math.random(0, 6)) end if pullBannerFromVPS(false) then lastWin = getBannerTimerInfo().WindowId end end task.wait(windowRolledOver and 8 or 20) end end end) local function formatBannerInspectorText() local info = getBannerTimerInfo() local bc = info.BasicCrate local statusLine if info.IsCurrent then statusLine = string.format("🌐 ACTIVE BANNER [%s] | ⏳ Reset in: %s", tostring(info.Source), info.TimeFormatted) elseif info.HasUnits then statusLine = string.format("⚠️ BANNER ROTATING (Syncing VPS...) | ⏳ Next Reset: %s", info.TimeFormatted) else statusLine = string.format("⚠️ Waiting VPS/Lobby Banner Sync... | ⏳ Next Reset: %s", info.TimeFormatted) end return table.concat({ statusLine, string.format("🌟 Godly (0.01%%): %s", tostring(bc.Godly or "Unknown")), string.format("🔥 Mythic (0.1%%): %s | ⚡ Leg (1%%): %s", tostring(bc.Mythic or "?"), tostring(bc.Legendary or "?")), string.format("💜 Epic (4%%): %s | 💙 Rare (15%%): %s", tostring(bc.Epic or "?"), tostring(bc.Rare or "?")), string.format("💚 Unc (30%%): %s | 🤍 Basic (49.89%%): %s", tostring(bc.Uncommon or "?"), tostring(bc.Basic or "?")), }, "\n") end local GAME_GROUP_ID = (tonumber(game.CreatorId) and tonumber(game.CreatorId) > 0) and tonumber(game.CreatorId) or 1111394813 -- Tự động Join Group của Game (Aligators Games - 1111394813) để mở khoá tốc độ 1.5x trong trận! local function doAutoJoinGameGroup(force) if not force and not Cfg.AutoJoinGroup then return false end if State.IsInGameGroup == true and not force then return true end if not force and (os.clock() - (State.LastGroupCheckClock or 0) < 12) then return State.IsInGameGroup == true end State.LastGroupCheckClock = os.clock() local inGroup = false pcall(function() inGroup = LocalPlayer:IsInGroup(GAME_GROUP_ID) end) if inGroup then State.IsInGameGroup = true return true end local function findJoinCommunityButton() local btn = nil pcall(function() btn = CoreGui.RobloxGui.FocusNavigationCoreScriptsWrapper.Main.DialogContentWrapper.Folder.Dialog.DialogInner.DialogBody.DialogActions.ActionsContainer:FindFirstChild("1-Join Community") end) if btn and btn:IsA("GuiButton") and btn.Visible then return btn end return nil end local joinBtn = findJoinCommunityButton() if not joinBtn and GroupService and GroupService.PromptJoinAsync then setStatus("👥 Đang mở hộp thoại Join Group Game (Aligators Games) để mở khoá 1.5x Speed...") task.spawn(function() pcall(function() GroupService:PromptJoinAsync(GAME_GROUP_ID) end) end) local t0 = os.clock() while os.clock() - t0 < 1.8 and isAlive() do joinBtn = findJoinCommunityButton() if joinBtn then break end task.wait(0.1) end end if joinBtn then setStatus("👥 Đang tự động bấm 'Join Community' (Aligators Games -> Mở 1.5x Speed)...") local wasHeadless = (Cfg.HeadlessNo3D == true) if wasHeadless then pcall(function() RunService:Set3dRenderingEnabled(true) end) task.wait(0.12) end pcall(function() if firesignal then pcall(firesignal, joinBtn.Activated) pcall(firesignal, joinBtn.MouseButton1Click) end end) pcall(function() local absPos = joinBtn.AbsolutePosition local absSize = joinBtn.AbsoluteSize if absSize.X > 0 and absSize.Y > 0 and VirtualInputManager then local inset = GuiService and GuiService:GetGuiInset() or Vector2.zero local cx = absPos.X + absSize.X * 0.5 local cy = absPos.Y + absSize.Y * 0.5 + inset.Y VirtualInputManager:SendMouseButtonEvent(cx, cy, 0, true, game, 1) task.wait(0.08) VirtualInputManager:SendMouseButtonEvent(cx, cy, 0, false, game, 1) end end) if wasHeadless then task.delay(0.25, function() if Cfg.HeadlessNo3D then pcall(function() RunService:Set3dRenderingEnabled(false) end) end end) end task.wait(1.2) pcall(function() inGroup = LocalPlayer:IsInGroup(GAME_GROUP_ID) end) if inGroup then State.IsInGameGroup = true setStatus("✅ Đã Join Group Game thành công! Đã mở khoá 1.5x Speed trong trận!") return true end end return false end -- ============================================================================ -- SMART DYNAMIC STEP ENGINE (Tự động đọc Tướng/Mode/Map từ từng Macro & Đổi Step Bất Kỳ Lúc Nào) -- ============================================================================ State.UnitAliasMap = { ["titan cinemaman"] = { "Titan Cinemaman", "Cinemaman", "Cinemaman Titan", "CinemaMan Titan" }, ["cinemaman"] = { "Cinemaman", "Titan Cinemaman", "Cinemaman Titan", "CinemaMan Titan" }, ["glitch cameraman"] = { "Glitch Cameraman", "Glitch Plungerman", "Glitch Plugerman" }, ["upgraded titan cameraman"] = { "Upgraded Titan Cameraman", "UTCM" }, ["utcm"] = { "Upgraded Titan Cameraman", "UTCM" }, ["upgraded titan speakerman"] = { "Upgraded Titan Speakerman", "UTSM" }, ["utsm"] = { "Upgraded Titan Speakerman", "UTSM" }, ["upgraded videotapper prototype"]= { "Upgraded Videotapper Prototype", "UTTVM", "Upgraded Titan TV Man" }, ["uttvm"] = { "Upgraded Videotapper Prototype", "UTTVM", "Upgraded Titan TV Man" }, } State.MacroMetaCache = {} function State.GetUnitAliases(unitName) local clean = tostring(unitName or ""):match("^%s*(.-)%s*$") if clean == "" then return {} end local low = clean:lower() local list = { clean } local seen = { [low] = true } local mapped = State.UnitAliasMap[low] if type(mapped) == "table" then for _, al in ipairs(mapped) do local alLow = tostring(al):lower() if not seen[alLow] then seen[alLow] = true table.insert(list, al) end end end return list end -- Kiểm tra xem 1 tướng (hoặc các tên alias của tướng đó) có đang nằm trên Banner Basic Crate hiện tại hay không local function isUnitOnActiveBanner(unitName) local aliases = State.GetUnitAliases(unitName) if #aliases == 0 then return false end local info = getBannerTimerInfo() if not info.HasUnits or type(info.BasicCrate) ~= "table" then return false end for _, bannerUnit in pairs(info.BasicCrate) do local bLow = tostring(bannerUnit or ""):lower():match("^%s*(.-)%s*$") if bLow ~= "" then for _, al in ipairs(aliases) do if bLow == tostring(al):lower() then return true end end end end return false end local function isUnitEquippedVal(eq) if eq == true then return true end local n = tonumber(eq) return (n ~= nil and n > 0) end local function getUnitInventoryStatus(data, unitName) local aliases = State.GetUnitAliases(unitName) local aliasSet = {} for _, al in ipairs(aliases) do aliasSet[tostring(al):lower()] = true end local ownedUUID = nil local equippedUUID = nil local actualName = tostring(unitName or "") local count = 0 if data and type(data.Towers) == "table" and next(aliasSet) ~= nil then for uuid, unit in pairs(data.Towers) do if type(unit) == "table" and type(unit.Name) == "string" then local uLow = unit.Name:lower():match("^%s*(.-)%s*$") if aliasSet[uLow] then count = count + 1 actualName = unit.Name ownedUUID = ownedUUID or tostring(uuid) if isUnitEquippedVal(unit.Equipped) then equippedUUID = tostring(uuid) end end end end end return { Owned = (ownedUUID ~= nil), Equipped = (equippedUUID ~= nil), UUID = equippedUUID or ownedUUID, Count = count, ActualName = actualName, } end State.SingleRollUnitSet = { ["speakerman"] = true, ["cameraman"] = true, ["car speakerman"] = true, ["large speakerman"] = true, ["large cameraman"] = true, ["popcorn cameraman"]= true, ["brown cameraman"] = true, ["lucky speakerman"] = true, ["tv man"] = true, } function State.IsSingleRollUnit(unitName) local aliases = State.GetUnitAliases(unitName) local info = getBannerTimerInfo() local bc = (info and type(info.BasicCrate) == "table") and info.BasicCrate or nil for _, al in ipairs(aliases) do local low = tostring(al):lower():match("^%s*(.-)%s*$") if State.SingleRollUnitSet[low] then return true end if bc then for tierKey, bUnit in pairs(bc) do if tostring(bUnit or ""):lower():match("^%s*(.-)%s*$") == low then local tk = tostring(tierKey):lower() if tk == "basic" or tk == "uncommon" or tk == "rare" then return true elseif tk == "epic" or tk == "legendary" or tk == "mythic" or tk == "godly" then return false end end end end local st = getTowerAnalysis(al) if st and tonumber(st.RarityRank) then return tonumber(st.RarityRank) <= 3 end end return false end State.CloudStepMacros = State.CloudStepMacros or {} State.LastCloudStepUpdatedAt = State.LastCloudStepUpdatedAt or 0 State.STEPS_VPS_URL = "http://160.191.242.43:8787/steps" -- Quy tắc Mode cố định theo Macro: -- * Macro 1, 2, 3, 4 (cac/cac1, cac2, cac3, cac4) -> Easy -- * Macro 5, 6 (cac5, cac6) -> Normal -- * Macro 7, 8 (cac7, cac8) -> Hard function State.GetCanonicalMacroMode(macroName) local low = tostring(macroName or ""):lower():match("^%s*(.-)%s*$") if low == "cac" or low == "cac1" or low == "cac2" or low == "cac3" or low == "cac4" or low == "default" then return "Easy" elseif low == "cac5" or low == "cac6" then return "Normal" elseif low == "cac7" or low == "cac8" then return "Hard" end return nil end -- Tự động đọc file .json để trích xuất danh sách Tướng cần dùng (TowerAdd), Mode và Map! function State.InspectMacroMeta(macroName, forceReload) local clean = tostring(macroName or ""):match("^%s*(.-)%s*$") if clean == "" then clean = "cac" end if clean:lower() == "cac1" then local p1 = getMacroFilePath("cac1") if not (isfile and isfile(p1)) then clean = "cac" end end local now = os.clock() local cached = State.MacroMetaCache[clean] if cached and not forceReload and (now - (cached.CachedAt or 0) < 6.0) then return cached end local defaultUnits = {} local defaultMode = State.GetCanonicalMacroMode(clean) or "Easy" local defaultMap = "Toilet City" local lowName = clean:lower() if lowName == "cac" or lowName == "cac1" or lowName == "default" then defaultUnits = { "Speakerman" } elseif lowName == "cac2" then defaultUnits = { "Cameraman" } elseif lowName == "cac3" then defaultUnits = { "Ninja Cameraman" } elseif lowName == "cac4" then defaultUnits = { "Car Speakerman" } elseif lowName == "cac5" then defaultUnits = { "Large Speakerman" } elseif lowName == "cac6" then defaultUnits = { "Large Cameraman" } elseif lowName == "cac7" then defaultUnits = { "Ninja Cameraman" } elseif lowName == "cac8" then defaultUnits = { "Ninja Cameraman", "Popcorn Cameraman" } end local cloudMeta = State.CloudStepMacros and State.CloudStepMacros[lowName] if type(cloudMeta) == "table" then if type(cloudMeta.RequiredUnits) == "table" and #cloudMeta.RequiredUnits > 0 then defaultUnits = cloudMeta.RequiredUnits end if type(cloudMeta.Mode) == "string" and cloudMeta.Mode ~= "" then defaultMode = cloudMeta.Mode end if type(cloudMeta.Map) == "string" and cloudMeta.Map ~= "" then defaultMap = cloudMeta.Map end end local foundUnits = {} local seenUnits = {} local mode = nil local map = nil local stepCount = 0 local function scanStepsTable(stepsTbl, rawMode, rawMap) if type(rawMode) == "string" and rawMode ~= "" and rawMode ~= "Voting" then mode = rawMode end if type(rawMap) == "string" and rawMap ~= "" then map = rawMap end if type(stepsTbl) == "table" then stepCount = #stepsTbl for _, s in ipairs(stepsTbl) do if type(s) == "table" then if s.Action == "ModeVote" and type(s.Mode) == "string" and s.Mode ~= "" and not mode then mode = s.Mode elseif s.Action == "TowerAdd" and type(s.TowerName) == "string" and s.TowerName ~= "" then local tName = s.TowerName:match("^%s*(.-)%s*$") local tLow = tName:lower() if tName ~= "" and not seenUnits[tLow] then seenUnits[tLow] = true table.insert(foundUnits, tName) end end end end end end pcall(function() local path = getMacroFilePath(clean) if isfile and readfile and isfile(path) then local raw = readfile(path) if type(raw) == "string" and #raw > 5 then local decoded = HttpService:JSONDecode(raw) if type(decoded) == "table" then scanStepsTable(decoded.Steps or decoded, decoded.Mode, decoded.Map) end end elseif tostring(Cfg.SelectedMacro or "") == clean and type(MacroState.Steps) == "table" and #MacroState.Steps > 0 then scanStepsTable(MacroState.Steps, MacroState.ModeName, MacroState.MapName) end end) if #foundUnits == 0 then for _, u in ipairs(defaultUnits) do table.insert(foundUnits, u) end end -- Ưu tiên Mode cấu hình trên Cloud / File Macro, nếu không có thì lấy theo quy tắc mặc định (1..4 Easy, 5..6 Normal, 7..8 Hard) local canonicalMode = State.GetCanonicalMacroMode(clean) local meta = { Macro = clean, Units = foundUnits, Mode = (type(cloudMeta) == "table" and cloudMeta.Mode and cloudMeta.Mode ~= "" and cloudMeta.Mode) or canonicalMode or mode or defaultMode, Map = map or defaultMap, StepCount = stepCount, CachedAt = now, } State.MacroMetaCache[clean] = meta return meta end -- Phân tích chuỗi cấu hình StepChainText từ Website (hỗ trợ: '>' tăng bậc, '|' ngang bậc, '~' chỉ Equip phụ không Roll, '(Mode)', ':ExtraUnits'): -- VD: "~cac(Easy) > ~cac2(Easy) | ~cac3(Easy) | ~cac4(Easy) > ~cac5(Normal) | ~cac6(Normal) > cac7(Hard) > cac8(Hard):Titan Cinemaman,Glitch Cameraman" function State.GetResolvedSteps(data) data = data or getReplicaData() local rawSteps = {} local hasExplicitNoRollPrefix = false if type(Cfg.ProgressionSteps) == "table" and #Cfg.ProgressionSteps > 0 then for idx, item in ipairs(Cfg.ProgressionSteps) do if type(item) == "string" then if item:find("~", 1, true) then hasExplicitNoRollPrefix = true end table.insert(rawSteps, { RawToken = item, Priority = idx }) elseif type(item) == "table" and item.Macro then if item.NoRoll ~= nil then hasExplicitNoRollPrefix = true end table.insert(rawSteps, item) end end else local chainStr = tostring(Cfg.StepChainText or DEFAULT_CFG.StepChainText or "") if chainStr:find("~", 1, true) then hasExplicitNoRollPrefix = true end chainStr = chainStr:gsub("%->", ">"):gsub("[\r\n;]+", ">") local tierNum = 0 for tierSeg in chainStr:gmatch("([^>]+)") do local cleanSeg = tierSeg:match("^%s*(.-)%s*$") if cleanSeg ~= "" then tierNum = tierNum + 1 local subIdx = 0 for subToken in cleanSeg:gmatch("([^|]+)") do local trimmed = subToken:match("^%s*(.-)%s*$") if trimmed ~= "" then subIdx = subIdx + 1 local explicitP, restTok = trimmed:match("^%[%s*[pP]?(%d+)%s*%]%s*(.+)$") if explicitP and restTok then table.insert(rawSteps, { RawToken = restTok:match("^%s*(.-)%s*$"), Priority = tonumber(explicitP) or tierNum, SubOrder = subIdx, }) else table.insert(rawSteps, { RawToken = trimmed, Priority = tierNum, SubOrder = subIdx, }) end end end end end end if #rawSteps == 0 then hasExplicitNoRollPrefix = true rawSteps = { { RawToken = "~cac(Easy)", Priority = 1, SubOrder = 1 }, { RawToken = "~cac2(Easy)", Priority = 2, SubOrder = 1 }, { RawToken = "~cac3(Easy)", Priority = 2, SubOrder = 2 }, { RawToken = "~cac4(Easy)", Priority = 2, SubOrder = 3 }, { RawToken = "~cac5(Normal)", Priority = 3, SubOrder = 1 }, { RawToken = "~cac6(Normal)", Priority = 3, SubOrder = 2 }, { RawToken = "cac7(Hard)", Priority = 4, SubOrder = 1 }, { RawToken = "cac8(Hard):Titan Cinemaman,Glitch Cameraman", Priority = 5, SubOrder = 1 }, } end -- Chuẩn hóa số thứ tự Priority liên tiếp (1..MaxPriority) và đếm số lượng Step ngang bậc trong mỗi Priority local uniqueP = {} local seenP = {} for _, entry in ipairs(rawSteps) do local p = tonumber(entry.Priority) or 1 entry.Priority = p if not seenP[p] then seenP[p] = true table.insert(uniqueP, p) end end table.sort(uniqueP) local pRemap = {} for normIdx, origP in ipairs(uniqueP) do pRemap[origP] = normIdx end local pGroupCounts = {} local pSubCounters = {} for _, entry in ipairs(rawSteps) do local np = pRemap[entry.Priority] or 1 entry.Priority = np pGroupCounts[np] = (pGroupCounts[np] or 0) + 1 pSubCounters[np] = (pSubCounters[np] or 0) + 1 entry.SubOrder = pSubCounters[np] end local resolved = {} for idx, entry in ipairs(rawSteps) do local macroName = entry.Macro local modeOverride = entry.Mode local mapOverride = entry.Map local noRoll = (entry.NoRoll == true) local extraUnits = {} if type(entry.HuntUnits) == "table" then for _, u in ipairs(entry.HuntUnits) do local cu = tostring(u or ""):match("^%s*(.-)%s*$") if cu ~= "" then table.insert(extraUnits, cu) end end end if entry.RawToken then local tokStr = tostring(entry.RawToken):match("^%s*(.-)%s*$") if tokStr:sub(1, 1) == "~" then noRoll = true tokStr = tokStr:sub(2):match("^%s*(.-)%s*$") end local leftPart, rightPart = tokStr:match("^([^:]+):(.*)$") if not leftPart then leftPart = tokStr rightPart = "" end leftPart = leftPart:match("^%s*(.-)%s*$") local baseMacro, parenContent = leftPart:match("^(.-)%s*%(([^%)]+)%)%s*$") if baseMacro and baseMacro ~= "" then macroName = baseMacro:match("^%s*(.-)%s*$") local pMode, pMap = parenContent:match("^([^@]+)@(.+)$") if pMode then modeOverride = pMode:match("^%s*(.-)%s*$") mapOverride = pMap:match("^%s*(.-)%s*$") else modeOverride = parenContent:match("^%s*(.-)%s*$") end else macroName = leftPart end if rightPart and rightPart ~= "" then for uToken in rightPart:gmatch("([^,%+/]+)") do local cu = uToken:match("^%s*(.-)%s*$") if cu ~= "" then table.insert(extraUnits, cu) end end end end macroName = tostring(macroName or "cac"):match("^%s*(.-)%s*$") if macroName == "" then macroName = "cac" end if macroName:sub(1, 1) == "~" then noRoll = true macroName = macroName:sub(2):match("^%s*(.-)%s*$") end -- Nếu chuỗi cũ chưa gắn cờ '~', mặc định cac/cac1..cac6 là Step phụ (NoRoll = true) và cac7/cac8 là Step chủ động săn Banner if not hasExplicitNoRollPrefix and entry.NoRoll == nil then local lowM = macroName:lower() if lowM == "cac" or lowM == "cac1" or lowM == "cac2" or lowM == "cac3" or lowM == "cac4" or lowM == "cac5" or lowM == "cac6" then noRoll = true end end local meta = State.InspectMacroMeta(macroName, false) local macroUnits = {} local allUnits = {} local seenAll = {} for _, u in ipairs(meta.Units or {}) do local cu = tostring(u):match("^%s*(.-)%s*$") if cu ~= "" then table.insert(macroUnits, cu) local low = cu:lower() if not seenAll[low] then seenAll[low] = true table.insert(allUnits, cu) end end end if type(entry.RequiredUnits) == "table" then for _, u in ipairs(entry.RequiredUnits) do local cu = tostring(u):match("^%s*(.-)%s*$") local low = cu:lower() if cu ~= "" and not seenAll[low] then seenAll[low] = true table.insert(macroUnits, cu) table.insert(allUnits, cu) end end end for _, u in ipairs(extraUnits) do local cu = tostring(u):match("^%s*(.-)%s*$") local low = cu:lower() if cu ~= "" and not seenAll[low] then seenAll[low] = true table.insert(allUnits, cu) end end local macroOwned = true local macroEquipped = true local missingMacroUnits = {} for _, u in ipairs(macroUnits) do local st = getUnitInventoryStatus(data, u) if not st.Owned then macroOwned = false table.insert(missingMacroUnits, u) end if not st.Equipped then macroEquipped = false end end local allOwned = true local allEquipped = true local missingUnits = {} local unequippedOwned = {} local unitStatuses = {} for _, u in ipairs(allUnits) do local st = getUnitInventoryStatus(data, u) unitStatuses[u] = st if not st.Owned then allOwned = false allEquipped = false table.insert(missingUnits, u) elseif not st.Equipped then allEquipped = false table.insert(unequippedOwned, u) end end local pVal = tonumber(entry.Priority) or idx table.insert(resolved, { Index = idx, Priority = pVal, SubOrder = tonumber(entry.SubOrder) or 1, GroupSize = pGroupCounts[pVal] or 1, Macro = meta.Macro or macroName, Mode = (modeOverride and modeOverride ~= "" and modeOverride) or meta.Mode or "Easy", Map = (mapOverride and mapOverride ~= "" and mapOverride) or meta.Map or "Toilet City", NoRoll = noRoll, HuntEnabled = not noRoll, MacroUnits = macroUnits, MissingMacroUnits = missingMacroUnits, ExtraUnits = extraUnits, AllUnits = allUnits, MacroUnitsOwned = macroOwned, MacroUnitsEquipped = macroEquipped, AllUnitsOwned = allOwned, AllUnitsEquipped = allEquipped, MissingUnits = missingUnits, UnequippedOwned = unequippedOwned, UnitStatuses = unitStatuses, }) end return resolved end -- Tính toán Step đang kích hoạt (ActiveStep) & Mục Tiêu Săn Banner 100% Động theo Cấu Hình Website: -- 1. Chọn Macro để cày: Luôn chọn Step có Priority cao nhất mà Acc đã sở hữu đủ MacroUnits trong kho (cả Step '📦 Phụ' lẫn '🎯 Săn Banner'). -- 2. Chọn Tướng để săn trên Banner: -- - Quét lần lượt từng Bậc Priority (1..MaxPriority): tìm Bậc ĐẦU TIÊN có bật '🎯 Săn Banner' (HuntEnabled = true) mà chưa mở khóa đủ MacroUnits. -- - Chỉ săn các tướng MacroUnits còn thiếu của Bậc đó (không nhảy cóc sang Bậc sau). -- - Khi toàn bộ các Bậc '🎯 Săn Banner' đã mở khóa đủ MacroUnits -> chuyển sang săn liên tục danh sách ':ExtraUnits' (VD: Titan Cinemaman, Glitch Cameraman) mỗi khi có trên Banner và đủ Vàng! function State.ResolveActiveStepState(data) data = data or getReplicaData() local steps = State.GetResolvedSteps(data) local totalSteps = #steps local tiers = {} local maxPriority = 1 for _, s in ipairs(steps) do local p = tonumber(s.Priority) or 1 if p > maxPriority then maxPriority = p end tiers[p] = tiers[p] or { Priority = p, Candidates = {}, LobbyUnlocked = false, MatchUnlocked = false } table.insert(tiers[p].Candidates, s) end local lobbyStepIdx = 1 local matchStepIdx = 1 local lobbyPriority = 1 local matchPriority = 1 for p = 1, maxPriority do local tier = tiers[p] if tier then local prevTier = tiers[p - 1] local bestLobbyCand = nil local bestMatchCand = nil for _, s in ipairs(tier.Candidates) do local isSameMacroHunt = false if prevTier then for _, ps in ipairs(prevTier.Candidates) do if ps.Macro == s.Macro and #s.ExtraUnits > 0 then isSameMacroHunt = true break end end end local canLobby = s.MacroUnitsOwned local canMatch = s.MacroUnitsEquipped if isSameMacroHunt and prevTier then local prevAllOwned = false for _, ps in ipairs(prevTier.Candidates) do if ps.AllUnitsOwned then prevAllOwned = true break end end canLobby = canLobby and prevAllOwned canMatch = canMatch and prevAllOwned end if canLobby then if not bestLobbyCand then bestLobbyCand = s elseif not bestLobbyCand.MacroUnitsEquipped and s.MacroUnitsEquipped then bestLobbyCand = s end end if canMatch and not bestMatchCand then bestMatchCand = s end end if bestLobbyCand then tier.LobbyUnlocked = true tier.BestLobbyStep = bestLobbyCand lobbyStepIdx = bestLobbyCand.Index lobbyPriority = p end if bestMatchCand then tier.MatchUnlocked = true tier.BestMatchStep = bestMatchCand matchStepIdx = bestMatchCand.Index matchPriority = p end end end local activeIdx = (CURRENT_PLACE == PLACE_LOBBY) and lobbyStepIdx or matchStepIdx local activeStep = steps[activeIdx] or steps[1] local lobbyStep = steps[lobbyStepIdx] or steps[1] local activePriority = (activeStep and activeStep.Priority) or 1 for _, s in ipairs(steps) do s.TierSatisfied = (s.Priority <= lobbyPriority) end -- Kiểm tra xem trong kho đã có tướng Macro bắt buộc của Bậc Priority cao hơn chưa được Equip vào Hotbar không local needsReturnToEquip = false local equipReason = "" if lobbyPriority > matchPriority or (not activeStep.MacroUnitsEquipped and activeStep.MacroUnitsOwned) then needsReturnToEquip = true equipReason = string.format("🎯 Đã mở khóa Bậc P%d [%s] (%s - %s) -> Về Lobby Equip ngay!", lobbyPriority, table.concat(lobbyStep.MacroUnits, ", "), lobbyStep.Macro, lobbyStep.Mode) end -- Tìm giai đoạn săn Banner động theo cấu hình Website: -- 1. Quét các Bậc Priority p = 1..maxPriority có ít nhất 1 Step bật HuntEnabled (không có dấu '~'): -- Nếu Bậc đó chưa mở khóa (not tier.LobbyUnlocked), dừng ngay tại Bậc đó và chỉ săn MissingMacroUnits của các Step HuntEnabled thuộc Bậc đó! -- 2. Nếu toàn bộ các Bậc HuntEnabled đều đã mở khóa MacroUnits, chuyển sang săn ExtraUnits (VD: Titan Cinemaman, Glitch Cameraman) của Step đang chạy / Step cuối! local huntPhase = 0 local huntPriority = nil local huntStepIdx = nil local primaryHuntUnits = {} local lookaheadHuntUnits = {} local seenHunt = {} local hasContinuousExtraHunt = false for p = 1, maxPriority do local tier = tiers[p] if tier then local hasHuntableStepInTier = false for _, s in ipairs(tier.Candidates) do if s.HuntEnabled and #s.MacroUnits > 0 then hasHuntableStepInTier = true break end end if hasHuntableStepInTier then huntPhase = huntPhase + 1 if not tier.LobbyUnlocked then huntPriority = p for _, s in ipairs(tier.Candidates) do if s.HuntEnabled and #s.MissingMacroUnits > 0 then s.IsCurrentHuntCandidate = true huntStepIdx = huntStepIdx or s.Index for _, u in ipairs(s.MissingMacroUnits) do local low = u:lower() if not seenHunt[low] then seenHunt[low] = true table.insert(primaryHuntUnits, u) end end end end break end end end end -- Khi đã hoàn tất toàn bộ các Bậc MacroUnits có bật HuntEnabled -> chuyển sang Giai đoạn săn ExtraUnits (VD: Titan Cinemaman, Glitch Cameraman) if not huntPriority then huntPhase = huntPhase + 1 for i = #steps, 1, -1 do local s = steps[i] if s.ExtraUnits and #s.ExtraUnits > 0 then s.IsCurrentHuntCandidate = true huntStepIdx = huntStepIdx or s.Index huntPriority = huntPriority or s.Priority hasContinuousExtraHunt = true for _, eu in ipairs(s.ExtraUnits) do local low = tostring(eu or ""):lower():match("^%s*(.-)%s*$") if low ~= "" and not seenHunt[low] then seenHunt[low] = true table.insert(primaryHuntUnits, eu) end end end end end return { Steps = steps, Tiers = tiers, TotalSteps = totalSteps, MaxPriority = maxPriority, ActiveIndex = activeIdx, ActivePriority = activePriority, LobbyStepIndex = lobbyStepIdx, LobbyPriority = lobbyPriority, MatchStepIndex = matchStepIdx, MatchPriority = matchPriority, ActiveStep = activeStep, LobbyStep = lobbyStep, HuntPhase = math.max(1, huntPhase), HuntStepIndex = huntStepIdx, HuntPriority = huntPriority, PrimaryHuntUnits = primaryHuntUnits, LookaheadHuntUnits = lookaheadHuntUnits, AllCompleted = (#primaryHuntUnits == 0 and not hasContinuousExtraHunt), NeedsReturnToEquip = needsReturnToEquip, EquipReason = equipReason, } end function State.GetActiveStepMode(data) local st = State.ResolveActiveStepState(data) return (st.ActiveStep and st.ActiveStep.Mode) or Cfg.VoteMode or "Easy" end function State.GetActiveStepMap(data) local st = State.ResolveActiveStepState(data) return (st.ActiveStep and st.ActiveStep.Map) or Cfg.PreferredMap or "Toilet City" end local function evaluateStarterBannerSummon(data) data = data or getReplicaData() local pipe = State.ResolveActiveStepState(data) local activeStep = pipe.ActiveStep local money = (data and tonumber(data.Money)) or 0 local minRollMoney = math.max(100, tonumber(Cfg.ReturnMoneyThreshold) or 900) -- Đảm bảo dữ liệu Banner được cập nhật mới nhất (từ Lobby hoặc VPS) local info = getBannerTimerInfo() if not info.IsCurrent then if CURRENT_PLACE == PLACE_LOBBY then syncLobbyBanner(false) else pullBannerFromVPS(false) end info = getBannerTimerInfo() end local bc = info.BasicCrate or {} local bannerKnown = (info.HasUnits == true) local huntStage = pipe.HuntStepIndex or pipe.ActiveIndex local huntPhase = pipe.HuntPhase or 1 local missingStr = table.concat(pipe.PrimaryHuntUnits, " / ") if not bannerKnown then return { Stage = huntStage, HuntPhase = huntPhase, ActiveStepIndex = pipe.ActiveIndex, ActiveMacro = activeStep.Macro, ActiveMode = activeStep.Mode, ActiveMap = activeStep.Map, TargetUnit = missingStr, BannerHasTarget = false, CanSummon = false, WantSingleRollOnly = false, NeedsReturnToEquip = pipe.NeedsReturnToEquip, EquipReason = pipe.EquipReason, BannerKnown = false, Reason = string.format("📡 [GĐ%d | %s %s | $%d] Đang đồng bộ Banner VPS để săn [%s] (Reset: %s)...", huntPhase, activeStep.Macro, activeStep.Mode, money, missingStr, info.TimeFormatted), } end -- Kiểm tra xem trên Banner hiện tại có đúng tướng mục tiêu của Giai Đoạn hiện tại không: -- * GĐ 1: Ninja Cameraman -- * GĐ 2: Popcorn Cameraman -- * GĐ 3: Titan Cinemaman hoặc Glitch Cameraman local matchedUnit = nil for _, u in ipairs(pipe.PrimaryHuntUnits) do if isUnitOnActiveBanner(u) then matchedUnit = u break end end if matchedUnit then -- Quy tắc nghiêm ngặt: TRÊN 900 VÀNG MỚI ROLL, DƯỚI 900 VÀNG NHẤT ĐỊNH KHÔNG ROLL! if money < minRollMoney then return { Stage = huntStage, HuntPhase = huntPhase, ActiveStepIndex = pipe.ActiveIndex, ActiveMacro = activeStep.Macro, ActiveMode = activeStep.Mode, ActiveMap = activeStep.Map, TargetUnit = matchedUnit, BannerHasTarget = true, CanSummon = false, WantSingleRollOnly = false, NeedsReturnToEquip = pipe.NeedsReturnToEquip, EquipReason = pipe.EquipReason, BannerKnown = true, Reason = string.format("💰 [GĐ%d | %s %s] Banner có %s nhưng Vàng $%d < $%d -> Dưới 900 vàng KHÔNG Roll, vào Map cày tiếp!", huntPhase, activeStep.Macro, activeStep.Mode, matchedUnit:upper(), money, minRollMoney), } end return { Stage = huntStage, HuntPhase = huntPhase, ActiveStepIndex = pipe.ActiveIndex, ActiveMacro = activeStep.Macro, ActiveMode = activeStep.Mode, ActiveMap = activeStep.Map, TargetUnit = matchedUnit, BannerHasTarget = true, CanSummon = true, WantSingleRollOnly = false, NeedsReturnToEquip = pipe.NeedsReturnToEquip, EquipReason = pipe.EquipReason, BannerKnown = true, Reason = string.format("🔥 [GĐ%d | %s %s] BANNER CÓ %s & Vàng $%d (>= $%d, Còn %s) -> Xả tiền Roll x10 ngay!", huntPhase, activeStep.Macro, activeStep.Mode, matchedUnit:upper(), money, minRollMoney, info.TimeFormatted), } end return { Stage = huntStage, HuntPhase = huntPhase, ActiveStepIndex = pipe.ActiveIndex, ActiveMacro = activeStep.Macro, ActiveMode = activeStep.Mode, ActiveMap = activeStep.Map, TargetUnit = missingStr, BannerHasTarget = false, CanSummon = false, WantSingleRollOnly = false, NeedsReturnToEquip = pipe.NeedsReturnToEquip, EquipReason = pipe.EquipReason, BannerKnown = true, Reason = string.format("⏳ [GĐ%d | %s %s | $%d] Chỉ săn [%s] (Leg: %s | Mythic: %s | Đổi: %s) -> Không Roll, Cày tiếp!", huntPhase, activeStep.Macro, activeStep.Mode, money, missingStr, tostring(bc.Legendary or "?"), tostring(bc.Mythic or "?"), info.TimeFormatted), } end local function ensureStarterUnitsEquipped(data) if not data or type(data.Towers) ~= "table" then return end local maxSlots = tonumber(data.MaxEquipped) or (Modules.Config and Modules.Config.GameSettings and tonumber(Modules.Config.GameSettings.HotbarSlots)) or 5 local equippedCount = 0 for _, unit in pairs(data.Towers) do if type(unit) == "table" and isUnitEquippedVal(unit.Equipped) then equippedCount = equippedCount + 1 end end local pipe = State.ResolveActiveStepState(data) local steps = pipe.Steps local lobbyStep = pipe.LobbyStep -- Sắp xếp thứ tự ưu tiên Equip vào Hotbar (tối đa maxSlots = 5 ô): -- 1. Toàn bộ MacroUnits của Step cao nhất đã mở khóa ở Lobby (bắt buộc phải có để chạy Macro!) -- 2. Các tướng ExtraUnits / HuntUnits đã sở hữu trong chuỗi Step (VD: Titan Cinemaman, Glitch Cameraman) -- 3. Các tướng MacroUnits của các Step trước đó nếu còn trống slot local desiredUnits = {} local seenDesired = {} local function addDesiredUnit(uName, roleTag) local clean = tostring(uName or ""):match("^%s*(.-)%s*$") local low = clean:lower() if clean == "" or seenDesired[low] or #desiredUnits >= maxSlots then return end local st = getUnitInventoryStatus(data, clean) if st.Owned and st.UUID then seenDesired[low] = true table.insert(desiredUnits, { Name = clean, UUID = st.UUID, Equipped = st.Equipped, RoleTag = roleTag or clean, }) end end if lobbyStep then for _, u in ipairs(lobbyStep.MacroUnits) do addDesiredUnit(u, string.format("Macro %s (%s)", lobbyStep.Macro, lobbyStep.Mode)) end for _, u in ipairs(lobbyStep.ExtraUnits) do addDesiredUnit(u, string.format("Step %d Extra", lobbyStep.Index)) end end for i = #steps, 1, -1 do local s = steps[i] for _, u in ipairs(s.ExtraUnits) do addDesiredUnit(u, string.format("Step %d Target", s.Index)) end end for i = #steps, 1, -1 do local s = steps[i] for _, u in ipairs(s.MacroUnits) do addDesiredUnit(u, string.format("Step %d (%s)", s.Index, s.Macro)) end end local needEquipList = {} for _, d in ipairs(desiredUnits) do if not d.Equipped then table.insert(needEquipList, d) end end if #needEquipList == 0 then return end if equippedCount + #needEquipList > maxSlots then setStatus("⚙️ Đang làm mới Hotbar để Equip bộ tướng cho Step hiện tại...") callRemote("EquipPart", "UnequipAll") task.wait(0.35) for _, d in ipairs(desiredUnits) do callRemote("EquipPart", d.UUID) end else for _, d in ipairs(needEquipList) do setStatus(string.format("🎯 Auto-Equipping %s [%s] vào Hotbar...", d.Name, d.RoleTag)) callRemote("EquipPart", d.UUID) end end -- Chờ tối đa 1.2s để Replica.Data.Towers xác nhận .Equipped == true trước khi vào thang máy! local t0 = os.clock() while os.clock() - t0 < 1.2 and isAlive() do local repNow = getReplicaData() local allReady = true for _, d in ipairs(desiredUnits) do if not getUnitInventoryStatus(repNow, d.Name).Equipped then allReady = false break end end if allReady then break end task.wait(0.1) end end -- Tự động chọn Macro, Độ khó (VoteMode) & Map (PreferredMap) theo Step cao nhất hiện tại (Có thể thay đổi bất kỳ lúc nào!) local function syncStarterMacroByCameraman(data) if not Cfg.AutoStarterCameraman or MacroState.IsRecording or MacroState.ManualMacroLocked then return end data = data or getReplicaData() if not data or type(data.Towers) ~= "table" then return end local pipe = State.ResolveActiveStepState(data) local activeStep = pipe.ActiveStep if not activeStep then return end local desiredMacro = tostring(activeStep.Macro or "cac") local desiredMode = tostring(activeStep.Mode or "Easy") local desiredMap = tostring(activeStep.Map or "Toilet City") if Cfg.PreferredMap ~= desiredMap then Cfg.PreferredMap = desiredMap end if Cfg.VoteMode ~= desiredMode then if UIRefs and UIRefs.SetVoteModeDropdown then UIRefs.SetVoteModeDropdown(desiredMode) else Cfg.VoteMode = desiredMode end end if Cfg.SelectedMacro ~= desiredMacro or #MacroState.Steps == 0 then if UIRefs and UIRefs.SetMacroDropdown then UIRefs.SetMacroDropdown(desiredMacro) else Cfg.SelectedMacro = desiredMacro end loadMacroFromDisk(desiredMacro) end end function State.FormatStepPipelineText(data) data = data or getReplicaData() local pipe = State.ResolveActiveStepState(data) local huntTargets = table.concat(pipe.PrimaryHuntUnits or {}, " / ") local minRoll = math.max(100, tonumber(Cfg.ReturnMoneyThreshold) or 900) local lines = { string.format("⚡ SMART STEP PIPELINE (%s | GĐ Săn %d: [%s >= $%d] | Đang chạy: %s [%s])", Cfg.AutoStarterCameraman and "ON" or "OFF", tonumber(pipe.HuntPhase) or 1, huntTargets ~= "" and huntTargets or "None", minRoll, tostring(pipe.ActiveStep and pipe.ActiveStep.Macro or "?"), tostring(pipe.ActiveStep and pipe.ActiveStep.Mode or "?")), } for _, s in ipairs(pipe.Steps) do local extraSet = {} for _, eu in ipairs(s.ExtraUnits or {}) do extraSet[tostring(eu):lower()] = true end local unitParts = {} for _, u in ipairs(s.AllUnits) do local st = s.UnitStatuses[u] local badge if extraSet[tostring(u):lower()] then local cnt = (st and tonumber(st.Count)) or 0 badge = string.format("♾️Săn[x%d]", cnt) else badge = (st and st.Equipped and "✅Eq") or (st and st.Owned and "📦Kho") or "❌Thiếu" end table.insert(unitParts, string.format("%s(%s)", u, badge)) end local uStr = (#unitParts > 0) and table.concat(unitParts, ", ") or "Auto" local pTag = (s.GroupSize and s.GroupSize > 1) and string.format("P%d⚖️", s.Priority) or string.format("P%d", s.Priority or s.Index) local rollTag = s.HuntEnabled and "🎯Săn" or "📦Phụ" local stateTag = "" if s.Index == pipe.ActiveIndex then stateTag = " 👈 [ĐANG CHẠY]" elseif s.IsCurrentHuntCandidate or s.Index == pipe.HuntStepIndex then stateTag = " 🎯 [MỤC TIÊU SĂN]" elseif s.AllUnitsOwned then stateTag = " ✔️" elseif s.TierSatisfied then stateTag = string.format(" ⏭️ [Qua P%d]", s.Priority or 1) end table.insert(lines, string.format("#%d [%s | %s %s | %s] -> %s%s", s.Index, pTag, s.Macro, s.Mode, rollTag, uStr, stateTag)) end return table.concat(lines, "\n") end function State.ApplyRemoteStepConfig(stepCfg, forceApply) if type(stepCfg) ~= "table" then return false end local remoteChain = tostring(stepCfg.StepChainText or ""):match("^%s*(.-)%s*$") if remoteChain == "" then return false end State.LastCloudStepConfig = stepCfg if type(stepCfg.Steps) == "table" then State.CloudStepMacros = State.CloudStepMacros or {} for _, s in ipairs(stepCfg.Steps) do if type(s) == "table" and type(s.Macro) == "string" then State.CloudStepMacros[s.Macro:lower()] = { RequiredUnits = s.RequiredUnits, Mode = s.Mode, Map = s.Map, StepCount = s.StepCount, } end end end local updAt = tonumber(stepCfg.UpdatedAt) or 0 local prevUpd = tonumber(State.LastCloudStepUpdatedAt) or 0 local retMoney = tonumber(stepCfg.ReturnMoneyThreshold) if retMoney then retMoney = math.max(100, retMoney) end local changed = false if forceApply or (updAt > 0 and updAt > prevUpd) then State.LastCloudStepUpdatedAt = updAt if retMoney and retMoney > 0 and Cfg.ReturnMoneyThreshold ~= retMoney then Cfg.ReturnMoneyThreshold = retMoney changed = true end if Cfg.StepChainText ~= remoteChain then State.ApplyStepChainNow(remoteChain, true) return true elseif changed then saveConfig() end end return changed end function State.PushStepConfigToVPS(chainText, retMoney) local clean = tostring(chainText or Cfg.StepChainText or DEFAULT_CFG.StepChainText):match("^%s*(.-)%s*$") if clean == "" then return false end local res = httpRequestSafe({ Url = State.STEPS_VPS_URL, Method = "POST", Headers = { ["Content-Type"] = "application/json", ["User-Agent"] = "CTD-Client-StepPush/1.0", }, Body = HttpService:JSONEncode({ StepChainText = clean, ReturnMoneyThreshold = tonumber(retMoney or Cfg.ReturnMoneyThreshold) or 900, }), }, 6.0) if res and (res.StatusCode == 200 or res.StatusCode == nil) and type(res.Body) == "string" then local ok, decoded = pcall(function() return HttpService:JSONDecode(res.Body) end) if ok and type(decoded) == "table" and decoded.ok then State.LastCloudStepUpdatedAt = tonumber(decoded.UpdatedAt) or os.time() State.ApplyRemoteStepConfig(decoded, true) saveBannerCacheToDisk() return true end end return false end function State.PullStepConfigFromVPS(force) local now = os.clock() if not force and (now - (State.LastStepVPSPullClock or 0) < 18) then return false end State.LastStepVPSPullClock = now local r = httpRequestSafe({ Url = State.STEPS_VPS_URL, Method = "GET", Headers = { ["User-Agent"] = "CTD-Client-StepPull/1.0" }, }, 6.0) if r and (r.StatusCode == 200 or r.StatusCode == nil) and type(r.Body) == "string" and #r.Body > 10 then local ok, decoded = pcall(function() return HttpService:JSONDecode(r.Body) end) if ok and type(decoded) == "table" and decoded.ok then local applied = State.ApplyRemoteStepConfig(decoded, force == true) saveBannerCacheToDisk() return applied end end return false end function State.ApplyStepChainNow(newChainText, fromRemoteVPS) local clean = tostring(newChainText or ""):match("^%s*(.-)%s*$") if clean == "" then clean = DEFAULT_CFG.StepChainText end Cfg.StepChainText = clean Cfg.ProgressionSteps = nil State.MacroMetaCache = {} MacroState.ManualMacroLocked = false saveConfig() if not fromRemoteVPS then task.spawn(function() State.PushStepConfigToVPS(clean, Cfg.ReturnMoneyThreshold) end) end if UIRefs then pcall(function() if UIRefs.StepChainBox then UIRefs.StepChainBox.Text = clean end if UIRefs.MatchStepChainBox then UIRefs.MatchStepChainBox.Text = clean end end) end local repData = getReplicaData() if CURRENT_PLACE == PLACE_LOBBY and Cfg.AutoStarterCameraman and repData then task.spawn(function() ensureStarterUnitsEquipped(repData) syncStarterMacroByCameraman(getReplicaData() or repData) end) elseif Cfg.AutoStarterCameraman and repData then syncStarterMacroByCameraman(repData) end local pipe = State.ResolveActiveStepState(repData) local act = pipe.ActiveStep local srcTag = fromRemoteVPS and "☁️ [Web VPS Sync]" or "✅" local huntStr = (#pipe.PrimaryHuntUnits > 0) and (" | Săn: " .. table.concat(pipe.PrimaryHuntUnits, ", ")) or " | Đã đủ mọi Tướng!" setStatus(string.format("%s Đã cập nhật Smart Step (%d Steps)! Đang chạy Step %d: %s (%s)%s", srcTag, pipe.TotalSteps, pipe.ActiveIndex, tostring(act and act.Macro or "?"), tostring(act and act.Mode or "?"), huntStr)) return pipe end -- Tự động đồng bộ cấu hình Step từ Website VPS (http://160.191.242.43:8787/steps) khi khởi động và định kỳ mỗi 25s task.spawn(function() task.wait(1.5 + math.random() * 2.0) State.PullStepConfigFromVPS(false) while isAlive() do task.wait(25 + math.random(0, 5)) pcall(State.PullStepConfigFromVPS, false) end end) local function formatUnitBadge(st) if not st or not st.Owned then return "❌ Chưa có" elseif st.Equipped then return string.format("✅ Equipped (x%d)", tonumber(st.Count) or 1) else return string.format("📦 Trong kho (x%d)", tonumber(st.Count) or 1) end end local function sendMatchEndWebhook(opts) opts = opts or {} local webhookUrl = tostring(Cfg.WebhookUrl or DEFAULT_CFG.WebhookUrl or "") if webhookUrl == "" or not webhookUrl:find("discord.com/api/webhooks/", 1, true) then if opts.IsTest then setStatus("⚠️ Webhook URL không hợp lệ!") end return false end -- Wait a brief moment on match end so Replica.Data.Money / Wins / Battlepass.EXP reflect match rewards if not opts.IsTest then task.wait(0.25) end local repData = getReplicaData() local money = (repData and tonumber(repData.Money)) or 0 local candy = (repData and tonumber(repData.Candy)) or 0 local gems = (repData and tonumber(repData.Gems)) or 0 local totalWins = (repData and tonumber(repData.Wins)) or 0 local bp = repData and repData.Battlepass local bpExp = (type(bp) == "table" and tonumber(bp.EXP)) or 0 local bpTier = (type(bp) == "table" and tonumber(bp.CurrentTier)) or 0 local spkStatus = getUnitInventoryStatus(repData, "Speakerman") local camStatus = getUnitInventoryStatus(repData, "Cameraman") local ninjaStatus = getUnitInventoryStatus(repData, "Ninja Cameraman") local cinemaStatus = getUnitInventoryStatus(repData, "Titan Cinemaman") local glitchStatus = getUnitInventoryStatus(repData, "Glitch Cameraman") local bannerEval = evaluateStarterBannerSummon(repData) local bannerInfo = getBannerTimerInfo() local totalUnits = 0 local rareCounts = {} if repData and type(repData.Towers) == "table" then for _, unit in pairs(repData.Towers) do if type(unit) == "table" and type(unit.Name) == "string" then totalUnits = totalUnits + 1 local st = getTowerAnalysis(unit.Name) local rRank = (st and st.RarityRank) or 1 if rRank >= 4 then rareCounts[unit.Name] = (rareCounts[unit.Name] or 0) + 1 end end end end local rareList = {} for uName, cnt in pairs(rareCounts) do table.insert(rareList, string.format("%s (x%d)", uName, cnt)) end table.sort(rareList) local rareSummary = (#rareList > 0) and table.concat(rareList, ", ") or "Chưa có tướng Legendary+" local isTest = (opts.IsTest == true) local isWin = (opts.IsWin == true) local wave = tonumber(opts.Wave) or tonumber(workspace:GetAttribute("Wave")) or 0 local maxWaves = tonumber(opts.MaxWaves) or tonumber(workspace:GetAttribute("MaxWaves")) or 20 local modeName = tostring(opts.Mode or workspace:GetAttribute("Mode") or Cfg.VoteMode or "Easy") local mapName = tostring(opts.Map or workspace:GetAttribute("Map") or Cfg.PreferredMap or "Toilet City") local durationSec = math.max(0, math.floor(os.clock() - (State.MatchStartClock or os.clock()))) local durationStr = string.format("%02d:%02d", math.floor(durationSec / 60), durationSec % 60) local bc = bannerInfo.BasicCrate or {} local bannerLine = string.format( "Reset: **%s**\nGodly: `%s` | Mythic: `%s`\nLegendary: `%s` | Epic: `%s`", tostring(bannerInfo.TimeFormatted or "??:??"), tostring(bc.Godly or "?"), tostring(bc.Mythic or "?"), tostring(bc.Legendary or "?"), tostring(bc.Epic or "?") ) local nextActionStr = tostring(opts.NextAction or "") if nextActionStr == "" then if opts.ShouldReturnLobby then nextActionStr = "🚪 Về Lobby (" .. tostring(opts.ReturnReason or "Roll / Equip") .. ")" elseif CURRENT_PLACE == PLACE_LOBBY then nextActionStr = "🏠 Đang ở Lobby" else nextActionStr = "🔄 Vote PlayAgain (Cày tiếp trong trận)" end end local titleStr = isTest and "📡 [TEST WEBHOOK] Camera Tower Defense — Báo Cáo Tài Khoản" or (isWin and "🏆 [VICTORY] Camera Tower Defense — Kết Thúc Trận Đấu" or "💀 [DEFEAT] Camera Tower Defense — Kết Thúc Trận Đấu") local embedColor = isTest and 3447003 or (isWin and 3066993 or 15158332) local resultTag = isTest and "🧪 Kiểm tra kết nối Webhook" or (isWin and "✅ THẮNG (YOU WON!)" or ("❌ THUA (" .. tostring(opts.Message or opts.GameState or "LOST") .. ")")) local payload = { username = "CTD Auto Farm Bot", embeds = { { title = titleStr, color = embedColor, fields = { { name = "👤 Tài khoản (Account)", value = string.format("**%s** (`%d`)\nPlace: `%s`", tostring(LocalPlayer.Name), tonumber(LocalPlayer.UserId) or 0, CURRENT_PLACE == PLACE_MATCH and "In Match" or "Lobby"), inline = true, }, { name = "⚔️ Thông tin trận đấu (Match)", value = string.format( "Kết quả: **%s**\nMap: **%s** (`%s`)\nWave: **`%d/%d`** | Thời gian: **`%s`**\nMacro: **`%s`** (`%d` steps)\nSession Wins: **`%d/%d`**", resultTag, mapName, modeName, wave, maxWaves, durationStr, tostring(Cfg.SelectedMacro or "Default"), #MacroState.Steps, tonumber(State.WinsSession) or 0, tonumber(State.MatchesPlayed) or 0 ), inline = true, }, { name = "💰 Tài nguyên & Battlepass", value = string.format( "💵 Money: **`$%d`**\n🍬 Candy: **`%d`** | 💎 Gems: **`%d`**\n🏆 Total Wins: **`%d`**\n🎫 Battlepass: **`%d EXP`** (Tier `%d`)", money, candy, gems, totalWins, bpExp, bpTier ), inline = false, }, { name = string.format("🎯 Tiến trình 4-Step (Step %s) & Kho Tướng (%d Units)", tostring(bannerEval.Stage or "?"), totalUnits), value = string.format( "• **Speakerman**: %s\n• **Cameraman**: %s\n• **Ninja Cameraman**: %s\n• **Titan Cinemaman**: %s\n• **Glitch Cameraman**: %s\n• **Legendary+**: `%s`\n• **Trạng thái Step**: %s", formatUnitBadge(spkStatus), formatUnitBadge(camStatus), formatUnitBadge(ninjaStatus), formatUnitBadge(cinemaStatus), formatUnitBadge(glitchStatus), rareSummary, tostring(bannerEval.Reason or "") ), inline = false, }, { name = "🎰 Banner Hiện Tại & Hành Động Tiếp Theo", value = bannerLine .. "\n➡️ **Tiếp theo**: " .. nextActionStr, inline = false, }, }, footer = { text = string.format("CTD Multi-Account Hub • %s", os.date("%Y-%m-%d %H:%M:%S")), }, }, }, } local jsonBody = HttpService:JSONEncode(payload) local sentOk = false for attempt = 1, 3 do local res = httpRequestSafe({ Url = webhookUrl, Method = "POST", Headers = { ["Content-Type"] = "application/json", }, Body = jsonBody, }, 6.0) local code = res and tonumber(res.StatusCode) if res and (code == 200 or code == 204 or code == nil) then sentOk = true break elseif code == 429 and attempt < 3 then -- Discord rate limit when multiple accounts finish simultaneously -> jittered backoff task.wait(1.5 + math.random() * 1.5) else break end end if opts.IsTest then if sentOk then setStatus("✅ Đã gửi thử Discord Webhook thành công!") else setStatus("⚠️ Gửi Discord Webhook thất bại (Kiểm tra HTTP của Executor)!") end end return sentOk end State.LastWaveSkipFire = 0 State.MatchAutoSkipWatcherStarted = false local function syncAutoSkipUI(enabled) pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local gameUI = pg and pg:FindFirstChild("GameUI") if not gameUI then return end local stats = gameUI:FindFirstChild("Stats") local autoSkip = stats and stats:FindFirstChild("AutoSkip") local btn = autoSkip and autoSkip:FindFirstChild("Button") if btn then local stroke = btn:FindFirstChild("UIStroke") local ui1 = btn:FindFirstChild("UI1") local ui2 = btn:FindFirstChild("UI2") if stroke then stroke.Color = enabled and Color3.fromRGB(29, 97, 0) or Color3.fromRGB(106, 0, 0) end if ui1 then ui1.Visible = not enabled end if ui2 then ui2.Visible = enabled end end if enabled then local wsg = gameUI:FindFirstChild("WaveSkipGui") if wsg and workspace:GetAttribute("CanSkip") ~= true then wsg.Visible = false end end end) end local function tryAutoSkipWaveNow(forceImmediate) if CURRENT_PLACE ~= PLACE_MATCH then return false end if not Cfg.Enabled or not Cfg.AutoSkipWave then return false end if workspace:GetAttribute("IsVoting") == true then return false end local wave = tonumber(workspace:GetAttribute("Wave")) or 0 local maxWaves = tonumber(workspace:GetAttribute("MaxWaves")) or 20 if wave < 1 or (maxWaves > 0 and wave >= maxWaves) then return false end local canSkipAttr = workspace:GetAttribute("CanSkip") == true local wsgVisible = false pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local wsg = pg and pg:FindFirstChild("GameUI") and pg.GameUI:FindFirstChild("WaveSkipGui") if wsg and wsg.Visible == true then wsgVisible = true end end) if not canSkipAttr and not wsgVisible then return false end local now = os.clock() if not forceImmediate and (now - State.LastWaveSkipFire) < 0.28 then return false end State.LastWaveSkipFire = now callRemote("WaveSkip") pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local wsg = pg and pg:FindFirstChild("GameUI") and pg.GameUI:FindFirstChild("WaveSkipGui") if wsg and workspace:GetAttribute("CanSkip") ~= true then wsg.Visible = false end end) return true end local function setupMatchAutoSkipWatcher() if State.MatchAutoSkipWatcherStarted or CURRENT_PLACE ~= PLACE_MATCH then return end State.MatchAutoSkipWatcherStarted = true trackConn(workspace:GetAttributeChangedSignal("CanSkip"):Connect(function() if not isAlive() then return end if workspace:GetAttribute("CanSkip") == true and Cfg.Enabled and Cfg.AutoSkipWave then task.spawn(function() for _ = 1, 10 do if not isAlive() or not Cfg.Enabled or not Cfg.AutoSkipWave then break end if workspace:GetAttribute("CanSkip") ~= true then break end tryAutoSkipWaveNow(true) task.wait(0.25) end syncAutoSkipUI(Cfg.AutoSkipWave) end) end end)) task.spawn(function() while isAlive() do pcall(function() if Cfg.Enabled then syncAutoSkipUI(Cfg.AutoSkipWave == true) if Cfg.AutoSkipWave then tryAutoSkipWaveNow(false) end end end) task.wait(0.25) end end) end if CURRENT_PLACE == PLACE_MATCH then setupMatchAutoSkipWatcher() end local function stepMatchAutomation() refreshCurrentPlace() ensureGameModules() setupAntiMacroBypass() setupMatchGameFolderWatcher() setupRecordingInputFallback() setupMatchAutoSkipWatcher() if MacroState.IsRecording then stepMacroRecorderDiff() end if not Cfg.Enabled then if not MacroState.IsRecording then setStatus("Paused (Master Toggle Off)") end return end if Cfg.AutoStarterCameraman and not MacroState.IsRecording then syncStarterMacroByCameraman(getReplicaData()) end local wave = tonumber(workspace:GetAttribute("Wave")) or 0 local isVotingAttr = workspace:GetAttribute("IsVoting") local isVoting = (isVotingAttr == true) or (isVotingAttr == nil and wave == 0 and (tostring(workspace:GetAttribute("Mode") or "") == "" or tostring(workspace:GetAttribute("Mode") or "") == "Voting")) local maxWaves = tonumber(workspace:GetAttribute("MaxWaves")) or 20 local msg = tostring(workspace:GetAttribute("Message") or "") local gameState = tostring(workspace:GetAttribute("GameState") or "") local canSkip = workspace:GetAttribute("CanSkip") == true local matchEnded = false if gameState == "Lost" or gameState == "Won" or msg == "You Won!" or msg == "You Lost!" or msg:find("Won") or msg:find("Lost") or msg:find("Game Over") then matchEnded = true end if matchEnded then if MacroState.IsRecording then stopMacroRecording(true) end State.MatchEndClock = State.MatchEndClock or os.clock() local stuckEndSec = os.clock() - State.MatchEndClock local isFirstEndTick = not State.MatchEndHandled local isWinThisMatch = (msg:find("Won") ~= nil) or (gameState == "Won") if isFirstEndTick then State.MatchEndHandled = true State.MatchesPlayed = State.MatchesPlayed + 1 if isWinThisMatch then State.WinsSession = State.WinsSession + 1 end end if Cfg.AutoMatchEnd then local repData = getReplicaData() local lobbyMoney = (repData and tonumber(repData.Money)) or 0 local lobbyCandy = (repData and tonumber(repData.Candy)) or 0 local targetMoney = math.max(100, tonumber(Cfg.ReturnMoneyThreshold) or 900) local bannerEval = evaluateStarterBannerSummon(repData) local shouldReturnLobby = false local returnReason = "" if Cfg.AutoStarterCameraman then -- Logic Smart Dynamic Step Engine kết hợp Check Banner thông minh từ VPS: -- 1. Nếu ĐÃ có tướng mục tiêu của các Step trong kho nhưng chưa Equip vào hotbar -> Về Lobby Equip ngay! -- 2. Nếu CHƯA có tướng mục tiêu tiếp theo, đã farm đủ >= ReturnMoneyThreshold tiền, VÀ TRÊN BANNER ĐANG CÓ tướng đó (bannerEval.CanSummon == true) -> Về Lobby xả tiền Roll ngay! -- 3. Nếu trên Banner CHƯA có tướng mục tiêu -> Bấm PlayAgain cày tích tiền liên tục! if bannerEval.NeedsReturnToEquip then shouldReturnLobby = true returnReason = bannerEval.EquipReason ~= "" and bannerEval.EquipReason or "🎯 Đã có Unit của Step trong kho -> Về Lobby Equip ngay!" elseif lobbyMoney >= targetMoney and bannerEval.CanSummon then shouldReturnLobby = true returnReason = string.format("🎯 %s (Money: $%d/$%d) -> Về Lobby Roll ngay!", bannerEval.Reason, lobbyMoney, targetMoney) end elseif Cfg.MatchEndAction == "ReturnLobby" then shouldReturnLobby = true returnReason = "Match Ended -> Returning to Lobby..." elseif Cfg.MatchEndAction == "Smart" then if (Cfg.AutoSummon and lobbyMoney >= targetMoney) or (Cfg.AutoBuyHalloweenCrate and lobbyCandy >= Cfg.ReturnCandyThreshold) then shouldReturnLobby = true returnReason = string.format("Smart MatchEnd ($%d) -> Returning to Lobby...", lobbyMoney) end end if isFirstEndTick and Cfg.WebhookOnMatchEnd then task.spawn(sendMatchEndWebhook, { IsWin = isWinThisMatch, Wave = wave, MaxWaves = maxWaves, GameState = gameState, Message = msg, ShouldReturnLobby = shouldReturnLobby, ReturnReason = returnReason, }) end if shouldReturnLobby then setStatus(returnReason ~= "" and returnReason or string.format("🎯 Đã đủ $%d/$%d tiền -> Về Lobby Roll / Equip!", lobbyMoney, targetMoney)) callRemote("OnLeave") task.wait(1.2) callRemote("TeleportBack") if stuckEndSec > 35 then pcall(function() TeleportService:Teleport(PLACE_LOBBY, LocalPlayer) end) end else -- Match End Watchdog: If PlayAgain timer freezes for > 45s on a lagging server, force return to Lobby so account never hangs! if stuckEndSec > 45 then setStatus("⚠️ PlayAgain Server Timeout (>45s) -> Tự động về Lobby để vào trận mới!") callRemote("OnLeave") task.wait(1.0) callRemote("TeleportBack") if stuckEndSec > 60 then pcall(function() TeleportService:Teleport(PLACE_LOBBY, LocalPlayer) end) end elseif not isUserInJsonAttr("PlayAgainVoters") or (stuckEndSec > 20 and math.floor(stuckEndSec) % 5 == 0) then if Cfg.AutoStarterCameraman then setStatus(string.format("🔄 [S%d %s %s | $%d] %s -> Voting PlayAgain...", tonumber(bannerEval.Stage) or 1, tostring(bannerEval.ActiveMacro or Cfg.SelectedMacro), tostring(bannerEval.ActiveMode or Cfg.VoteMode), lobbyMoney, tostring(bannerEval.Reason or ""))) else setStatus("Match Ended -> Voting PlayAgain...") end callRemote("PlayAgain") else local timer = workspace:GetAttribute("PlayAgainTimer") or "..." setStatus(string.format("Match Ended -> PlayAgain Voted (%ss | Money: $%d | %s)", tostring(timer), lobbyMoney, bannerEval.Reason)) end end else if isFirstEndTick and Cfg.WebhookOnMatchEnd then task.spawn(sendMatchEndWebhook, { IsWin = isWinThisMatch, Wave = wave, MaxWaves = maxWaves, GameState = gameState, Message = msg, ShouldReturnLobby = false, NextAction = "⏸️ Waiting for user (AutoMatchEnd Off)", }) end setStatus("Match Ended (Waiting for user)") end return else State.MatchEndClock = nil end if isVoting then State.MatchStartClock = os.clock() State.SpeedSetThisMatch = false State.SpeedAttemptCount = 0 if State.MatchEndHandled or (getMyPlacedTowerCount() == 0 and MacroState.CurrentStep > 2) then State.MatchEndHandled = false MacroState.AuthoritativeTowers = {} MacroState.AbilityCooldowns = {} resetMacroPlaybackMappings() end elseif wave <= 1 and (State.LastObservedWave or 0) > 1 then State.SpeedSetThisMatch = false State.SpeedAttemptCount = 0 end State.LastObservedWave = wave if MacroState.IsRecording then return end if isVoting and Cfg.AutoVote then local targetMode = Cfg.VoteMode if Cfg.AutoStarterCameraman and not MacroState.ManualMacroLocked then targetMode = State.GetActiveStepMode(getReplicaData()) elseif targetMode == "Macro" then targetMode = MacroState.ModeName or "Easy" for _, s in ipairs(MacroState.Steps) do if s.Action == "ModeVote" and s.Mode then targetMode = s.Mode break end end end if not isUserInJsonAttr(tostring(targetMode) .. "Voters") then if (os.clock() - (State.LastModeVoteClock or 0)) > 1.8 then State.LastModeVoteClock = os.clock() setStatus("Voting Mode: " .. tostring(targetMode)) callRemote("ModeVote", targetMode) else setStatus("Voting Mode: " .. tostring(targetMode) .. " (Waiting Server Confirm)") end else setStatus("Voted Mode: " .. tostring(targetMode) .. " (Waiting for match start)") end end -- Auto Speed: Mở 2x Speed (nếu có Gamepass) hoặc 1.5x Speed (Group Aligators Games)! if Cfg.AutoSpeed2x and not isVoting and wave >= 1 then local repData = getReplicaData() local has2x = repData and repData.ActivatedGamepasses and repData.ActivatedGamepasses["X2 Speed"] == true local targetSpeed = has2x and 2 or 1.5 local function getSpeedButtonShadow(spdName) local sh = nil pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local gs = pg and pg:FindFirstChild("GameUI") and pg.GameUI:FindFirstChild("GameSpeeds") local f = gs and gs:FindFirstChild(tostring(spdName)) local tf = f and f:FindFirstChild("TroopFrame") local bg = tf and tf:FindFirstChild("Background") sh = bg and bg:FindFirstChild("UIShadow") end) return sh end local function syncSpeedButtonHighlight(activeSpd) pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local gs = pg and pg:FindFirstChild("GameUI") and pg.GameUI:FindFirstChild("GameSpeeds") if gs then local targetName = tostring(activeSpd) for _, f in ipairs(gs:GetChildren()) do if f:IsA("Frame") and f:FindFirstChild("TroopFrame") then local sh = f.TroopFrame:FindFirstChild("Background") and f.TroopFrame.Background:FindFirstChild("UIShadow") if sh then sh.Color = (f.Name == targetName) and Color3.fromRGB(8, 138, 12) or Color3.fromRGB(0, 0, 0) end end end end end) end -- Nếu nút 1x trên UI vẫn đang sáng màu xanh (G > 0.3) hoặc nút targetSpeed chưa sáng xanh -> Cần gọi ChangeSpeed! local sh1 = getSpeedButtonShadow("1") local shTarget = getSpeedButtonShadow(targetSpeed) local uiStillOn1x = (sh1 ~= nil and sh1.Color.G > 0.3) or (shTarget ~= nil and shTarget.Color.G < 0.3) if uiStillOn1x and State.SpeedSetThisMatch then State.SpeedSetThisMatch = false end if State.SpeedSetThisMatch then syncSpeedButtonHighlight(State.AppliedSpeedValue or targetSpeed) elseif (os.clock() - (State.LastSpeedAttempt or 0) > 2.0) and (State.SpeedAttemptCount or 0) < 12 then State.LastSpeedAttempt = os.clock() State.SpeedAttemptCount = (State.SpeedAttemptCount or 0) + 1 if Cfg.AutoJoinGroup and not has2x and not State.IsInGameGroup then doAutoJoinGameGroup(false) end local okSpd, resSpd = callRemote("ChangeSpeed", targetSpeed) if (not okSpd or resSpd ~= true) and targetSpeed == 2 then targetSpeed = 1.5 okSpd, resSpd = callRemote("ChangeSpeed", 1.5) end if okSpd and resSpd == true then State.SpeedSetThisMatch = true State.AppliedSpeedValue = targetSpeed syncSpeedButtonHighlight(targetSpeed) setStatus(string.format("⚡ Đã bật tốc độ x%s Speed!", tostring(targetSpeed))) end end end if Cfg.AutoSkipWave and not isVoting then syncAutoSkipUI(true) tryAutoSkipWaveNow(false) end if Cfg.AutoAbility and not isVoting then local myTowers = getLivePlacedTowers() local anyStunned = false for _, t in ipairs(myTowers) do if t.Stunned then anyStunned = true break end end if anyStunned then for _, t in ipairs(myTowers) do local st = getTowerAnalysis(t.Name) if st and st.IsMedic and not t.Stunned then local pos = t.Pos or Vector3.zero callRemote("TowerAbility", { Id = tostring(t.Id), Args = { TargetPosition = pos, TargetCFrame = CFrame.new(pos) }, }) break end end end end if Cfg.PlayMacro then -- Burst Execution: When multiple consecutive steps are ready (e.g. Late-Game Boss Micro: -- selling front defense towers and rapidly packing + upgrading new towers in 1 spot at the back), -- execute up to 6 consecutive advancing steps without waiting for the 0.25s outer loop! for _ = 1, 6 do local prevStep = MacroState.CurrentStep stepMacroPlayback() if MacroState.CurrentStep == prevStep or MacroState.CurrentStep > #MacroState.Steps then break end end return end setStatus(string.format("Wave %d/%d | Cash: $%d | Macro Paused (Ready)", wave, maxWaves, getMyMatchCash())) end local KNOWN_CODES = { "UTCM", "Battlepass", "LStriderSpeaker", "Halloween", "RELEASE", "UPDATE1", "UPDATE2", "UPDATE3", "UPDATE4", "TRADES", "Engineer", "UpgradedTitanSpeakerman", "NewMaps", } genv.__CTD_TRIED_CODES = genv.__CTD_TRIED_CODES or {} State.CodesFile = "CTD_Codes.json" State.TriedCodesSession = genv.__CTD_TRIED_CODES State.CodesLastWebSync = 0 State.CodeStopwords = { ["shop"] = true, ["redeem"] = true, ["scroll"] = true, ["problox"] = true, ["roblox"] = true, ["codes"] = true, ["code"] = true, ["launch"] = true, ["click"] = true, ["enter"] = true, ["type"] = true, ["paste"] = true, ["active"] = true, ["expired"] = true, ["working"] = true, ["note"] = true, ["here"] = true, ["free"] = true, ["coins"] = true, ["gems"] = true, ["candy"] = true, ["update"] = true, ["cameraman"] = true, ["defense"] = true, } local function insertKnownCode(rawCode, toFront) local clean = tostring(rawCode or ""):gsub(" ", ""):match("^%s*([%w_%-]+)%s*$") if not clean or #clean < 3 or #clean > 28 then return false end local low = clean:lower() if State.CodeStopwords[low] or tonumber(clean) ~= nil then return false end for _, existing in ipairs(KNOWN_CODES) do if tostring(existing):lower() == low then return false end end if toFront then table.insert(KNOWN_CODES, 1, clean) else table.insert(KNOWN_CODES, clean) end State.TriedCodesSession[low] = nil return true end local function saveCustomCodesToDisk() pcall(function() if writefile then writefile(State.CodesFile, HttpService:JSONEncode({ UpdatedAt = State.CodesLastWebSync, Codes = KNOWN_CODES, })) end end) end local function loadCustomCodesFromDisk() pcall(function() if isfile and readfile and isfile(State.CodesFile) then local decoded = HttpService:JSONDecode(readfile(State.CodesFile)) if type(decoded) == "table" then local list = type(decoded.Codes) == "table" and decoded.Codes or decoded if tonumber(decoded.UpdatedAt) then State.CodesLastWebSync = tonumber(decoded.UpdatedAt) end for i = #list, 1, -1 do insertKnownCode(list[i], true) end end end end) end loadCustomCodesFromDisk() -- Automatically scrape & update KNOWN_CODES from: -- 1. In-game ReplicatedStorage.ModuleLoader.Shared.Updates (e.g. "Codes (RELEASE)", "Update 5" -> "UPDATE5") -- 2. Live Code Databases via executor HTTP (RobloxDen, Eurogamer, RockPaperShotgun, Destructoid) local function autoUpdateKnownCodes(force) if not force then loadCustomCodesFromDisk() if os.time() - State.CodesLastWebSync < 3600 then return 0, KNOWN_CODES end end State.CodesLastWebSync = os.time() local addedCount = 0 -- 1. Scan In-Game Updates Module (only if already loaded in shared.Modules; never call require on game ModuleScripts!) pcall(function() local updMod = Modules and Modules.Updates if type(updMod) == "table" then for _, entry in ipairs(updMod) do if type(entry) == "table" then local title = tostring(entry.Title or "") local updNum = title:match("[Uu]pdate%s*(%d+)") if updNum then if insertKnownCode("UPDATE" .. updNum, true) then addedCount = addedCount + 1 end end if type(entry.Logs) == "table" then for _, logLine in ipairs(entry.Logs) do local s = tostring(logLine or "") for codeInParens in s:gmatch("[Cc]odes?%s*%(([^%)]+)%)") do for token in codeInParens:gmatch("([%w_%-]+)") do if insertKnownCode(token, true) then addedCount = addedCount + 1 end end end end end end end end end) -- 2. Scrape Live Web Code Trackers via Timeout-Safe Executor HTTP local function fetchUrlBody(url) local r = httpRequestSafe({ Url = url, Method = "GET", Headers = { ["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", }, }, 7.0) if r and (r.StatusCode == 200 or r.StatusCode == nil) and type(r.Body) == "string" then return r.Body end return nil end local webSources = { "https://robloxden.com/game-codes/cameraman-tower-defense", "https://www.eurogamer.net/cameraman-tower-defense-codes", "https://www.rockpapershotgun.com/cameraman-tower-defense-codes", "https://www.destructoid.com/cameraman-tower-defense-codes/", } for _, url in ipairs(webSources) do local body = fetchUrlBody(url) if type(body) == "string" and #body > 500 then -- Pattern A: RobloxDen machine-readable data-search-terms="["CODE"]" for code in body:gmatch('data%-search="game%-codes"%s+data%-search%-terms="%["([^&"]+)"%]"') do if insertKnownCode(code, true) then addedCount = addedCount + 1 end end -- Pattern B: Standard
  • CODE used by Eurogamer, RPS, Destructoid for code in body:gmatch("
  • %s*([^<]+)") do if insertKnownCode(code, true) then addedCount = addedCount + 1 end end end end saveCustomCodesToDisk() if addedCount > 0 then setStatus(string.format("🌐 Auto-Updated KNOWN_CODES: +%d code mới (Tổng: %d codes)!", addedCount, #KNOWN_CODES)) elseif force then setStatus(string.format("✅ KNOWN_CODES đã mới nhất (Tổng: %d codes: %s, ...)", #KNOWN_CODES, table.concat({ KNOWN_CODES[1] or "", KNOWN_CODES[2] or "", KNOWN_CODES[3] or "" }, ", "))) end return addedCount, KNOWN_CODES end -- Run background auto-update ONLY in Lobby (with random jitter so 50 accounts don't scrape simultaneously) if CURRENT_PLACE == PLACE_LOBBY then task.spawn(function() task.wait(1.5 + math.random() * 3.5) if isAlive() then autoUpdateKnownCodes(false) end end) end local function isCodeAlreadyRedeemed(data, code) if not data or type(data.RedeemedCodes) ~= "table" then return false end local target = tostring(code):lower() if data.RedeemedCodes[code] == true or data.RedeemedCodes[target] == true then return true end for k, v in pairs(data.RedeemedCodes) do if tostring(k):lower() == target and v == true then return true end if tostring(v):lower() == target then return true end end return false end local function redeemSingleCode(code) local clean = tostring(code or ""):match("^%s*(.-)%s*$") if clean == "" then return false end State.TriedCodesSession[clean:lower()] = true setStatus("🎁 Redeeming Code: " .. clean) local ok = callRemote("RedeemCode", clean) return ok end local function addAndRedeemCustomInput(rawText) local count = 0 for token in tostring(rawText or ""):gmatch("[^,%s;]+") do local clean = token:match("^%s*(.-)%s*$") if clean ~= "" then insertKnownCode(clean, true) State.TriedCodesSession[clean:lower()] = nil redeemSingleCode(clean) count = count + 1 end end if count > 0 then saveCustomCodesToDisk() end return count end local function doAutoRedeemCodes(data, forceRetry) if not forceRetry and not Cfg.AutoRedeemCodes then return end local winsNow = (data and tonumber(data.Wins)) or 0 local winsTier = (winsNow >= 3) and "3+" or "0" if genv.__CTD_TRIED_CODES_WINS_TIER ~= winsTier then genv.__CTD_TRIED_CODES_WINS_TIER = winsTier table.clear(State.TriedCodesSession) end if forceRetry then autoUpdateKnownCodes(true) table.clear(State.TriedCodesSession) end -- Note: RemoteEvent.RedeemCode is mounted on Lobby servers (and requires 3 Wins for some codes) local rem = getRawRemoteInstance("RedeemCode") if not rem and CURRENT_PLACE ~= PLACE_LOBBY then if forceRetry then setStatus(string.format("✅ Đã cập nhật %d KNOWN_CODES! (Vào Lobby để Redeem lên Server)", #KNOWN_CODES)) end return end local redeemedAny = false for _, code in ipairs(KNOWN_CODES) do local key = tostring(code):lower() if not State.TriedCodesSession[key] and not isCodeAlreadyRedeemed(data, code) then State.TriedCodesSession[key] = true setStatus("🎁 Auto-Redeeming Code: " .. tostring(code)) callRemote("RedeemCode", tostring(code)) redeemedAny = true data = getReplicaData() or data end end if forceRetry and not redeemedAny then setStatus(string.format("✅ Đã thử toàn bộ %d KNOWN_CODES!", #KNOWN_CODES)) end end State.LastQuestSync = 0 State.CachedServerQuests = {} State.CachedPassQuests = nil State.CachedPassQuestsClock = 0 State.QuestClaimAttemptsSession = {} State.TierClaimFailedSession = {} State.QuestClaimRetryAfter = {} State.TierClaimRetryAfter = {} State.BoundLobbyQuestRemotes = {} local function isQuestEntryCompleted(q) if type(q) ~= "table" or not q.Id then return false end if q.RewardClaimed == true or q.Claimed == true or q.Collected == true then return false end if q.Completed == true or q.State == "Completed" then return true end local curProg = tonumber(q.CurrentProgress) local reqProg = tonumber(q.Progress) or tonumber(q.Goal) or tonumber(q.Target) if not reqProg and Modules and Modules.Config and Modules.Config.Battlepass and type(Modules.Config.Battlepass.Quests) == "table" then for stageName, stageTbl in pairs(Modules.Config.Battlepass.Quests) do if type(stageTbl) == "table" and type(stageTbl[q.Id]) == "table" then reqProg = tonumber(stageTbl[q.Id].Progress) if not q.Stage then q.Stage = stageName end break end end end if curProg and reqProg and reqProg > 0 and curProg >= reqProg then return true end return false end local function doAutoClaimQuestsAndPass(data, forceManual) ensureGameModules() data = data or getReplicaData() if not data then return 0 end -- Lazy, self-healing listeners for SendQuestData, QuestCompleted, and SendPassQuestData pcall(function() if not State.BoundLobbyQuestRemotes.SendQuestData then local sendQuestRem = getRawRemoteInstance("SendQuestData") if sendQuestRem and sendQuestRem:IsA("RemoteEvent") then State.BoundLobbyQuestRemotes.SendQuestData = true trackConn(sendQuestRem.OnClientEvent:Connect(function(qData) if type(qData) == "table" then for k, v in pairs(qData) do State.CachedServerQuests[k] = v end end end)) end end if not State.BoundLobbyQuestRemotes.QuestCompleted then local questDoneRem = getRawRemoteInstance("QuestCompleted") if questDoneRem and questDoneRem:IsA("RemoteEvent") then State.BoundLobbyQuestRemotes.QuestCompleted = true trackConn(questDoneRem.OnClientEvent:Connect(function(qItem) if type(qItem) == "table" and qItem.Id then local cat = tostring(qItem.QuestType or "Daily") State.CachedServerQuests[cat] = State.CachedServerQuests[cat] or {} State.CachedServerQuests[cat][qItem.Id] = qItem end end)) end end if not State.BoundLobbyQuestRemotes.SendPassQuestData then local sendPassRem = getRawRemoteInstance("SendPassQuestData") if sendPassRem and sendPassRem:IsA("RemoteEvent") then State.BoundLobbyQuestRemotes.SendPassQuestData = true trackConn(sendPassRem.OnClientEvent:Connect(function(pData) if type(pData) == "table" then State.CachedPassQuests = pData State.CachedPassQuestsClock = os.clock() end end)) end end end) if CURRENT_PLACE ~= PLACE_LOBBY and not getRawRemoteInstance("CollectQuest") and not getRawRemoteInstance("CollectPassQuest") then if forceManual then setStatus("⚠️ Hãy về Lobby để nhận thưởng Quests & Battlepass!") end return 0 end if forceManual or (os.clock() - State.LastQuestSync > 15) then State.LastQuestSync = os.clock() if forceManual then table.clear(State.QuestClaimAttemptsSession) table.clear(State.TierClaimFailedSession) table.clear(State.QuestClaimRetryAfter) table.clear(State.TierClaimRetryAfter) end callRemote("RequestQuestData") task.wait(0.55) data = getReplicaData() or data end local totalClaimed = 0 -- 1. Auto-Claim Daily / Weekly Quests (data.ActiveQuests[category].Quests + State.CachedServerQuests) if Cfg.AutoClaimQuests or forceManual then local winsNow = tonumber(data.Wins) or 0 -- Note: In UI.HUD.luau, the Quests tab unlocks at 20 Wins if winsNow >= 20 or forceManual then local sources = {} if next(State.CachedServerQuests) ~= nil then table.insert(sources, State.CachedServerQuests) end if type(data.ActiveQuests) == "table" then table.insert(sources, data.ActiveQuests) end if type(data.Quests) == "table" then table.insert(sources, data.Quests) end for _, src in ipairs(sources) do for category, catTable in pairs(src) do local questList = (type(catTable) == "table" and type(catTable.Quests) == "table" and catTable.Quests) or catTable if type(questList) == "table" then for _, q in pairs(questList) do if isQuestEntryCompleted(q) then local qId = tostring(q.Id) local qCat = tostring(q.QuestType or category or "Daily") local attemptKey = qCat .. ":" .. qId local retryAt = State.QuestClaimRetryAfter[attemptKey] or 0 if forceManual or (os.clock() >= retryAt and not State.QuestClaimAttemptsSession[attemptKey]) then setStatus(string.format("🎁 Claiming %s Quest: %s (+%s Gems)", qCat, tostring(q.Name or qId), tostring(q.Gems or q.Reward or 5))) local ok, res = callRemote("CollectQuest", qId, qCat) if ok and res == true then q.RewardClaimed = true State.QuestClaimAttemptsSession[attemptKey] = true totalClaimed = totalClaimed + 1 task.wait(0.35) else State.QuestClaimRetryAfter[attemptKey] = os.clock() + 12 end end end end end end end end end -- Helper: Move character into workspace.Zones.Event (Halloween Battlepass Zone) & open Battlepass UI before claiming local movedToHalloweenZone = false local anyPassQuestRejected = false local function ensureInHalloweenEventZone() if movedToHalloweenZone then return end movedToHalloweenZone = true if CURRENT_PLACE ~= PLACE_LOBBY then return end pcall(function() local zonesFolder = workspace:FindFirstChild("Zones") local eventZone = zonesFolder and zonesFolder:FindFirstChild("Event") local targetPos = (eventZone and eventZone:IsA("BasePart")) and Vector3.new(eventZone.Position.X, eventZone.Position.Y - 5.2, eventZone.Position.Z) or Vector3.new(580.77, 306.5, 495.83) local plat = workspace:FindFirstChild("CTD_SafetyPlatform") if plat and plat:IsA("BasePart") then plat.Position = Vector3.new(targetPos.X, targetPos.Y - 3.2, targetPos.Z) end local lobbyFloor = workspace:FindFirstChild("CTD_LobbySafetyFloor") if lobbyFloor and lobbyFloor:IsA("BasePart") and lobbyFloor.Position.Y > (targetPos.Y - 3.2) then lobbyFloor.Position = Vector3.new(lobbyFloor.Position.X, targetPos.Y - 4.5, lobbyFloor.Position.Z) end local char = LocalPlayer.Character local hrp = char and char:FindFirstChild("HumanoidRootPart") local hum = char and char:FindFirstChildOfClass("Humanoid") if hrp then if (hrp.Position - targetPos).Magnitude > 7 then setStatus("🎃 Đang di chuyển tới khu vực Halloween Event để nhận thưởng...") hrp.AssemblyLinearVelocity = Vector3.zero hrp.CFrame = CFrame.new(targetPos) if hum then hum:MoveTo(targetPos + Vector3.new(0.5, 0, 0.5)) end end if eventZone and eventZone:IsA("BasePart") and type(firetouchinterest) == "function" then pcall(firetouchinterest, hrp, eventZone, 0) pcall(firetouchinterest, hrp, eventZone, 1) end end if Modules and Modules.TweenButtons and type(Modules.TweenButtons.OnButtonClick) == "function" then pcall(Modules.TweenButtons.OnButtonClick, "Battlepass", true) end if Modules and Modules.UI and Modules.UI.Battlepass and type(Modules.UI.Battlepass.Update) == "function" then pcall(Modules.UI.Battlepass.Update) end end) task.wait(0.45) end -- 2. Auto-Claim Halloween Event Missions (PassQuests - 5 Stages: Easy -> Medium -> Hard -> Insane -> Nightmare) if Cfg.AutoClaimQuests or Cfg.AutoClaimBattlepass or forceManual then local bpQuestsCfg = Modules and Modules.Config and Modules.Config.Battlepass and Modules.Config.Battlepass.Quests local missionFrameMap = { Destroy = "KillToilets", Beat = "WinMatches", Collect = "OpenUltraCrate", Play = "PlayForHours", Open = "OpenDrillCrate", } local function markPassQuestClaimedEverywhere(claimedId) local function markInObj(obj) if type(obj) ~= "table" then return end local list = (type(obj.Quests) == "table" and obj.Quests) or obj if type(list) == "table" then for _, item in pairs(list) do if type(item) == "table" and tostring(item.Id) == tostring(claimedId) then item.RewardClaimed = true end end end end markInObj(State.CachedPassQuests) local curData = getReplicaData() or data if curData and curData.Battlepass then markInObj(curData.Battlepass.PassQuests) end end for _stagePass = 1, 6 do data = getReplicaData() or data local bp = data and data.Battlepass local candidateMap = {} local candidateOrder = {} local function addPassCandidatesFromObj(obj) if type(obj) ~= "table" then return end local list = (type(obj.Quests) == "table" and obj.Quests) or obj if type(list) ~= "table" then return end for _, rawQ in pairs(list) do if type(rawQ) == "table" and rawQ.Id then local qId = tostring(rawQ.Id) local existing = candidateMap[qId] if not existing then local copyQ = {} for k, v in pairs(rawQ) do copyQ[k] = v end if type(bpQuestsCfg) == "table" then for stg, stgTbl in pairs(bpQuestsCfg) do local cfgItem = type(stgTbl) == "table" and stgTbl[qId] if type(cfgItem) == "table" then copyQ.Stage = copyQ.Stage or stg copyQ.Name = copyQ.Name or cfgItem.Title copyQ.Progress = copyQ.Progress or cfgItem.Progress local rw = type(cfgItem.Rewards) == "table" and cfgItem.Rewards or {} copyQ.Exp = copyQ.Exp or rw.EXP or 250 copyQ.Currency = copyQ.Currency or rw.Currency or 100 break end end end candidateMap[qId] = copyQ table.insert(candidateOrder, copyQ) else if tonumber(rawQ.CurrentProgress or 0) > tonumber(existing.CurrentProgress or 0) then existing.CurrentProgress = rawQ.CurrentProgress end if rawQ.Completed == true or rawQ.State == "Completed" then existing.Completed = true existing.State = "Completed" end if rawQ.RewardClaimed == true then existing.RewardClaimed = true end end end end end -- Check BOTH State.CachedPassQuests (from SendPassQuestData) AND ReplicaData.Battlepass.PassQuests if State.CachedPassQuests then addPassCandidatesFromObj(State.CachedPassQuests) end if bp and bp.PassQuests then addPassCandidatesFromObj(bp.PassQuests) end if bp and Modules and Modules.Progression and type(Modules.Progression.Battlepass) == "function" then local okProg, enrichedBp = pcall(function() return Modules.Progression.Battlepass(bp, Modules.Config and Modules.Config.Battlepass) end) if okProg and type(enrichedBp) == "table" and enrichedBp.PassQuests then addPassCandidatesFromObj(enrichedBp.PassQuests) end end -- Also inspect live BattlepassFrame.Missions UI cards (check ClaimButton.Visible vs ClaimedText.Visible) pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local missionsGui = pg and pg:FindFirstChild("Main") and pg.Main:FindFirstChild("Main") and pg.Main.Main:FindFirstChild("Battlepass") and pg.Main.Main.Battlepass:FindFirstChild("BattlepassFrame") and pg.Main.Main.Battlepass.BattlepassFrame:FindFirstChild("Missions") if missionsGui then for prefix, frameName in pairs(missionFrameMap) do local card = missionsGui:FindFirstChild(frameName, true) local claimFrame = card and card:FindFirstChild("Claim") local claimBtn = claimFrame and claimFrame:FindFirstChild("ClaimButton") local claimedText = claimFrame and claimFrame:FindFirstChild("ClaimedText") local isVisAlreadyClaimed = (claimedText and claimedText.Visible == true) and not (claimBtn and claimBtn.Visible == true) local isVisClaimable = (claimBtn and claimBtn.Visible == true) and not (claimedText and claimedText.Visible == true) for _, cQ in ipairs(candidateOrder) do if tostring(cQ.Id):match("^" .. prefix) then if isVisAlreadyClaimed then cQ.RewardClaimed = true elseif isVisClaimable and not cQ.RewardClaimed then cQ.Completed = true cQ.State = "Completed" end end end end end end) local function checkPassQuestVerifiedClaimed(qId, expBefore, candyBefore) local refreshed = getReplicaData() local rBp = refreshed and refreshed.Battlepass if rBp then if expBefore and tonumber(rBp.EXP or 0) > expBefore then return true end if candyBefore and tonumber(refreshed.Candy or 0) > candyBefore then return true end local rqList = rBp.PassQuests and rBp.PassQuests.Quests if type(rqList) == "table" then local foundSameId = false for _, rq in pairs(rqList) do if type(rq) == "table" and tostring(rq.Id) == qId then foundSameId = true if rq.RewardClaimed == true then return true end end end if not foundSameId then -- Quest ID advanced to the next stage in PassQuests.Quests return true end end end local uiClaimed = false pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local missionsGui = pg and pg:FindFirstChild("Main") and pg.Main:FindFirstChild("Main") and pg.Main.Main:FindFirstChild("Battlepass") and pg.Main.Main.Battlepass:FindFirstChild("BattlepassFrame") and pg.Main.Main.Battlepass.BattlepassFrame:FindFirstChild("Missions") if missionsGui then for prefix, frameName in pairs(missionFrameMap) do if qId:match("^" .. prefix) then local card = missionsGui:FindFirstChild(frameName, true) local claimFrame = card and card:FindFirstChild("Claim") local claimBtn = claimFrame and claimFrame:FindFirstChild("ClaimButton") local claimedText = claimFrame and claimFrame:FindFirstChild("ClaimedText") if claimedText and claimedText.Visible == true and (not claimBtn or claimBtn.Visible == false) then uiClaimed = true end break end end end end) return uiClaimed end local claimedInPass = 0 for _, q in ipairs(candidateOrder) do if isQuestEntryCompleted(q) then local qId = tostring(q.Id) local stageTag = tostring(q.Stage or "Pass") local attemptKey = "Pass:" .. stageTag .. ":" .. qId local retryAt = State.QuestClaimRetryAfter[attemptKey] or 0 if forceManual or (os.clock() >= retryAt and not State.QuestClaimAttemptsSession[attemptKey]) then ensureInHalloweenEventZone() local curSnap = getReplicaData() or data local expBefore = tonumber(curSnap and curSnap.Battlepass and curSnap.Battlepass.EXP) or 0 local candyBefore = tonumber(curSnap and curSnap.Candy) or 0 setStatus(string.format("🎃 Claiming Halloween Mission [%s]: %s (+%s EXP, +%s Candy)", stageTag, tostring(q.Name or qId), tostring(q.Exp or 250), tostring(q.Currency or 100))) local ok, res = callRemote("CollectPassQuest", qId) local verified = (ok and res == true) or checkPassQuestVerifiedClaimed(qId, expBefore, candyBefore) if not verified then -- Wait 0.60s so RemoteSecurity's 0.50s Delay on CollectPassQuest expires before UI button fallback task.wait(0.60) -- Fallback: switch to Missions tab & fire native UI ClaimButton.Activated if present pcall(function() local pg = LocalPlayer:FindFirstChildOfClass("PlayerGui") local bpFrame = pg and pg:FindFirstChild("Main") and pg.Main:FindFirstChild("Main") and pg.Main.Main:FindFirstChild("Battlepass") and pg.Main.Main.Battlepass:FindFirstChild("BattlepassFrame") local missionsGui = bpFrame and bpFrame:FindFirstChild("Missions") if bpFrame and type(getconnections) == "function" then local mTabBtn = bpFrame:FindFirstChild("Buttons") and bpFrame.Buttons:FindFirstChild("Missions") if mTabBtn then for _, conn in ipairs(getconnections(mTabBtn.Activated)) do if conn.Function then pcall(conn.Function) end end end end if missionsGui and type(getconnections) == "function" then for prefix, frameName in pairs(missionFrameMap) do if qId:match("^" .. prefix) then local card = missionsGui:FindFirstChild(frameName, true) local claimBtn = card and card:FindFirstChild("Claim") and card.Claim:FindFirstChild("ClaimButton") if claimBtn and claimBtn.Visible then for _, conn in ipairs(getconnections(claimBtn.Activated)) do if conn.Function then task.spawn(conn.Function) end end State.LastRemoteTime["CollectPassQuest"] = os.clock() task.wait(0.60) end break end end end end) verified = checkPassQuestVerifiedClaimed(qId, expBefore, candyBefore) end if verified then q.RewardClaimed = true State.QuestClaimAttemptsSession[attemptKey] = true markPassQuestClaimedEverywhere(qId) claimedInPass = claimedInPass + 1 totalClaimed = totalClaimed + 1 task.wait(0.55) else anyPassQuestRejected = true State.QuestClaimAttemptsSession[attemptKey] = true State.QuestClaimRetryAfter[attemptKey] = os.clock() + 120 task.wait(0.35) end end end end if claimedInPass == 0 then break end callRemote("RequestQuestData") task.wait(0.55) end end -- 3. Auto-Claim Halloween Event Pass Tiers 1..31 (data.Battlepass + Modules.Config.Battlepass.Tiers) if (Cfg.AutoClaimBattlepass or Cfg.AutoClaimQuests or forceManual) and Modules and Modules.Config and Modules.Config.Battlepass then local bpCfg = Modules.Config.Battlepass local rawTiers = bpCfg.Tiers if type(rawTiers) == "table" then local sortedTiers = {} for _, tInfo in pairs(rawTiers) do if type(tInfo) == "table" then table.insert(sortedTiers, tInfo) end end table.sort(sortedTiers, function(a, b) return (tonumber(a.TierNumber) or 0) < (tonumber(b.TierNumber) or 0) end) for idx, tierInfo in ipairs(sortedTiers) do data = getReplicaData() or data local bp = data and data.Battlepass if type(bp) ~= "table" then break end local tierNum = tonumber(tierInfo.TierNumber) or idx local reqExp = tonumber(tierInfo.TierEXP) or tonumber(tierInfo.EXP) or tonumber(tierInfo.RequiredEXP) or 999999 local myExp = tonumber(bp.EXP) or tonumber(data.BattlepassEXP) or 0 local curTier = tonumber(bp.CurrentTier) or 0 local nextTier = tonumber(bp.NextTier) or (curTier + 1) local claimedTiers = type(bp.ClaimedTiers) == "table" and bp.ClaimedTiers or {} local claimedDefault = type(claimedTiers.Default) == "table" and claimedTiers.Default or claimedTiers local claimedPremium = type(claimedTiers.Premium) == "table" and claimedTiers.Premium or (bp.ClaimedPremiumTiers or {}) local alreadyFree = (claimedDefault[tierNum] == true or claimedDefault[tostring(tierNum)] == true) local freeKey = "F_" .. tostring(tierNum) local freeRetryAt = State.TierClaimRetryAfter[freeKey] or 0 local canClaimFreeNow = not alreadyFree and ((myExp >= reqExp and (tierNum == nextTier or tierNum <= curTier + 1)) or tierNum <= curTier) and (forceManual or (os.clock() >= freeRetryAt and not State.TierClaimFailedSession[freeKey])) if canClaimFreeNow then ensureInHalloweenEventZone() local rewardInfo = tierInfo.Default local rewardDesc = (type(rewardInfo) == "table" and string.format("%s %s", tostring(rewardInfo.Award or ""), tostring(rewardInfo.AwardType or ""))) or "" setStatus(string.format("🏆 Claiming Event Pass Tier %d [%s] (EXP: %d/%d)...", tierNum, rewardDesc, myExp, reqExp)) local ok, res = callRemote("ClaimTier", tierNum, false) if ok and res == true then State.TierClaimFailedSession[freeKey] = true totalClaimed = totalClaimed + 1 task.wait(0.55) else State.TierClaimRetryAfter[freeKey] = os.clock() + 12 end end data = getReplicaData() or data bp = data and data.Battlepass if bp and bp.Premium == true then curTier = tonumber(bp.CurrentTier) or curTier claimedTiers = type(bp.ClaimedTiers) == "table" and bp.ClaimedTiers or {} claimedPremium = type(claimedTiers.Premium) == "table" and claimedTiers.Premium or (bp.ClaimedPremiumTiers or {}) local alreadyPrem = (claimedPremium[tierNum] == true or claimedPremium[tostring(tierNum)] == true) local premKey = "P_" .. tostring(tierNum) local premRetryAt = State.TierClaimRetryAfter[premKey] or 0 if not alreadyPrem and (myExp >= reqExp or tierNum <= curTier) and (forceManual or (os.clock() >= premRetryAt and not State.TierClaimFailedSession[premKey])) then ensureInHalloweenEventZone() setStatus(string.format("💎 Claiming Premium Event Pass Tier %d...", tierNum)) local ok, res = callRemote("ClaimTier", tierNum, true) if ok and res == true then State.TierClaimFailedSession[premKey] = true totalClaimed = totalClaimed + 1 task.wait(0.55) else State.TierClaimRetryAfter[premKey] = os.clock() + 12 end end end end end end -- Refresh Battlepass UI if open so claimed buttons visually switch to "Claimed" immediately if totalClaimed > 0 then pcall(function() if Modules and Modules.UI and Modules.UI.Battlepass and type(Modules.UI.Battlepass.Update) == "function" then Modules.UI.Battlepass.Update() end end) end if anyPassQuestRejected and totalClaimed == 0 then local winsCount = tonumber(data and data.Wins) or 0 if winsCount < 20 then setStatus(string.format("⚠️ Đã tới khu Halloween nhưng Server từ chối CollectPassQuest (Wins: %d/20 hoặc Server khóa Event Quest)", winsCount)) else setStatus("⚠️ Đã tới khu Halloween nhưng Server từ chối CollectPassQuest (Server khóa Event Quest)") end elseif forceManual and totalClaimed == 0 then setStatus("✅ Không còn Quest hoặc Event Pass nào chưa nhận!") elseif totalClaimed > 0 then setStatus(string.format("✅ Đã tự động nhận %d phần thưởng Quest & Event Pass!", totalClaimed)) end return totalClaimed end local function doAutoSellDuplicates(data, forceEmergency) if (not Cfg.AutoSellDuplicates and not forceEmergency) or not data or type(data.Towers) ~= "table" then return end local byName = {} for uuid, unit in pairs(data.Towers) do if type(unit) == "table" and type(unit.Name) == "string" then byName[unit.Name] = byName[unit.Name] or {} table.insert(byName[unit.Name], { UUID = tostring(uuid), Equipped = isUnitEquippedVal(unit.Equipped), }) end end local toSell = {} local keepN = forceEmergency and 1 or math.max(1, tonumber(Cfg.KeepCopies) or 2) local protectedExtra = { ["titan cinemaman"] = true, ["glitch cameraman"] = true, } pcall(function() local steps = State.GetResolvedSteps(data) for _, s in ipairs(steps) do for _, eu in ipairs(s.ExtraUnits or {}) do local low = tostring(eu or ""):lower():match("^%s*(.-)%s*$") if low ~= "" then protectedExtra[low] = true end end end end) for towerName, list in pairs(byName) do local st = getTowerAnalysis(towerName) local lowName = tostring(towerName or ""):lower() -- Tuyệt đối bảo vệ toàn bộ Mythic / Exclusive / Godly (RarityRank >= 6) VÀ mọi tướng thuộc diện Săn Thêm Liên Tục (ExtraUnits: Titan Cinemaman, Glitch Cameraman...). -- Với tướng <= Legendary (kể cả Speakerman, Cameraman, Ninja Cameraman): luôn giữ lại bản đang Equipped (và >= keepN bản), -- bán sạch các bản trùng lặp dư thừa để giải phóng kho & hoàn tiền! if not protectedExtra[lowName] and st and st.RarityRank <= 5 and #list > keepN then table.sort(list, function(a, b) if a.Equipped ~= b.Equipped then return a.Equipped end return a.UUID < b.UUID end) for i = keepN + 1, #list do if not list[i].Equipped then table.insert(toSell, list[i].UUID) if #toSell >= 50 then break end end end end if #toSell >= 50 then break end end if #toSell > 0 then setStatus(string.format("Auto-Selling %d duplicate towers...", #toSell)) callRemote("SellPart", toSell) end end local function shouldSummonBasicCrateNow(data) if State.SummonBlockedThisSession then return false, nil end local money = (data and tonumber(data.Money)) or 0 local minRollMoney = math.max(100, tonumber(Cfg.ReturnMoneyThreshold) or 900) if Cfg.AutoStarterCameraman then local eval = evaluateStarterBannerSummon(data) -- Dưới ngưỡng vàng cấu hình trên Website (mặc định 900 vàng) nhất định KHÔNG BAO GIỜ ROLL! if money < minRollMoney or not eval.CanSummon then return false, eval end return true, eval end if Cfg.AutoSummon then local minMoney = math.max(100, tonumber(Cfg.SummonMinMoney) or 900) if money >= minMoney then return true, nil end end return false, nil end local function doAutoSummonAndCrates(data) if not data then return end local money = tonumber(data.Money) or 0 local candy = tonumber(data.Candy) or 0 local minRollMoney = math.max(100, tonumber(Cfg.ReturnMoneyThreshold) or 900) if Cfg.AutoBuyHalloweenCrate and candy >= 500 and not State.CrateBlockedThisSession then local candyBefore = candy local buyCount = (candy >= 5000) and 10 or 1 setStatus(string.format("Buying Exclusive Halloween Crate x%d (Candy: %d)", buyCount, candy)) callRemote("BuyCrate", "Exclusive Halloween Crate", buyCount) task.wait(0.35) local candyAfter = tonumber((getReplicaData() or data).Candy) or candyBefore if candyAfter >= candyBefore then State.CrateFailStreak = (State.CrateFailStreak or 0) + 1 if State.CrateFailStreak >= 3 then State.CrateBlockedThisSession = true end else State.CrateFailStreak = 0 end end if Cfg.AutoOpenCrates and type(data.Items) == "table" then for itemUUID, itemData in pairs(data.Items) do if type(itemData) == "table" then local itemName = tostring(itemData.Name or itemUUID) local amount = tonumber(itemData.Amount) or tonumber(itemData.Count) or 1 if itemName:find("Crate") and amount > 0 then local openAmt = (amount >= 10) and 10 or 1 setStatus(string.format("Opening %s x%d", itemName, openAmt)) callRemote("OpenCrate", tostring(itemUUID), openAmt) end end end end local canSummon, starterEval = shouldSummonBasicCrateNow(data) -- Quy tắc nghiêm ngặt: Chỉ Roll khi Vàng >= 900, dưới 900 vàng nhất định không Roll! if canSummon and money >= minRollMoney then local moneyBefore = money local count = 10 local targetTag = (starterEval and starterEval.TargetUnit) and (" [Săn GĐ" .. tostring(starterEval.HuntPhase or 1) .. ": " .. tostring(starterEval.TargetUnit) .. "]") or "" setStatus(string.format("🎲 Summoning Basic Crate x%d (Money: $%d)%s...", count, money, targetTag)) callRemote("SummonTowers", "Basic Crate", count) local t0 = os.clock() local moneyAfter = moneyBefore while os.clock() - t0 < 0.65 and isAlive() do local repNow = getReplicaData() moneyAfter = (repNow and tonumber(repNow.Money)) or moneyBefore if moneyAfter < moneyBefore then break end task.wait(0.08) end -- Phòng hờ trường hợp Server yêu cầu 1000 vàng cho x10 khi đang ở mức 900..999 vàng: if moneyAfter >= moneyBefore and moneyBefore >= 900 and moneyBefore < 1000 then callRemote("SummonTowers", "Basic Crate", 1) local t1 = os.clock() while os.clock() - t1 < 0.55 and isAlive() do local repNow = getReplicaData() moneyAfter = (repNow and tonumber(repNow.Money)) or moneyBefore if moneyAfter < moneyBefore then break end task.wait(0.08) end end if moneyAfter < moneyBefore then State.SummonFailStreak = 0 else -- Summon did not deduct Money (e.g. Unit Inventory Full!) State.SummonFailStreak = (State.SummonFailStreak or 0) + 1 if State.SummonFailStreak >= 2 then doAutoSellDuplicates(getReplicaData() or data, true) end if State.SummonFailStreak >= 4 then State.SummonBlockedThisSession = true setStatus("⚠️ Kho Tướng Đầy / Không thể Summon thêm -> Tự động vào Map Farm!") end end end end local function doAutoEquipBestTeam(data) if not data or type(data.Towers) ~= "table" then return end local maxSlots = tonumber(data.MaxEquipped) or (Modules.Config and Modules.Config.GameSettings and tonumber(Modules.Config.GameSettings.MaxEquipped)) or 5 local bestByName = {} local currentEquippedUUIDs = {} local currentEquippedCount = 0 for uuid, unit in pairs(data.Towers) do if type(unit) == "table" and type(unit.Name) == "string" then local st = getTowerAnalysis(unit.Name) if st then local eq = isUnitEquippedVal(unit.Equipped) if eq then currentEquippedUUIDs[tostring(uuid)] = true currentEquippedCount = currentEquippedCount + 1 end local existing = bestByName[unit.Name] if not existing or (eq and not existing.Equipped) then bestByName[unit.Name] = { UUID = tostring(uuid), Name = unit.Name, Equipped = eq, Stats = st, } end end end end local uniqueUnits = {} for _, entry in pairs(bestByName) do table.insert(uniqueUnits, entry) end if #uniqueUnits == 0 then return end local desiredUUIDs = {} local desiredList = {} local function pickUnit(entry) if not entry or desiredUUIDs[entry.UUID] or #desiredList >= maxSlots then return false end desiredUUIDs[entry.UUID] = true table.insert(desiredList, entry) return true end for _, step in ipairs(MacroState.Steps) do if step.Action == "TowerAdd" and step.TowerName and bestByName[step.TowerName] then pickUnit(bestByName[step.TowerName]) end end local bestFarm = nil for _, u in ipairs(uniqueUnits) do if u.Stats.IsFarm then if not bestFarm or u.Stats.MaxCashPerWave > bestFarm.Stats.MaxCashPerWave then bestFarm = u end end end if bestFarm and #uniqueUnits > 1 then pickUnit(bestFarm) end local bestEarly = nil for _, u in ipairs(uniqueUnits) do if not u.Stats.IsFarm and not u.Stats.IsMedic and u.Stats.PriceL1 <= 300 then if not bestEarly or u.Stats.CombatScore > bestEarly.Stats.CombatScore then bestEarly = u end end end if bestEarly then pickUnit(bestEarly) end table.sort(uniqueUnits, function(a, b) return a.Stats.CombatScore > b.Stats.CombatScore end) for _, u in ipairs(uniqueUnits) do if not u.Stats.IsFarm then pickUnit(u) end end local same = (currentEquippedCount == #desiredList) if same then for uuid in pairs(desiredUUIDs) do if not currentEquippedUUIDs[uuid] then same = false break end end end if same then return end setStatus("Updating Team Loadout...") callRemote("EquipPart", "UnequipAll") task.wait(0.35) for _, u in ipairs(desiredList) do callRemote("EquipPart", u.UUID) end end local function doAutoJoinElevator() if not Cfg.AutoJoinElevator and not Cfg.AutoStarterCameraman then return end local elevatorsFolder = workspace:FindFirstChild("Elevators") if not elevatorsFolder then return end local char = LocalPlayer.Character local hrp = char and char:FindFirstChild("HumanoidRootPart") if not hrp then return end local desiredMap = (Cfg.AutoStarterCameraman and State.GetActiveStepMap(getReplicaData())) or Cfg.PreferredMap local hostedElev = nil local matchingEmpty = {} local fallbackEmpty = {} for _, elev in ipairs(elevatorsFolder:GetChildren()) do local basePart = elev:FindFirstChild("BasePart") if basePart and basePart:IsA("BasePart") then local mapName = tostring(elev:GetAttribute("Map") or "") local playersIn = tonumber(elev:GetAttribute("Players")) or 0 local hostId = tonumber(elev:GetAttribute("Host")) or 0 if hostId == LocalPlayer.UserId then if playersIn <= 1 and (desiredMap == "Any" or mapName == desiredMap) then hostedElev = elev break else -- Multi-account anti-collision: Another player entered our elevator or map changed -> leave immediately! callRemote("ElevatorLeave") task.wait(0.25) end elseif playersIn == 0 then if desiredMap == "Any" or mapName == desiredMap then table.insert(matchingEmpty, elev) else table.insert(fallbackEmpty, elev) end end end end local targetElev = hostedElev if not targetElev and #matchingEmpty > 0 then -- Spread 50 accounts evenly across available elevators using UserId + random jitter! local pickIdx = ((LocalPlayer.UserId + math.random(0, 99)) % #matchingEmpty) + 1 targetElev = matchingEmpty[pickIdx] elseif not targetElev and #fallbackEmpty > 0 then local pickIdx = ((LocalPlayer.UserId + math.random(0, 99)) % #fallbackEmpty) + 1 targetElev = fallbackEmpty[pickIdx] end if not targetElev then setStatus("Waiting for an empty Solo Elevator (" .. tostring(desiredMap) .. ")...") return end local basePart = targetElev:FindFirstChild("BasePart") local areaPart = targetElev:FindFirstChild("Area") or targetElev:FindFirstChild("Hitbox") or basePart if not basePart or not areaPart then return end local mapName = tostring(targetElev:GetAttribute("Map") or targetElev.Name) local standPos = Vector3.new(areaPart.Position.X, basePart.Position.Y + 3.5, areaPart.Position.Z) -- Move safety platforms to match the elevator floor height so ensureSafetyPlatform never pulls the character out! pcall(function() local plat = workspace:FindFirstChild("CTD_SafetyPlatform") if plat and plat:IsA("BasePart") then plat.Position = Vector3.new(standPos.X, basePart.Position.Y - 1.5, standPos.Z) end local lobbyFloor = workspace:FindFirstChild("CTD_LobbySafetyFloor") if lobbyFloor and lobbyFloor:IsA("BasePart") then lobbyFloor.Size = Vector3.new(2048, 4, 2048) lobbyFloor.Position = Vector3.new(standPos.X, basePart.Position.Y - 1.5, standPos.Z) end end) hrp.AssemblyLinearVelocity = Vector3.zero hrp.AssemblyAngularVelocity = Vector3.zero hrp.CFrame = CFrame.new(standPos) task.wait(0.25) local hostId = tonumber(targetElev:GetAttribute("Host")) or 0 if hostId ~= LocalPlayer.UserId then setStatus(string.format("🚪 Entering %s (%s)...", targetElev.Name, mapName)) callRemote("ElevatorEnter", targetElev.Name) task.wait(0.35) end -- Verify we are the solo host after entering (in case 2 accounts entered in the same 0.1s window) local playersAfter = tonumber(targetElev:GetAttribute("Players")) or 0 local hostAfter = tonumber(targetElev:GetAttribute("Host")) or 0 if playersAfter > 1 or (hostAfter ~= 0 and hostAfter ~= LocalPlayer.UserId) then setStatus("⚠️ Phát hiện trùng thang máy -> Tự động rời sang thang máy Solo khác...") callRemote("ElevatorLeave") State.ElevatorStartAttempts = 0 return end if State.LastElevatorName == targetElev.Name then State.ElevatorStartAttempts = (State.ElevatorStartAttempts or 0) + 1 else State.LastElevatorName = targetElev.Name State.ElevatorStartAttempts = 1 end if State.ElevatorStartAttempts >= 4 then State.ElevatorStartAttempts = 0 callRemote("ElevatorLeave") return end setStatus(string.format("🚀 Starting Solo Match on %s (%s - %s)...", targetElev.Name, mapName, tostring(Cfg.VoteMode or "Easy"))) callRemote("ElevatorStart", targetElev.Name) end local function stepLobbyAutomation() refreshCurrentPlace() ensureGameModules() if not Cfg.Enabled then setStatus("Paused (Master Toggle Off)") return end -- Chế độ Acc Treo Check Banner 24/7 ở Lobby: Chỉ đứng ở Lobby đọc GetSummonSet và đẩy lên VPS 160.191.242.43:8787 if Cfg.BannerWatcherOnly then syncLobbyBanner(false) local info = getBannerTimerInfo() local bc = info.BasicCrate setStatus(string.format("📡 Acc Treo Check Banner -> VPS (Reset %s): [Godly] %s | [Mythic] %s", info.TimeFormatted, tostring(bc.Godly or "?"), tostring(bc.Mythic or "?"))) return end local data = getReplicaData() if not data then setStatus("Waiting for Player ReplicaData...") return end -- 1. Vừa vào game: Nhập toàn bộ Code + Nhận Quest/Battlepass trước tiên (AutoJoinGroup chỉ chạy khi bật thủ công) if Cfg.AutoJoinGroup then doAutoJoinGameGroup(false) end doAutoRedeemCodes(data, false) doAutoClaimQuestsAndPass(data) data = getReplicaData() or data if Cfg.AutoUseBoosts and type(data.Boosts) == "table" then for boostName, count in pairs(data.Boosts) do if tonumber(count) and tonumber(count) > 0 then callRemote("UseBoost", tostring(boostName)) end end end -- 2. Kiểm tra & Equip Speakerman + Cameraman + Ninja Cameraman if Cfg.AutoStarterCameraman then ensureStarterUnitsEquipped(data) data = getReplicaData() or data end -- Tự động bán bớt tướng trùng lặp nếu kho tướng đã tích luỹ >= 90 con (tránh đầy kho khi quay Ninja Cameraman) local totalTowerCount = 0 if type(data.Towers) == "table" then for _ in pairs(data.Towers) do totalTowerCount = totalTowerCount + 1 end end if totalTowerCount >= 90 then doAutoSellDuplicates(data, true) data = getReplicaData() or data end -- 3. Summon Unit thông minh theo Banner (chỉ quay khi Cameraman / Ninja Cameraman thực sự có trên Banner!) doAutoSummonAndCrates(data) data = getReplicaData() or data -- Kiểm tra lại ngay sau khi Summon: nếu vừa roll ra Unit của các Step thì Equip vào luôn! if Cfg.AutoStarterCameraman then ensureStarterUnitsEquipped(data) data = getReplicaData() or data -- Tự động đồng bộ Macro + Mode + Map theo Step cao nhất đã đủ Unit syncStarterMacroByCameraman(data) if not MacroState.IsRecording then Cfg.PlayMacro = true end Cfg.AutoVote = true Cfg.PreferredMap = State.GetActiveStepMap(data) end doAutoSellDuplicates(data, false) if Cfg.AutoEquipBest and not Cfg.AutoStarterCameraman then doAutoEquipBestTeam(data) end -- 4. Khi không cần/không đủ điều kiện summon nữa (hoặc Banner chưa có tướng mục tiêu) -> Join Map chơi Solo ngay! local money = tonumber(data.Money) or 0 local candy = tonumber(data.Candy) or 0 local canSummonMore = shouldSummonBasicCrateNow(data) local busySummoning = canSummonMore or (not State.CrateBlockedThisSession and Cfg.AutoBuyHalloweenCrate and candy >= 500) if busySummoning then return end if Cfg.AutoStarterCameraman or Cfg.AutoJoinElevator then doAutoJoinElevator() else setStatus(string.format("Lobby Ready | Money: $%d | Candy: %d | Wins: %d", money, candy, tonumber(data.Wins) or 0)) end end -- ============================================================================ -- 9. FULL CUSTOM GUI WITH EXPANDABLE DROPDOWNS & SUPER MAX LOW TAB -- ============================================================================ local function createGUI() if not isAlive() then return end elevateThread() pcall(function() if gethui then local hui = gethui() local old0 = hui and hui:FindFirstChild("CTD_AutoHub") if old0 then old0:Destroy() end end end) pcall(function() local old1 = CoreGui:FindFirstChild("CTD_AutoHub") if old1 then old1:Destroy() end end) pcall(function() local pg = LocalPlayer and LocalPlayer:FindFirstChildOfClass("PlayerGui") local old2 = pg and pg:FindFirstChild("CTD_AutoHub") if old2 then old2:Destroy() end end) local sg = Instance.new("ScreenGui") sg.Name = "CTD_AutoHub" sg.ResetOnSpawn = false sg.ZIndexBehavior = Enum.ZIndexBehavior.Sibling -- Parent to PlayerGui FIRST so GUI elements NEVER inherit 'Plugin' capability restrictions from CoreGui! -- This guarantees 0 "The current thread cannot access 'Instance' (lacking capability Plugin)" errors on any signal/button callback thread! local parented = false local pg = LocalPlayer and LocalPlayer:FindFirstChildOfClass("PlayerGui") if pg then parented = pcall(function() sg.Parent = pg end) and (sg.Parent ~= nil) end if not parented or not sg.Parent then task.spawn(function() elevateThread() local pgDeadline = os.clock() + 10 while isAlive() and not pg and os.clock() < pgDeadline do task.wait(0.1) LocalPlayer = Players.LocalPlayer or LocalPlayer pg = LocalPlayer and LocalPlayer:FindFirstChildOfClass("PlayerGui") end if isAlive() and pg then pcall(function() sg.Parent = pg end) end end) end UIRefs.ScreenGui = sg -- Floating Toggle Button local floatBtn = Instance.new("TextButton") floatBtn.Name = "FloatToggle" floatBtn.Size = UDim2.new(0, 68, 0, 32) floatBtn.Position = UDim2.new(0, 16, 0.5, -16) floatBtn.BackgroundColor3 = Color3.fromRGB(22, 26, 36) floatBtn.BorderSizePixel = 0 floatBtn.Font = Enum.Font.GothamBold floatBtn.TextSize = 13 floatBtn.TextColor3 = Color3.fromRGB(80, 210, 255) floatBtn.Text = "[CTD]" floatBtn.Parent = sg local floatCorner = Instance.new("UICorner") floatCorner.CornerRadius = UDim.new(0, 8) floatCorner.Parent = floatBtn local floatStroke = Instance.new("UIStroke") floatStroke.Color = Color3.fromRGB(80, 210, 255) floatStroke.Thickness = 1.5 floatStroke.Parent = floatBtn -- Main Window local main = Instance.new("Frame") main.Name = "MainFrame" main.Size = UDim2.new(0, 560, 0, 430) main.Position = UDim2.new(0.5, -280, 0.5, -215) main.BackgroundColor3 = Color3.fromRGB(16, 18, 26) main.BorderSizePixel = 0 main.Parent = sg local mainCorner = Instance.new("UICorner") mainCorner.CornerRadius = UDim.new(0, 10) mainCorner.Parent = main local mainStroke = Instance.new("UIStroke") mainStroke.Color = Color3.fromRGB(55, 65, 95) mainStroke.Thickness = 1.5 mainStroke.Parent = main -- Top Bar local topBar = Instance.new("Frame") topBar.Size = UDim2.new(1, 0, 0, 36) topBar.BackgroundColor3 = Color3.fromRGB(24, 28, 40) topBar.BorderSizePixel = 0 topBar.Parent = main local topCorner = Instance.new("UICorner") topCorner.CornerRadius = UDim.new(0, 10) topCorner.Parent = topBar local titleLbl = Instance.new("TextLabel") titleLbl.Size = UDim2.new(1, -110, 1, 0) titleLbl.Position = UDim2.new(0, 14, 0, 0) titleLbl.BackgroundTransparency = 1 titleLbl.Font = Enum.Font.GothamBold titleLbl.TextSize = 14 titleLbl.TextXAlignment = Enum.TextXAlignment.Left titleLbl.TextColor3 = Color3.fromRGB(235, 242, 255) titleLbl.Text = "CAMERAMAN TD - MACRO & SUPER MAX LOW HUB" titleLbl.Parent = topBar local minBtn = Instance.new("TextButton") minBtn.Size = UDim2.new(0, 28, 0, 24) minBtn.Position = UDim2.new(1, -36, 0, 6) minBtn.BackgroundColor3 = Color3.fromRGB(40, 46, 66) minBtn.BorderSizePixel = 0 minBtn.Font = Enum.Font.GothamBold minBtn.TextSize = 14 minBtn.TextColor3 = Color3.fromRGB(220, 230, 255) minBtn.Text = "-" minBtn.Parent = topBar local minCorner = Instance.new("UICorner") minCorner.CornerRadius = UDim.new(0, 6) minCorner.Parent = minBtn do local dragging = false local dragStart, startPos trackConn(topBar.InputBegan:Connect(function(inp) elevateThread() if inp.UserInputType == Enum.UserInputType.MouseButton1 or inp.UserInputType == Enum.UserInputType.Touch then pcall(function() local ds = inp.Position local sp = main.Position if ds and sp then dragStart = ds startPos = sp dragging = true end end) end end)) trackConn(UserInputService.InputChanged:Connect(function(inp) if dragging and dragStart and startPos and (inp.UserInputType == Enum.UserInputType.MouseMovement or inp.UserInputType == Enum.UserInputType.Touch) then elevateThread() pcall(function() local delta = inp.Position - dragStart main.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y) end) end end)) trackConn(UserInputService.InputEnded:Connect(function(inp) if inp.UserInputType == Enum.UserInputType.MouseButton1 or inp.UserInputType == Enum.UserInputType.Touch then dragging = false end end)) end trackConn(floatBtn.MouseButton1Click:Connect(function() elevateThread() pcall(function() main.Visible = not main.Visible end) end)) trackConn(minBtn.MouseButton1Click:Connect(function() elevateThread() pcall(function() main.Visible = false end) end)) -- Telemetry Banner local statusBox = Instance.new("Frame") statusBox.Size = UDim2.new(1, -20, 0, 52) statusBox.Position = UDim2.new(0, 10, 0, 42) statusBox.BackgroundColor3 = Color3.fromRGB(22, 26, 38) statusBox.BorderSizePixel = 0 statusBox.Parent = main local statusCorner = Instance.new("UICorner") statusCorner.CornerRadius = UDim.new(0, 8) statusCorner.Parent = statusBox local statsLbl = Instance.new("TextLabel") statsLbl.Size = UDim2.new(1, -16, 0, 22) statsLbl.Position = UDim2.new(0, 8, 0, 4) statsLbl.BackgroundTransparency = 1 statsLbl.Font = Enum.Font.GothamSemibold statsLbl.TextSize = 12 statsLbl.TextXAlignment = Enum.TextXAlignment.Left statsLbl.TextColor3 = Color3.fromRGB(130, 220, 255) refreshCurrentPlace() if CURRENT_PLACE == PLACE_MATCH then statsLbl.Text = string.format("▶️ PLAY [%s] | Voting W:0/20 | Cash:$%d | T:0/15", tostring(Cfg.SelectedMacro or "cac"), getMyMatchCash()) else statsLbl.Text = string.format("LOBBY | Macro:%s (%d)", tostring(Cfg.SelectedMacro or "cac"), #MacroState.Steps) end statsLbl.Parent = statusBox UIRefs.StatsLabel = statsLbl local actionLbl = Instance.new("TextLabel") actionLbl.Size = UDim2.new(1, -16, 0, 20) actionLbl.Position = UDim2.new(0, 8, 0, 26) actionLbl.BackgroundTransparency = 1 actionLbl.Font = Enum.Font.Gotham actionLbl.TextSize = 12 actionLbl.TextXAlignment = Enum.TextXAlignment.Left actionLbl.TextColor3 = Color3.fromRGB(190, 205, 230) actionLbl.Text = "Status: Ready" actionLbl.Parent = statusBox UIRefs.ActionLabel = actionLbl -- Tab Bar local tabBar = Instance.new("Frame") tabBar.Size = UDim2.new(0, 128, 1, -108) tabBar.Position = UDim2.new(0, 10, 0, 100) tabBar.BackgroundColor3 = Color3.fromRGB(22, 25, 36) tabBar.BorderSizePixel = 0 tabBar.Parent = main local tabCorner = Instance.new("UICorner") tabCorner.CornerRadius = UDim.new(0, 8) tabCorner.Parent = tabBar local tabLayout = Instance.new("UIListLayout") tabLayout.Padding = UDim.new(0, 6) tabLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center tabLayout.Parent = tabBar local tabPad = Instance.new("UIPadding") tabPad.PaddingTop = UDim.new(0, 8) tabPad.Parent = tabBar -- Content Container local contentArea = Instance.new("Frame") contentArea.Size = UDim2.new(1, -154, 1, -108) contentArea.Position = UDim2.new(0, 144, 0, 100) contentArea.BackgroundColor3 = Color3.fromRGB(20, 23, 33) contentArea.BorderSizePixel = 0 contentArea.Parent = main local contentCorner = Instance.new("UICorner") contentCorner.CornerRadius = UDim.new(0, 8) contentCorner.Parent = contentArea local Pages = {} local TabButtons = {} local function createTab(tabName) local btn = Instance.new("TextButton") btn.Size = UDim2.new(1, -14, 0, 32) btn.BackgroundColor3 = Color3.fromRGB(30, 35, 50) btn.BorderSizePixel = 0 btn.Font = Enum.Font.GothamSemibold btn.TextSize = 12 btn.TextColor3 = Color3.fromRGB(200, 210, 235) btn.Text = tabName btn.Parent = tabBar local bc = Instance.new("UICorner") bc.CornerRadius = UDim.new(0, 6) bc.Parent = btn local page = Instance.new("ScrollingFrame") page.Size = UDim2.new(1, -12, 1, -12) page.Position = UDim2.new(0, 6, 0, 6) page.BackgroundTransparency = 1 page.BorderSizePixel = 0 page.ScrollBarThickness = 4 page.AutomaticCanvasSize = Enum.AutomaticSize.Y page.CanvasSize = UDim2.new(0, 0, 0, 0) page.Visible = false page.Parent = contentArea local layout = Instance.new("UIListLayout") layout.Padding = UDim.new(0, 6) layout.SortOrder = Enum.SortOrder.LayoutOrder layout.Parent = page Pages[tabName] = page TabButtons[tabName] = btn trackConn(btn.MouseButton1Click:Connect(function() elevateThread() pcall(function() for k, p in pairs(Pages) do p.Visible = (k == tabName) TabButtons[k].BackgroundColor3 = (k == tabName) and Color3.fromRGB(55, 120, 210) or Color3.fromRGB(30, 35, 50) end end) end)) return page end local function addToggle(page, labelText, cfgKey, order, onChange) local row = Instance.new("Frame") row.Size = UDim2.new(1, -8, 0, 30) row.BackgroundColor3 = Color3.fromRGB(28, 32, 46) row.BorderSizePixel = 0 row.LayoutOrder = order row.Parent = page local rc = Instance.new("UICorner") rc.CornerRadius = UDim.new(0, 6) rc.Parent = row local lbl = Instance.new("TextLabel") lbl.Size = UDim2.new(1, -76, 1, 0) lbl.Position = UDim2.new(0, 10, 0, 0) lbl.BackgroundTransparency = 1 lbl.Font = Enum.Font.Gotham lbl.TextSize = 12 lbl.TextXAlignment = Enum.TextXAlignment.Left lbl.TextColor3 = Color3.fromRGB(225, 232, 248) lbl.Text = labelText lbl.Parent = row local tBtn = Instance.new("TextButton") tBtn.Size = UDim2.new(0, 54, 0, 22) tBtn.Position = UDim2.new(1, -62, 0.5, -11) tBtn.BorderSizePixel = 0 tBtn.Font = Enum.Font.GothamBold tBtn.TextSize = 11 tBtn.Parent = row local tc = Instance.new("UICorner") tc.CornerRadius = UDim.new(0, 5) tc.Parent = tBtn local function refresh() elevateThread() pcall(function() local on = Cfg[cfgKey] == true tBtn.Text = on and "ON" or "OFF" tBtn.BackgroundColor3 = on and Color3.fromRGB(45, 175, 105) or Color3.fromRGB(160, 55, 65) tBtn.TextColor3 = Color3.fromRGB(255, 255, 255) end) end refresh() trackConn(tBtn.MouseButton1Click:Connect(function() elevateThread() Cfg[cfgKey] = not Cfg[cfgKey] refresh() saveConfig() if onChange then pcall(onChange, Cfg[cfgKey]) end end)) return refresh end -- TRUE EXPANDABLE DROPDOWN MENU COMPONENT local function addDropdown(page, labelText, cfgKey, getOptionsFn, order, onChange) local container = Instance.new("Frame") container.Size = UDim2.new(1, -8, 0, 32) container.BackgroundColor3 = Color3.fromRGB(28, 32, 46) container.BorderSizePixel = 0 container.ClipsDescendants = true container.LayoutOrder = order container.Parent = page local cc = Instance.new("UICorner") cc.CornerRadius = UDim.new(0, 6) cc.Parent = container local header = Instance.new("Frame") header.Size = UDim2.new(1, 0, 0, 32) header.BackgroundTransparency = 1 header.Parent = container local lbl = Instance.new("TextLabel") lbl.Size = UDim2.new(1, -165, 1, 0) lbl.Position = UDim2.new(0, 10, 0, 0) lbl.BackgroundTransparency = 1 lbl.Font = Enum.Font.Gotham lbl.TextSize = 12 lbl.TextXAlignment = Enum.TextXAlignment.Left lbl.TextColor3 = Color3.fromRGB(225, 232, 248) lbl.Text = labelText lbl.Parent = header local dropBtn = Instance.new("TextButton") dropBtn.Size = UDim2.new(0, 150, 0, 24) dropBtn.Position = UDim2.new(1, -158, 0, 4) dropBtn.BackgroundColor3 = Color3.fromRGB(44, 80, 155) dropBtn.BorderSizePixel = 0 dropBtn.Font = Enum.Font.GothamBold dropBtn.TextSize = 11 dropBtn.TextColor3 = Color3.fromRGB(255, 255, 255) dropBtn.Text = tostring(Cfg[cfgKey]) .. " ▼" dropBtn.Parent = header local dbc = Instance.new("UICorner") dbc.CornerRadius = UDim.new(0, 5) dbc.Parent = dropBtn local listFrame = Instance.new("Frame") listFrame.Size = UDim2.new(1, -16, 0, 0) listFrame.Position = UDim2.new(0, 8, 0, 34) listFrame.BackgroundTransparency = 1 listFrame.Parent = container local listLayout = Instance.new("UIListLayout") listLayout.Padding = UDim.new(0, 4) listLayout.SortOrder = Enum.SortOrder.LayoutOrder listLayout.Parent = listFrame local isOpen = false local function rebuildOptions() elevateThread() for _, ch in ipairs(listFrame:GetChildren()) do if ch:IsA("TextButton") then ch:Destroy() end end local options = type(getOptionsFn) == "function" and getOptionsFn() or getOptionsFn for i, opt in ipairs(options) do local selected = (tostring(Cfg[cfgKey]) == tostring(opt)) local itemBtn = Instance.new("TextButton") itemBtn.Size = UDim2.new(1, 0, 0, 24) itemBtn.BackgroundColor3 = selected and Color3.fromRGB(52, 115, 200) or Color3.fromRGB(36, 42, 60) itemBtn.BorderSizePixel = 0 itemBtn.Font = selected and Enum.Font.GothamBold or Enum.Font.Gotham itemBtn.TextSize = 11 itemBtn.TextXAlignment = Enum.TextXAlignment.Left itemBtn.TextColor3 = selected and Color3.fromRGB(255, 255, 255) or Color3.fromRGB(205, 215, 238) itemBtn.Text = (selected and " ✓ " or " ") .. tostring(opt) itemBtn.LayoutOrder = i itemBtn.Parent = listFrame local ic = Instance.new("UICorner") ic.CornerRadius = UDim.new(0, 5) ic.Parent = itemBtn trackConn(itemBtn.MouseButton1Click:Connect(function() elevateThread() pcall(function() Cfg[cfgKey] = opt isOpen = false dropBtn.Text = tostring(Cfg[cfgKey]) .. " ▼" container.Size = UDim2.new(1, -8, 0, 32) saveConfig() end) if onChange then pcall(onChange, Cfg[cfgKey]) end end)) end return #options end local function setExternalValue(newVal) elevateThread() pcall(function() Cfg[cfgKey] = newVal dropBtn.Text = tostring(Cfg[cfgKey]) .. (isOpen and " ▲" or " ▼") if isOpen then local count = rebuildOptions() container.Size = UDim2.new(1, -8, 0, 36 + count * 28) end end) end trackConn(dropBtn.MouseButton1Click:Connect(function() elevateThread() pcall(function() isOpen = not isOpen if isOpen then local count = rebuildOptions() dropBtn.Text = tostring(Cfg[cfgKey]) .. " ▲" container.Size = UDim2.new(1, -8, 0, 36 + count * 28) else dropBtn.Text = tostring(Cfg[cfgKey]) .. " ▼" container.Size = UDim2.new(1, -8, 0, 32) end end) end)) return setExternalValue end local function addActionButton(page, labelText, color, callback, order) local btn = Instance.new("TextButton") btn.Size = UDim2.new(1, -8, 0, 30) btn.BackgroundColor3 = color or Color3.fromRGB(44, 72, 130) btn.BorderSizePixel = 0 btn.Font = Enum.Font.GothamSemibold btn.TextSize = 12 btn.TextColor3 = Color3.fromRGB(240, 246, 255) btn.Text = labelText btn.LayoutOrder = order btn.Parent = page local bc = Instance.new("UICorner") bc.CornerRadius = UDim.new(0, 6) bc.Parent = btn trackConn(btn.MouseButton1Click:Connect(function() task.spawn(function() elevateThread() pcall(callback) end) end)) return btn end -- Create 5 Pages local macroPage = createTab("Macro System") local lowPage = createTab("Super Max Low") local matchPage = createTab("Match & Cam") local lobbyPage = createTab("Lobby Auto") local logPage = createTab("Macro Steps") UIRefs.MainFrame = main UIRefs.LobbyPage = lobbyPage UIRefs.LogPage = logPage -- 1. Populate MACRO SYSTEM Page local setMacroDropdown = addDropdown(macroPage, "Macro Profile (Dropdown)", "SelectedMacro", listSavedMacros, 1, function(newName) MacroState.ManualMacroLocked = true loadMacroFromDisk(newName) end) UIRefs.SetMacroDropdown = setMacroDropdown local pendingNewMacroBox = nil -- New Macro Name Input Row do local newMacroRow = Instance.new("Frame") newMacroRow.Size = UDim2.new(1, -8, 0, 30) newMacroRow.BackgroundColor3 = Color3.fromRGB(28, 32, 46) newMacroRow.BorderSizePixel = 0 newMacroRow.LayoutOrder = 2 newMacroRow.Parent = macroPage local nmc = Instance.new("UICorner") nmc.CornerRadius = UDim.new(0, 6) nmc.Parent = newMacroRow local nameBox = Instance.new("TextBox") nameBox.Size = UDim2.new(1, -96, 0, 22) nameBox.Position = UDim2.new(0, 8, 0, 4) nameBox.BackgroundColor3 = Color3.fromRGB(18, 22, 32) nameBox.BorderSizePixel = 0 nameBox.Font = Enum.Font.Gotham nameBox.TextSize = 11 nameBox.TextColor3 = Color3.fromRGB(235, 242, 255) nameBox.PlaceholderText = "Nhập tên Macro mới (VD: Toilet_Win)..." nameBox.PlaceholderColor3 = Color3.fromRGB(120, 130, 155) nameBox.Text = "" nameBox.ClearTextOnFocus = false nameBox.Parent = newMacroRow pendingNewMacroBox = nameBox local nbc = Instance.new("UICorner") nbc.CornerRadius = UDim.new(0, 5) nbc.Parent = nameBox local createBtn = Instance.new("TextButton") createBtn.Size = UDim2.new(0, 76, 0, 22) createBtn.Position = UDim2.new(1, -82, 0, 4) createBtn.BackgroundColor3 = Color3.fromRGB(45, 160, 100) createBtn.BorderSizePixel = 0 createBtn.Font = Enum.Font.GothamBold createBtn.TextSize = 11 createBtn.TextColor3 = Color3.fromRGB(255, 255, 255) createBtn.Text = "+ Tạo Mới" createBtn.Parent = newMacroRow local cbc = Instance.new("UICorner") cbc.CornerRadius = UDim.new(0, 5) cbc.Parent = createBtn local function createAndSelectMacroFromBox() local clean = tostring(nameBox.Text or ""):gsub("[^%w_%-]", "_") if clean ~= "" then MacroState.ManualMacroLocked = true table.insert(MacroState.CustomNames, clean) setMacroDropdown(clean) if MacroState.IsRecording then MacroState.RecordingMacroName = clean end MacroState.Steps = {} MacroState.CurrentStep = 1 saveMacroToDisk(clean) nameBox.Text = "" return clean end return nil end trackConn(createBtn.MouseButton1Click:Connect(createAndSelectMacroFromBox)) trackConn(nameBox.FocusLost:Connect(function(enterPressed) if enterPressed then createAndSelectMacroFromBox() end end)) end local refreshPlayToggle refreshPlayToggle = addToggle(macroPage, "▶️ Play Macro Automatically", "PlayMacro", 3, function(on) if on and MacroState.IsRecording then stopMacroRecording(true) end if on then resetMacroPlaybackMappings() end end) local recBtn recBtn = addActionButton(macroPage, "🔴 Start Recording Macro (Manual Play)", Color3.fromRGB(175, 45, 55), function() if not MacroState.IsRecording then -- If user typed a new macro name into the box and clicked Start Recording directly, select & create it first! if pendingNewMacroBox then local typed = tostring(pendingNewMacroBox.Text or ""):gsub("[^%w_%-]", "_") if typed ~= "" then table.insert(MacroState.CustomNames, typed) setMacroDropdown(typed) pendingNewMacroBox.Text = "" end end local targetMacro = tostring(Cfg.SelectedMacro or "Default") startMacroRecording(true, targetMacro) if refreshPlayToggle then refreshPlayToggle() end recBtn.Text = string.format("⏹ Stop & Save Macro ['%s']", targetMacro) recBtn.BackgroundColor3 = Color3.fromRGB(45, 165, 95) else stopMacroRecording(true) recBtn.Text = "🔴 Start Recording Macro (Manual Play)" recBtn.BackgroundColor3 = Color3.fromRGB(175, 45, 55) end end, 4) addDropdown(macroPage, "Playback Sync Mode", "MacroTimingMode", { "Cash & Wave", "Cash Only", "Exact Time" }, 5) addToggle(macroPage, "Show 3D Macro Markers On Map", "ShowMacroVisuals", 6, function() refreshMacroVisuals() end) addToggle(macroPage, "Record Wave Skips", "RecordWaveSkips", 7) -- Quick Placed Tower Micro (Select & Sell / Upgrade for Sell & Re-Place Macros!) addDropdown(macroPage, "🎯 Select Placed Tower (Micro)", "QuickTowerTarget", getQuickTowerDropdownOptions, 8, function(val) MacroState.QuickTargetSelection = val local t = resolveQuickSelectedTower() if t then MacroState.SelectedTowerId = t.Id syncRecordingTowerHitboxes() end end) addActionButton(macroPage, "💰 Sell Selected Tower (Recordable -> Place New Spot)", Color3.fromRGB(200, 115, 35), function() local t = resolveQuickSelectedTower() if t and Modules and Modules.Network then local net = Modules.Network.Get("TowerSell") if net then net:FireServer({ Id = tostring(t.Id) }) setStatus(string.format("💰 Selling Tower %s (%s) -> Ready to Place at New Spot!", tostring(t.Name), tostring(t.Id):sub(1, 6))) end else setStatus("⚠️ No Placed Tower Found to Sell!") end end, 9) addActionButton(macroPage, "⬆️ Upgrade Selected Tower (Recordable)", Color3.fromRGB(45, 135, 95), function() local t = resolveQuickSelectedTower() if t and Modules and Modules.Network then local net = Modules.Network.Get("TowerUpgrade") if net then net:FireServer({ Id = tostring(t.Id) }) end else setStatus("⚠️ No Placed Tower Found to Upgrade!") end end, 10) addActionButton(macroPage, "💾 Save Current Macro To Disk", Color3.fromRGB(44, 72, 130), function() saveMacroToDisk(Cfg.SelectedMacro) end, 11) addActionButton(macroPage, "🔄 Reload Macro & Reset To Step 1", Color3.fromRGB(44, 72, 130), function() loadMacroFromDisk(Cfg.SelectedMacro) end, 12) addActionButton(macroPage, "☁️ Pull / Update All Macros From VPS Server", Color3.fromRGB(38, 120, 175), function() syncMacrosFromVPS(true, false) end, 13) addActionButton(macroPage, "☁️ Upload Current Macro To VPS Server", Color3.fromRGB(42, 145, 110), function() saveMacroToDisk(Cfg.SelectedMacro, true) uploadMacroToVPS(Cfg.SelectedMacro, false) end, 14) addActionButton(macroPage, "🗑️ Clear Current Macro Steps", Color3.fromRGB(120, 50, 55), function() MacroState.ManualMacroLocked = true MacroState.Steps = {} resetMacroPlaybackMappings() saveMacroToDisk(Cfg.SelectedMacro) setStatus("Cleared Macro: " .. tostring(Cfg.SelectedMacro)) end, 15) addActionButton(macroPage, "❌ Delete Macro File From Disk (delfile)", Color3.fromRGB(165, 35, 45), function() local deletedName = Cfg.SelectedMacro deleteMacroFileFromDisk(deletedName) setMacroDropdown("Default") loadMacroFromDisk("Default") end, 16) -- 2. Populate SUPER MAX LOW (True Delete Everything / Headless) Page addToggle(lowPage, "⚡ Enable Super Max Low Engine", "SuperMaxLow", 1, function(on) if on then task.spawn(applySuperMaxLow) end end) addToggle(lowPage, "🔥 True Delete Everything (0-Part Void / TTD Mode)", "DeleteEverything", 2, function(on) if on then task.spawn(applySuperMaxLow) else restoreMinimalForRecording() end end) addToggle(lowPage, "🧱 Create Safety Platform Under Feet (Anti-Void Death)", "SafetyPlatform", 3, function(on) if on then ensureSafetyPlatform() end end) addToggle(lowPage, "🗑️ Delete All Game UI (Keep Only CTD Hub)", "DeleteGameUI", 4, function(on) if on then deleteAllGameUIAndSeverConns() end end) addToggle(lowPage, "🛡️ Skip All Errors & Anti-RAM Leak (Nuke 103k Cutscenes)", "SkipAllErrors", 5, function(on) if on then suppressAllErrorsAndPreventRAMLeak() end end) addToggle(lowPage, "📦 Strip Towers & Mobs To 1-Part Box (When Recording)", "StripModelsToBox", 6, function(on) if on then task.spawn(applySuperMaxLow) end end) addToggle(lowPage, "🖥️ Headless Mode (Disable 3D Render - 0% GPU)", "HeadlessNo3D", 7, function() applyHeadlessMode() end) addDropdown(lowPage, "FPS Cap Limit (Dropdown)", "FPSCap", { "Unlimited", "60 FPS", "30 FPS", "15 FPS", "10 FPS" }, 8, function() applyFPSCap() end) addActionButton(lowPage, "🔥 Clean Delete Map Now", Color3.fromRGB(175, 50, 55), function() Cfg.SuperMaxLow = true Cfg.DeleteEverything = true Cfg.SafetyPlatform = true task.spawn(applySuperMaxLow) end, 9) addActionButton(lowPage, "🛠️ Restore Minimal Ground (For Manual Recording)", Color3.fromRGB(45, 135, 95), function() restoreMinimalForRecording() end, 10) -- 3. Populate MATCH & CAMERA (Anti-AntiMacro) Page addToggle(matchPage, "Master Enable Automation", "Enabled", 1) addToggle(matchPage, "🛡️ Bypass AntiMacro (Lock Camera Spin)", "BypassAntiMacro", 2) addToggle(matchPage, "📷 Lock Fixed Top-Down Camera", "LockTopDownCamera", 3, function(on) if not on and workspace.CurrentCamera then workspace.CurrentCamera.CameraType = Enum.CameraType.Custom end end) addToggle(matchPage, "Auto Vote Difficulty", "AutoVote", 4) UIRefs.SetVoteModeDropdown = addDropdown(matchPage, "Vote Difficulty (Dropdown)", "VoteMode", { "Easy", "Normal", "Hard", "Insane", "Nightmare", "Macro" }, 5) addToggle(matchPage, "⚡ Auto Speed (1.5x Group / 2x Pass)", "AutoSpeed2x", 6, function(on) if on then State.SpeedSetThisMatch = false State.SpeedAttemptCount = 0 end end) addToggle(matchPage, "Auto Skip Waves (Always)", "AutoSkipWave", 7) addToggle(matchPage, "Auto Medic Ability (Un-Stun)", "AutoAbility", 8) addToggle(matchPage, "Auto Handle Match End", "AutoMatchEnd", 9) addDropdown(matchPage, "Match End Action (Dropdown)", "MatchEndAction", { "PlayAgain", "Smart", "ReturnLobby" }, 10) addToggle(matchPage, "🚀 Smart Dynamic Step Progression (Auto Macro/Unit/Banner)", "AutoStarterCameraman", 11, function(on) if on then MacroState.ManualMacroLocked = false syncStarterMacroByCameraman(getReplicaData()) end end) do local matchStepRow = Instance.new("Frame") matchStepRow.Size = UDim2.new(1, -8, 0, 30) matchStepRow.BackgroundColor3 = Color3.fromRGB(28, 32, 46) matchStepRow.BorderSizePixel = 0 matchStepRow.LayoutOrder = 11 matchStepRow.Parent = matchPage local msrc = Instance.new("UICorner") msrc.CornerRadius = UDim.new(0, 6) msrc.Parent = matchStepRow local matchStepBox = Instance.new("TextBox") matchStepBox.Size = UDim2.new(1, -158, 0, 22) matchStepBox.Position = UDim2.new(0, 8, 0, 4) matchStepBox.BackgroundColor3 = Color3.fromRGB(18, 22, 32) matchStepBox.BorderSizePixel = 0 matchStepBox.Font = Enum.Font.Code matchStepBox.TextSize = 11 matchStepBox.TextColor3 = Color3.fromRGB(235, 242, 255) matchStepBox.PlaceholderText = "Chuỗi Step (VD: cac > cac2 > cac3 > cac3:Titan Cinemaman,Glitch Cameraman)" matchStepBox.PlaceholderColor3 = Color3.fromRGB(120, 130, 155) matchStepBox.Text = tostring(Cfg.StepChainText or DEFAULT_CFG.StepChainText) matchStepBox.ClearTextOnFocus = false matchStepBox.Parent = matchStepRow UIRefs.MatchStepChainBox = matchStepBox local msbc = Instance.new("UICorner") msbc.CornerRadius = UDim.new(0, 5) msbc.Parent = matchStepBox local saveStepBtn = Instance.new("TextButton") saveStepBtn.Size = UDim2.new(0, 72, 0, 22) saveStepBtn.Position = UDim2.new(1, -146, 0, 4) saveStepBtn.BackgroundColor3 = Color3.fromRGB(45, 160, 100) saveStepBtn.BorderSizePixel = 0 saveStepBtn.Font = Enum.Font.GothamBold saveStepBtn.TextSize = 11 saveStepBtn.TextColor3 = Color3.fromRGB(255, 255, 255) saveStepBtn.Text = "💾 Lưu Step" saveStepBtn.Parent = matchStepRow local ssbc = Instance.new("UICorner") ssbc.CornerRadius = UDim.new(0, 5) ssbc.Parent = saveStepBtn local addStepBtn = Instance.new("TextButton") addStepBtn.Size = UDim2.new(0, 66, 0, 22) addStepBtn.Position = UDim2.new(1, -70, 0, 4) addStepBtn.BackgroundColor3 = Color3.fromRGB(55, 115, 195) addStepBtn.BorderSizePixel = 0 addStepBtn.Font = Enum.Font.GothamBold addStepBtn.TextSize = 11 addStepBtn.TextColor3 = Color3.fromRGB(255, 255, 255) addStepBtn.Text = "➕ +Macro" addStepBtn.Parent = matchStepRow local asbc = Instance.new("UICorner") asbc.CornerRadius = UDim.new(0, 5) asbc.Parent = addStepBtn trackConn(saveStepBtn.MouseButton1Click:Connect(function() State.ApplyStepChainNow(matchStepBox.Text) end)) trackConn(addStepBtn.MouseButton1Click:Connect(function() local curChain = tostring(matchStepBox.Text or Cfg.StepChainText or ""):gsub("^%s+", ""):gsub("%s+$", "") local curMacro = tostring(Cfg.SelectedMacro or "cac") local nextChain = (curChain ~= "") and (curChain .. " > " .. curMacro) or curMacro State.ApplyStepChainNow(nextChain) end)) end addDropdown(matchPage, "Farm Money To Return & Roll (Dropdown)", "ReturnMoneyThreshold", { 100, 500, 900, 1000, 2000 }, 12) addToggle(matchPage, "📨 Send Discord Webhook On Match End", "WebhookOnMatchEnd", 13) addActionButton(matchPage, "📨 Test Send Discord Webhook Now", Color3.fromRGB(88, 101, 242), function() task.spawn(function() sendMatchEndWebhook({ IsTest = true }) end) end, 14) addActionButton(matchPage, "Teleport Back To Lobby Now", Color3.fromRGB(130, 60, 50), function() callRemote("OnLeave") task.wait(1) callRemote("TeleportBack") end, 15) -- 4. Populate LOBBY AUTO Page (With Cross-Place Summon Banner Inspector!) do local bannerBox = Instance.new("TextLabel") bannerBox.Size = UDim2.new(1, -8, 0, 88) bannerBox.BackgroundColor3 = Color3.fromRGB(24, 30, 46) bannerBox.BorderSizePixel = 0 bannerBox.Font = Enum.Font.Code bannerBox.TextSize = 11 bannerBox.TextXAlignment = Enum.TextXAlignment.Left bannerBox.TextYAlignment = Enum.TextYAlignment.Top bannerBox.TextColor3 = Color3.fromRGB(220, 238, 255) bannerBox.Text = formatBannerInspectorText() bannerBox.LayoutOrder = 1 bannerBox.Parent = lobbyPage UIRefs.BannerInfoLabel = bannerBox local bbc = Instance.new("UICorner") bbc.CornerRadius = UDim.new(0, 6) bbc.Parent = bannerBox local bbs = Instance.new("UIStroke") bbs.Color = Color3.fromRGB(65, 130, 220) bbs.Thickness = 1 bbs.Parent = bannerBox local bbp = Instance.new("UIPadding") bbp.PaddingLeft = UDim.new(0, 8) bbp.PaddingTop = UDim.new(0, 6) bbp.Parent = bannerBox end addActionButton(lobbyPage, "🌐 Check / Refresh Summon Banner (VPS 160.191.242.43 & Lobby)", Color3.fromRGB(48, 115, 185), function() if CURRENT_PLACE == PLACE_LOBBY then syncLobbyBanner(true) else pullBannerFromVPS(true) local info = getBannerTimerInfo() if info.HasUnits then local bc = info.BasicCrate setStatus(string.format("🌐 VPS Banner [%s | Reset %s]: [Godly] %s | [Mythic] %s", tostring(info.Source), info.TimeFormatted, tostring(bc.Godly or "?"), tostring(bc.Mythic or "?"))) else setStatus(string.format("⚠️ VPS chưa có dữ liệu Banner (Hãy treo 1 Acc ở Lobby)! Next Reset: %s", info.TimeFormatted)) end end end, 2) addToggle(lobbyPage, "📡 Chế Độ Acc Treo Check Banner 24/7 (Stay Lobby -> VPS)", "BannerWatcherOnly", 3, function(on) if on and CURRENT_PLACE == PLACE_LOBBY then syncLobbyBanner(true) end end) addToggle(lobbyPage, "👥 Auto Join Group Game (Mở 1.5x Speed)", "AutoJoinGroup", 4, function(on) if on then task.spawn(function() doAutoJoinGameGroup(true) end) end end) addToggle(lobbyPage, "🎁 Auto Redeem All Codes (In Lobby)", "AutoRedeemCodes", 5) -- Manual Code Input Row (Redeem & Save to CTD_Codes.json) do local codeRow = Instance.new("Frame") codeRow.Size = UDim2.new(1, -8, 0, 30) codeRow.BackgroundColor3 = Color3.fromRGB(28, 32, 46) codeRow.BorderSizePixel = 0 codeRow.LayoutOrder = 5 codeRow.Parent = lobbyPage local crc = Instance.new("UICorner") crc.CornerRadius = UDim.new(0, 6) crc.Parent = codeRow local codeBox = Instance.new("TextBox") codeBox.Size = UDim2.new(1, -104, 0, 22) codeBox.Position = UDim2.new(0, 8, 0, 4) codeBox.BackgroundColor3 = Color3.fromRGB(18, 22, 32) codeBox.BorderSizePixel = 0 codeBox.Font = Enum.Font.Gotham codeBox.TextSize = 11 codeBox.TextColor3 = Color3.fromRGB(235, 242, 255) codeBox.PlaceholderText = "Nhập Code mới (VD: UTCM, Battlepass)..." codeBox.PlaceholderColor3 = Color3.fromRGB(120, 130, 155) codeBox.Text = "" codeBox.ClearTextOnFocus = false codeBox.Parent = codeRow local cbc = Instance.new("UICorner") cbc.CornerRadius = UDim.new(0, 5) cbc.Parent = codeBox local redeemBtn = Instance.new("TextButton") redeemBtn.Size = UDim2.new(0, 84, 0, 22) redeemBtn.Position = UDim2.new(1, -90, 0, 4) redeemBtn.BackgroundColor3 = Color3.fromRGB(45, 160, 100) redeemBtn.BorderSizePixel = 0 redeemBtn.Font = Enum.Font.GothamBold redeemBtn.TextSize = 11 redeemBtn.TextColor3 = Color3.fromRGB(255, 255, 255) redeemBtn.Text = "🎁 Nhập Code" redeemBtn.Parent = codeRow local rbc = Instance.new("UICorner") rbc.CornerRadius = UDim.new(0, 5) rbc.Parent = redeemBtn trackConn(redeemBtn.MouseButton1Click:Connect(function() local raw = tostring(codeBox.Text or "") if raw:match("%S") then task.spawn(function() addAndRedeemCustomInput(raw) end) codeBox.Text = "" else task.spawn(function() doAutoRedeemCodes(getReplicaData(), true) end) end end)) end addActionButton(lobbyPage, "🌐 Auto-Update & Redeem All Codes Now", Color3.fromRGB(45, 135, 95), function() doAutoRedeemCodes(getReplicaData(), true) end, 5) addActionButton(lobbyPage, "🏆 Claim All Quests & Battlepass Now", Color3.fromRGB(165, 115, 35), function() doAutoClaimQuestsAndPass(getReplicaData(), true) end, 5) addToggle(lobbyPage, "🚀 Smart Dynamic Step Progression (Auto Macro/Unit/Banner)", "AutoStarterCameraman", 6, function(on) if on then MacroState.ManualMacroLocked = false syncStarterMacroByCameraman(getReplicaData()) end end) -- Smart Step Pipeline Inspector & Live Editor (Lobby Tab) do local pipeBox = Instance.new("TextLabel") pipeBox.Size = UDim2.new(1, -8, 0, 134) pipeBox.BackgroundColor3 = Color3.fromRGB(22, 28, 42) pipeBox.BorderSizePixel = 0 pipeBox.Font = Enum.Font.Code pipeBox.TextSize = 11 pipeBox.TextXAlignment = Enum.TextXAlignment.Left pipeBox.TextYAlignment = Enum.TextYAlignment.Top pipeBox.TextColor3 = Color3.fromRGB(215, 242, 230) pipeBox.Text = State.FormatStepPipelineText(getReplicaData()) pipeBox.LayoutOrder = 6 pipeBox.Parent = lobbyPage UIRefs.StepPipelineLabel = pipeBox local pbc = Instance.new("UICorner") pbc.CornerRadius = UDim.new(0, 6) pbc.Parent = pipeBox local pbs = Instance.new("UIStroke") pbs.Color = Color3.fromRGB(50, 165, 115) pbs.Thickness = 1 pbs.Parent = pipeBox local pbp = Instance.new("UIPadding") pbp.PaddingLeft = UDim.new(0, 8) pbp.PaddingTop = UDim.new(0, 6) pbp.Parent = pipeBox local stepEditRow = Instance.new("Frame") stepEditRow.Size = UDim2.new(1, -8, 0, 30) stepEditRow.BackgroundColor3 = Color3.fromRGB(28, 32, 46) stepEditRow.BorderSizePixel = 0 stepEditRow.LayoutOrder = 6 stepEditRow.Parent = lobbyPage local serc = Instance.new("UICorner") serc.CornerRadius = UDim.new(0, 6) serc.Parent = stepEditRow local stepChainBox = Instance.new("TextBox") stepChainBox.Size = UDim2.new(1, -220, 0, 22) stepChainBox.Position = UDim2.new(0, 8, 0, 4) stepChainBox.BackgroundColor3 = Color3.fromRGB(18, 22, 32) stepChainBox.BorderSizePixel = 0 stepChainBox.Font = Enum.Font.Code stepChainBox.TextSize = 11 stepChainBox.TextColor3 = Color3.fromRGB(235, 242, 255) stepChainBox.PlaceholderText = "VD: cac > cac2 > cac4 > cac5 > cac6 > cac7 > cac8" stepChainBox.PlaceholderColor3 = Color3.fromRGB(120, 130, 155) stepChainBox.Text = tostring(Cfg.StepChainText or DEFAULT_CFG.StepChainText) stepChainBox.ClearTextOnFocus = false stepChainBox.Parent = stepEditRow UIRefs.StepChainBox = stepChainBox local scbc = Instance.new("UICorner") scbc.CornerRadius = UDim.new(0, 5) scbc.Parent = stepChainBox local applyBtn = Instance.new("TextButton") applyBtn.Size = UDim2.new(0, 68, 0, 22) applyBtn.Position = UDim2.new(1, -208, 0, 4) applyBtn.BackgroundColor3 = Color3.fromRGB(45, 160, 100) applyBtn.BorderSizePixel = 0 applyBtn.Font = Enum.Font.GothamBold applyBtn.TextSize = 11 applyBtn.TextColor3 = Color3.fromRGB(255, 255, 255) applyBtn.Text = "💾 Lưu Step" applyBtn.Parent = stepEditRow local apbc = Instance.new("UICorner") apbc.CornerRadius = UDim.new(0, 5) apbc.Parent = applyBtn local appendBtn = Instance.new("TextButton") appendBtn.Size = UDim2.new(0, 68, 0, 22) appendBtn.Position = UDim2.new(1, -136, 0, 4) appendBtn.BackgroundColor3 = Color3.fromRGB(55, 115, 195) appendBtn.BorderSizePixel = 0 appendBtn.Font = Enum.Font.GothamBold appendBtn.TextSize = 11 appendBtn.TextColor3 = Color3.fromRGB(255, 255, 255) appendBtn.Text = "➕ +Macro" appendBtn.Parent = stepEditRow local appbc = Instance.new("UICorner") appbc.CornerRadius = UDim.new(0, 5) appbc.Parent = appendBtn local resetStepBtn = Instance.new("TextButton") resetStepBtn.Size = UDim2.new(0, 60, 0, 22) resetStepBtn.Position = UDim2.new(1, -64, 0, 4) resetStepBtn.BackgroundColor3 = Color3.fromRGB(130, 85, 185) resetStepBtn.BorderSizePixel = 0 resetStepBtn.Font = Enum.Font.GothamBold resetStepBtn.TextSize = 11 resetStepBtn.TextColor3 = Color3.fromRGB(255, 255, 255) resetStepBtn.Text = "☁️ SyncWeb" resetStepBtn.Parent = stepEditRow local rsbc = Instance.new("UICorner") rsbc.CornerRadius = UDim.new(0, 5) rsbc.Parent = resetStepBtn trackConn(applyBtn.MouseButton1Click:Connect(function() State.ApplyStepChainNow(stepChainBox.Text) end)) trackConn(appendBtn.MouseButton1Click:Connect(function() local curChain = tostring(stepChainBox.Text or Cfg.StepChainText or ""):gsub("^%s+", ""):gsub("%s+$", "") local curMacro = tostring(Cfg.SelectedMacro or "cac") local nextChain = (curChain ~= "") and (curChain .. " > " .. curMacro) or curMacro State.ApplyStepChainNow(nextChain) end)) trackConn(resetStepBtn.MouseButton1Click:Connect(function() task.spawn(function() State.PullStepConfigFromVPS(true) end) end)) end addDropdown(lobbyPage, "Farm Money To Return & Roll (Dropdown)", "ReturnMoneyThreshold", { 100, 500, 900, 1000, 2000 }, 7) addToggle(lobbyPage, "Auto Join Solo Elevator", "AutoJoinElevator", 8) addDropdown(lobbyPage, "Preferred Map (Dropdown)", "PreferredMap", { "Toilet City", "Desert", "Cameraman HQ", "Any" }, 9) addToggle(lobbyPage, "Auto Equip Macro / Best Team", "AutoEquipBest", 10) addToggle(lobbyPage, "Auto Claim Quests", "AutoClaimQuests", 11) addToggle(lobbyPage, "Auto Claim Battlepass", "AutoClaimBattlepass", 12) addToggle(lobbyPage, "Auto Summon (Basic Crate)", "AutoSummon", 13) addDropdown(lobbyPage, "Summon Min Money (Dropdown)", "SummonMinMoney", { 100, 500, 1000, 2000, 5000 }, 14) addToggle(lobbyPage, "Auto Buy Halloween Crate", "AutoBuyHalloweenCrate", 15) addToggle(lobbyPage, "Auto Open Inventory Crates", "AutoOpenCrates", 16) addToggle(lobbyPage, "Auto Sell Duplicate Units", "AutoSellDuplicates", 17) addDropdown(lobbyPage, "Keep Copies Per Unit (Dropdown)", "KeepCopies", { 1, 2, 3, 4 }, 18) -- 5. Populate MACRO STEPS Inspector Page local stepInfoLbl = Instance.new("TextLabel") stepInfoLbl.Size = UDim2.new(1, -8, 0, 260) stepInfoLbl.BackgroundColor3 = Color3.fromRGB(24, 28, 40) stepInfoLbl.BorderSizePixel = 0 stepInfoLbl.Font = Enum.Font.Code stepInfoLbl.TextSize = 11 stepInfoLbl.TextXAlignment = Enum.TextXAlignment.Left stepInfoLbl.TextYAlignment = Enum.TextYAlignment.Top stepInfoLbl.TextColor3 = Color3.fromRGB(215, 230, 250) stepInfoLbl.Text = "No Macro Steps Recorded." stepInfoLbl.LayoutOrder = 1 stepInfoLbl.Parent = logPage UIRefs.StepInfoLabel = stepInfoLbl local sic = Instance.new("UICorner") sic.CornerRadius = UDim.new(0, 6) sic.Parent = stepInfoLbl local sip = Instance.new("UIPadding") sip.PaddingLeft = UDim.new(0, 8) sip.PaddingTop = UDim.new(0, 6) sip.Parent = stepInfoLbl Pages["Macro System"].Visible = true TabButtons["Macro System"].BackgroundColor3 = Color3.fromRGB(55, 120, 210) end -- ============================================================================ -- 10. TELEMETRY & MAIN AUTOMATION LOOP -- ============================================================================ State.LastLabelTexts = setmetatable({}, { __mode = "k" }) local function setLabelTextIfChanged(lbl, newText) if not lbl then return end if State.LastLabelTexts[lbl] ~= newText then State.LastLabelTexts[lbl] = newText pcall(function() lbl.Text = newText end) end end local function updateTelemetryUI() if UIRefs.MainFrame and not UIRefs.MainFrame.Visible then return end local rep = getReplicaData() local money = (rep and tonumber(rep.Money)) or 0 local candy = (rep and tonumber(rep.Candy)) or 0 local bannerInfo = getBannerTimerInfo() if CURRENT_PLACE == PLACE_MATCH then local wave = tonumber(workspace:GetAttribute("Wave")) or 0 local maxWaves = tonumber(workspace:GetAttribute("MaxWaves")) or 20 local mode = tostring(workspace:GetAttribute("Mode") or "Voting") local cash = getMyMatchCash() local placed = getMyPlacedTowerCount() local activeMacroTag = tostring((MacroState.IsRecording and MacroState.RecordingMacroName) or Cfg.SelectedMacro or "Default") local modeTag = MacroState.IsRecording and string.format("🔴 REC [%s]", activeMacroTag) or (Cfg.PlayMacro and string.format("▶️ PLAY [%s] (%d/%d)", activeMacroTag, math.min(MacroState.CurrentStep, #MacroState.Steps), #MacroState.Steps)) or string.format("⏸️ IDLE [%s]", activeMacroTag) setLabelTextIfChanged(UIRefs.StatsLabel, string.format( "%s | %s W:%d/%d | Cash:$%d | T:%d/15 | Money:$%d | Banner:%s", modeTag, mode, wave, maxWaves, cash, placed, money, bannerInfo.TimeFormatted )) else local placeTag = (CURRENT_PLACE == PLACE_LOBBY and "LOBBY") or (CURRENT_PLACE == PLACE_AFK and "AFK WORLD") or (CURRENT_PLACE == PLACE_TRADING and "TRADING PLAZA") or ("PLACE " .. tostring(CURRENT_PLACE)) setLabelTextIfChanged(UIRefs.StatsLabel, string.format( "%s | Macro:%s (%d) | Money:$%d | Candy:%d | Banner:%s", placeTag, Cfg.SelectedMacro, #MacroState.Steps, money, candy, bannerInfo.TimeFormatted )) end setLabelTextIfChanged(UIRefs.ActionLabel, "Status: " .. tostring(State.StatusText)) if UIRefs.BannerInfoLabel and (not UIRefs.LobbyPage or UIRefs.LobbyPage.Visible) then setLabelTextIfChanged(UIRefs.BannerInfoLabel, formatBannerInspectorText()) end if UIRefs.StepPipelineLabel and (not UIRefs.LobbyPage or UIRefs.LobbyPage.Visible) then setLabelTextIfChanged(UIRefs.StepPipelineLabel, State.FormatStepPipelineText(rep)) end if UIRefs.StepInfoLabel and (not UIRefs.LogPage or UIRefs.LogPage.Visible) then local activeMacroTag = tostring((MacroState.IsRecording and MacroState.RecordingMacroName) or Cfg.SelectedMacro or "Default") local lines = { string.format("Macro: %s%s | Total Steps: %d | Current Step: %d", activeMacroTag, MacroState.IsRecording and " [RECORDING]" or "", #MacroState.Steps, MacroState.CurrentStep), "--------------------------------------------------", } local total = #MacroState.Steps local startI = math.max(1, MacroState.CurrentStep - 3) local endI = math.min(total, startI + 12) for i = startI, endI do local s = MacroState.Steps[i] if s then local prefix = (i == MacroState.CurrentStep and Cfg.PlayMacro) and ">> " or " " table.insert(lines, string.format("%s%d. [W%d] %s", prefix, i, tonumber(s.Wave) or 0, tostring(s.Summary or s.Action))) end end if total == 0 then table.insert(lines, " (Empty - Click 'Start Recording Macro' and play 1 match!)") end setLabelTextIfChanged(UIRefs.StepInfoLabel, table.concat(lines, "\n")) end end createGUI() pcall(updateTelemetryUI) refreshMacroVisuals() task.spawn(function() if Cfg.SafetyPlatform then ensureSafetyPlatform() end if CURRENT_PLACE == PLACE_MATCH then local waitStart = os.clock() while isAlive() and (os.clock() - waitStart < 4.5) do local g = workspace:FindFirstChild("Game") if g and g:FindFirstChild("Waypoints") and g:FindFirstChild("Base") then break end task.wait(0.2) end task.wait(1.0) else task.wait(1.5) end if isAlive() then applySuperMaxLow() end end) task.spawn(function() setStatus("CTD Macro & Super Max Low Hub Ready") local lastLowTick = os.clock() while isAlive() do elevateThread() genv.__CTD_HEARTBEAT = os.clock() local ok, err = pcall(function() refreshCurrentPlace() ensureGameModules() local guiOk = false pcall(function() guiOk = (UIRefs.ScreenGui ~= nil and UIRefs.ScreenGui.Parent ~= nil) end) if not guiOk then pcall(createGUI) end if Cfg.SafetyPlatform then ensureSafetyPlatform() end updateTelemetryUI() if Cfg.SuperMaxLow and (os.clock() - lastLowTick > 15) then lastLowTick = os.clock() stepLightLowMaintenance() end if CURRENT_PLACE == PLACE_MATCH then stepMatchAutomation() elseif CURRENT_PLACE == PLACE_LOBBY then stepLobbyAutomation() else setStatus("AFK / Trading Protection Active") end end) if not ok then setStatus("Loop Err: " .. tostring(err)) end task.wait(0.25) end end) genv.CTD_Bot = { State = State, Config = Cfg, MacroState = MacroState, BannerState = BannerState, BannerVPSUrl = BANNER_VPS_URL, MacrosVPSUrl = MACROS_VPS_URL, KnownCodes = KNOWN_CODES, AutoUpdateKnownCodes = autoUpdateKnownCodes, DoAutoJoinGameGroup = doAutoJoinGameGroup, DoAutoClaimQuestsAndPass = doAutoClaimQuestsAndPass, SendMatchEndWebhook = sendMatchEndWebhook, IsUnitOnActiveBanner = isUnitOnActiveBanner, EvaluateStarterBannerSummon = evaluateStarterBannerSummon, InspectMacroMeta = State.InspectMacroMeta, GetResolvedSteps = State.GetResolvedSteps, ResolveActiveStepState = State.ResolveActiveStepState, FormatStepPipelineText = State.FormatStepPipelineText, SetStepChain = State.ApplyStepChainNow, GetBannerTimerInfo = getBannerTimerInfo, SyncLobbyBanner = syncLobbyBanner, PushBannerToVPS = pushBannerToVPS, PullBannerFromVPS = pullBannerFromVPS, SyncMacrosFromVPS = syncMacrosFromVPS, UploadMacroToVPS = uploadMacroToVPS, ApplySuperMaxLow = applySuperMaxLow, StepLightLowMaintenance = stepLightLowMaintenance, LoadMacroFromDisk = loadMacroFromDisk, SaveMacroToDisk = saveMacroToDisk, StartMacroRecording = startMacroRecording, StopMacroRecording = stopMacroRecording, } return true