-- PuckAFK | +1 Web Swing Escape | v3.0 STAGE ENGINE OVERHAUL -- PlaceId: 110668201954727 -- Rebuilt from the supplied place + remote log and using PuckUI v3.8+. -- Fast path: -- * Centralized stage scheduler: probes and farming share the same server cooldown clock. -- * Binary-search bootstrap finds the farthest claimable pad in at most ~4 probes. -- * Progress-aware next-pad checks upgrade automatically without locking to an old reward. -- * Win confirmation uses WinsPopupEvent first with replicated Wins as fallback. -- * XP, rebirths, upgrades, rewards, and world progression run independently. -- * Only free rewards are claimed; no Robux purchase prompts are fired. local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local HttpService = game:GetService("HttpService") local RunService = game:GetService("RunService") local VirtualUser = game:GetService("VirtualUser") local LocalPlayer = Players.LocalPlayer if game.PlaceId ~= 110668201954727 then warn("PuckAFK: this script is only for +1 Web Swing Escape (110668201954727).") return end local ENV = (getgenv and getgenv()) or _G if ENV.PuckWebFarm and type(ENV.PuckWebFarm.Unload) == "function" then pcall(ENV.PuckWebFarm.Unload) end ------------------------------------------------------------------------ -- PuckUI ------------------------------------------------------------------------ local PUCK_UI_URL = "https://raw.githubusercontent.com/PuckAFK/Puck-Loader/main/ui/PuckUI.lua" local function loadPuckUI() local compiler = loadstring if type(compiler) ~= "function" then return nil, "loadstring is unavailable" end local okHttp, source = pcall(function() return game:HttpGet(PUCK_UI_URL) end) if not okHttp or type(source) ~= "string" or source == "" then return nil, "failed to download PuckUI" end local chunk, compileError = compiler(source) if not chunk then return nil, "PuckUI compile error: " .. tostring(compileError) end local okRun, library = pcall(chunk) if not okRun or type(library) ~= "table" then return nil, "PuckUI load error: " .. tostring(library) end return library end local PuckUI, uiError = loadPuckUI() if not PuckUI then warn("PuckAFK:", uiError) return end ------------------------------------------------------------------------ -- Game data ------------------------------------------------------------------------ local Remotes = ReplicatedStorage:WaitForChild("Remotes", 20) local Shared = ReplicatedStorage:WaitForChild("Shared", 20) local ConfigFolder = Shared and Shared:WaitForChild("Config", 20) if not Remotes or not ConfigFolder then warn("PuckAFK: the game's remotes/config did not load.") return end local function loadModule(name) local moduleScript = ConfigFolder:FindFirstChild(name) if not moduleScript then return {} end local ok, result = pcall(require, moduleScript) if ok and type(result) == "table" then return result end return {} end local MountConfig = loadModule("MountConfig") local ProductConfig = loadModule("ProductConfig") local GameConfig = loadModule("GameConfig") local QuestConfig = loadModule("QuestConfig") local PlaytimeRewards = loadModule("PlaytimeRewards") local StageConfig = loadModule("StageConfig") local GAME_TOUCH_COOLDOWN = tonumber(StageConfig.TouchCooldown) or 1 local MIN_WIN_INTERVAL = math.max(0.20, GAME_TOUCH_COOLDOWN + 0.02) ------------------------------------------------------------------------ -- Runtime state + configuration ------------------------------------------------------------------------ local C = { Wins = true, XP = true, Rebirth = true, Suits = true, Trails = true, Auras = true, Worlds = true, Rewards = true, Freebies = true, AntiAFK = true, Stage = 0, -- 0 = auto highest proven World = 0, -- 0 = auto highest unlocked WinInterval = math.max(1.03, MIN_WIN_INTERVAL), ProbeFallback = 6.0, -- retry next locked stage even if progression signals are quiet } local S = { alive = true, enabled = false, epoch = 0, status = "Ready", loadedAt = os.clock(), connections = {}, last = {}, pendingRequests = {}, failures = {}, routeCache = {}, routeWorld = 0, routeAt = 0, -- Stage-engine state. All stage touches (farm + probes) use one scheduler. targetStage = nil, targetWorld = 0, targetReward = 0, armedWorld = 0, stageSlotAt = 0, stageBusy = false, stageMode = "bootstrap", -- bootstrap | farm | manual searchGood = 0, -- highest confirmed claimable stage index searchBad = 14, -- lowest confirmed unclaimable stage index (exclusive upper bound) maxAccessibleStage = 0, nextCandidateStage = 1, nextStageProbeAt = 0, -- fallback deadline nextStageProbeEarliestAt = 0, -- progression-trigger throttle lastProbeLevel = 0, lastProbeSpeed = 0, lastProbeAt = 0, probeCount = 0, probeSuccesses = 0, stageMisses = 0, stageAwards = 0, lastStageAttempt = nil, winPopupSerial = 0, lastWinPopupAmount = 0, lastWinPopupAt = 0, nextWinTouch = 0, -- retained for UI/backward state compatibility; mirrors stageSlotAt winAttempts = 0, attemptsSinceGain = 0, confirmedCycles = 0, confirmedWins = 0, observedWins = 0, lastWinGainAt = os.clock(), pendingSuit = nil, spendLockUntil = 0, accessoryTurn = false, lastRebirths = 0, lastWorld = 1, rebirthRetryAt = 0, questState = {}, playtimeState = {}, offlineChecked = false, groupChecked = false, inGroup = false, ui = nil, } ENV.PuckWebFarm = S local function connect(signal, callback) local connection = signal:Connect(callback) table.insert(S.connections, connection) return connection end local function active(token) return S.alive and S.enabled and (token == nil or token == S.epoch) end local function waitActive(seconds, token) local deadline = os.clock() + math.max(0, tonumber(seconds) or 0) repeat task.wait(math.min(0.05, math.max(0, deadline - os.clock()))) until not active(token) or os.clock() >= deadline return active(token) end local function due(key, seconds) local now = os.clock() local previous = S.last[key] or -math.huge if now - previous < seconds then return false end S.last[key] = now return true end local remoteCache = {} local function getRemote(name) local cached = remoteCache[name] if cached and cached.Parent then return cached end local found = Remotes:FindFirstChild(name) remoteCache[name] = found return found end local function fire(name, ...) if not active() then return false end local remote = getRemote(name) if not remote or not remote:IsA("RemoteEvent") then return false end return pcall(remote.FireServer, remote, ...) end local function request(name, callback, ...) if not active() or S.pendingRequests[name] then return false end local remote = getRemote(name) if not remote or not remote:IsA("RemoteFunction") then return false end local args = table.pack(...) local token = S.epoch S.pendingRequests[name] = true task.spawn(function() local result = table.pack(pcall(function() return remote:InvokeServer(table.unpack(args, 1, args.n)) end)) S.pendingRequests[name] = nil if not result[1] or not active(token) then return end if type(callback) == "function" then local callbackArgs = {} for i = 2, result.n do callbackArgs[#callbackArgs + 1] = result[i] end pcall(callback, table.unpack(callbackArgs)) end end) return true end local function event(name, callback) local remote = getRemote(name) if remote and remote:IsA("RemoteEvent") then return connect(remote.OnClientEvent, function(...) if S.alive then pcall(callback, ...) end end) end end ------------------------------------------------------------------------ -- Stats / character helpers ------------------------------------------------------------------------ local function statObject(name) for _, folderName in ipairs({"HiddenStats", "leaderstats"}) do local folder = LocalPlayer:FindFirstChild(folderName) local value = folder and folder:FindFirstChild(name) if value and value:IsA("ValueBase") then return value end end end local function stat(name, default) local value = statObject(name) if value then return value.Value end local attribute = LocalPlayer:GetAttribute(name) if attribute ~= nil then return attribute end return default end local function number(name) return tonumber(stat(name, 0)) or 0 end local function body() local character = LocalPlayer.Character local humanoid = character and character:FindFirstChildOfClass("Humanoid") local root = character and character:FindFirstChild("HumanoidRootPart") if character and humanoid and root and humanoid.Health > 0 then return character, humanoid, root end end local function currentWorld() return math.clamp(math.floor(tonumber(LocalPlayer:GetAttribute("World")) or 1), 1, 5) end local function worldSuffix(world) return world == 1 and "" or ("_W" .. tostring(world)) end local function invalidateRoute(reason) S.routeAt = 0 S.routeWorld = 0 table.clear(S.routeCache) S.targetStage = nil S.targetWorld = 0 S.targetReward = 0 S.armedWorld = 0 S.stageSlotAt = 0 S.nextWinTouch = 0 S.stageBusy = false S.stageMode = C.Stage > 0 and "manual" or "bootstrap" S.searchGood = 0 S.searchBad = 14 S.maxAccessibleStage = 0 S.nextCandidateStage = 1 S.nextStageProbeAt = 0 S.nextStageProbeEarliestAt = 0 S.lastProbeLevel = number("Level") S.lastProbeSpeed = number("Speed") S.lastProbeAt = 0 S.stageMisses = 0 S.attemptsSinceGain = 0 S.lastWinGainAt = os.clock() if reason then S.status = reason end end ------------------------------------------------------------------------ -- Fast movement/touch ------------------------------------------------------------------------ local function pivot(character, root, cf) character:PivotTo(cf) root.AssemblyLinearVelocity = Vector3.zero root.AssemblyAngularVelocity = Vector3.zero end local function fastTouch(part, token) if not active(token) or not part or not part:IsA("BasePart") or not part:IsDescendantOf(workspace) then return false end local character, humanoid, root = body() if not character then return false end humanoid.Sit = false local contactOffset = part.Size.Y * 0.5 + humanoid.HipHeight + root.Size.Y * 0.5 - 0.20 local contact = part.CFrame * CFrame.new(0, contactOffset, 0) local above = contact * CFrame.new(0, 2.2, 0) -- One separation frame + one contact frame gives the server a real edge, -- instead of the old pair of fixed 80 ms sleeps. pivot(character, root, above) RunService.Heartbeat:Wait() if not active(token) or not part.Parent then return false end pivot(character, root, contact) RunService.Heartbeat:Wait() if firetouchinterest and active(token) and part.Parent then pcall(firetouchinterest, root, part, 0) pcall(firetouchinterest, root, part, 1) end return active(token) end -- The server emits the exact awarded amount through this event. Tracking it -- lets better-pad probes confirm almost immediately without waiting on stat UI -- replication, and also gives us the real payout when StageConfig is unusual. event("WinsPopupEvent", function(amount) local value = tonumber(amount) if value and value > 0 then S.winPopupSerial = (S.winPopupSerial or 0) + 1 S.lastWinPopupAmount = value S.lastWinPopupAt = os.clock() S.stageAwards = (S.stageAwards or 0) + 1 -- Popup confirmation is earlier and more reliable than waiting for the -- replicated Wins value. Use it to keep route-health logic accurate. S.lastWinGainAt = os.clock() S.attemptsSinceGain = 0 end end) ------------------------------------------------------------------------ -- Stage routing / centralized stage engine ------------------------------------------------------------------------ -- The supplied place exposes exactly 13 stage rewards in each world and a -- one-second StageConfig.TouchCooldown. We still read the live module first, -- because that keeps the script compatible if the game updates the values. local FALLBACK_REWARDS = { [1] = {1, 3, 8, 21, 58, 160, 450, 1250, 3400, 9500, 26000, 72000, 200000}, [2] = {1e6, 3e6, 1e7, 3e7, 1e8, 3e8, 1e9, 3e9, 1e10, 3e10, 1e11, 3e11, 1e12}, [3] = {5e12, 1.3e13, 3.3e13, 8.4e13, 2.15e14, 5.5e14, 1.4e15, 3.6e15, 9.3e15, 2.4e16, 6.1e16, 1.56e17, 4e17}, [4] = {2e18, 6e18, 1.8e19, 5.3e19, 1.6e2, 4.75e2, 1.4e21, 4.2e21, 1.26e22, 3.75e22, 1.12e23, 3.35e23, 1e24}, [5] = {5e24, 1.27e25, 3.2e25, 8.1e25, 2.06e26, 5.22e26, 1.32e27, 3.35e27, 8.5e27, 2.15e28, 5.45e28, 1.38e29, 3.5e29}, } local function stageRewardTable(world) local live if world <= 1 then live = StageConfig.StageRewards else live = StageConfig["W" .. tostring(world) .. "StageRewards"] end return type(live) == "table" and live or FALLBACK_REWARDS[world] or {} end local function numericReward(raw) if type(raw) == "number" then return raw elseif type(raw) == "string" then return tonumber(raw) or 0 elseif type(raw) == "table" then for _, key in ipairs({"Wins", "Win", "Reward", "Amount", "Value"}) do local value = tonumber(raw[key]) if value then return value end end end return 0 end local function rewardForStage(rewards, index, stageFolder) local reward = numericReward(rewards[index]) if reward <= 0 then reward = numericReward(rewards[tostring(index)]) end if reward <= 0 and stageFolder then for _, name in ipairs({"Wins", "WinReward", "Reward", "Amount"}) do reward = numericReward(stageFolder:GetAttribute(name)) if reward > 0 then break end end end return reward end local function getStages(force) local world = currentWorld() if not force and world == S.routeWorld and os.clock() - S.routeAt < 4 and #S.routeCache > 0 then return S.routeCache end S.routeWorld = world S.routeAt = os.clock() S.routeCache = {} local folder = workspace:FindFirstChild("StageWinPaths" .. worldSuffix(world)) local normal = folder and folder:FindFirstChild("Normal") local rewards = stageRewardTable(world) if normal then for _, stageFolder in ipairs(normal:GetChildren()) do local index = tonumber(stageFolder.Name) local part = stageFolder:FindFirstChild("Hitbox") or stageFolder:FindFirstChild("Win") or (stageFolder:IsA("BasePart") and stageFolder) if index and part and part:IsA("BasePart") then table.insert(S.routeCache, { n = index, part = part, reward = rewardForStage(rewards, index, stageFolder), }) end end end table.sort(S.routeCache, function(a, b) return a.n < b.n end) -- Search bounds follow the actual replicated stage count, not a hardcoded -- stage name. StageConfig currently has 13 entries in every world. local count = #S.routeCache if count > 0 and S.stageMode == "bootstrap" then -- Clamp the existing binary-search upper bound; never reset progress -- just because the route cache refreshed during the scan. S.searchBad = math.min( math.max((S.searchGood or 0) + 1, S.searchBad or (count + 1)), count + 1 ) end return S.routeCache end local function entryByStage(index) index = tonumber(index) if not index then return nil end for _, entry in ipairs(getStages()) do if entry.n == index and entry.part and entry.part.Parent then return entry end end return nil end local function bestRewardEntryUpTo(maxIndex) local best for _, entry in ipairs(getStages()) do if entry.n <= maxIndex then if not best or (tonumber(entry.reward) or 0) > (tonumber(best.reward) or 0) or ((tonumber(entry.reward) or 0) == (tonumber(best.reward) or 0) and entry.n > best.n) then best = entry end end end return best end local function setTarget(entry, reason) if not entry then return end S.targetStage = entry.n S.targetWorld = currentWorld() S.targetReward = tonumber(entry.reward) or 0 S.stageMisses = 0 S.attemptsSinceGain = 0 if reason then S.status = reason end end local function stageEntryPart(world) local map = workspace:FindFirstChild("Map" .. worldSuffix(world)) local entry = map and map:FindFirstChild("StageEntry") return entry and ( entry:FindFirstChild("MainGround") or entry:FindFirstChildWhichIsA("BasePart", true) ) end local function armWorld(token, force) local world = currentWorld() if not force and S.armedWorld == world then return true end local entry = stageEntryPart(world) if entry and entry:IsA("BasePart") then S.status = "Arming world " .. world .. " route" fastTouch(entry, token) waitActive(0.08, token) end S.armedWorld = world return active(token) end local function stageInterval() return math.max(MIN_WIN_INTERVAL, tonumber(C.WinInterval) or MIN_WIN_INTERVAL) end local function waitForStageSlot(token) while active(token) do local remaining = (S.stageSlotAt or 0) - os.clock() if remaining <= 0 then return true end if not waitActive(math.min(0.04, remaining), token) then return false end end return false end -- Exactly one function owns the stage-pad cooldown. A probe can never race a -- normal farm touch anymore, which was the main failure mode in v2.1. local function touchStage(entry, token, purpose) if not entry or not entry.part or not entry.part.Parent or not active(token) then return false, nil end if S.stageBusy or not waitForStageSlot(token) then return false, nil end S.stageBusy = true local started = os.clock() local beforeWins = number("Wins") local beforePopupSerial = S.winPopupSerial or 0 S.lastStageAttempt = { world = currentWorld(), stage = entry.n, reward = tonumber(entry.reward) or 0, purpose = purpose or "farm", started = started, } local touched = fastTouch(entry.part, token) S.winAttempts = S.winAttempts + (touched and 1 or 0) -- The slot is reserved from attempt START, exactly like the server debounce. S.stageSlotAt = math.max(os.clock() + 0.01, started + stageInterval()) S.nextWinTouch = S.stageSlotAt S.stageBusy = false if not touched then return false, {success = false, gain = 0} end -- Normal farming is asynchronous. Probe touches use the otherwise-idle -- remainder of this exact server cooldown window for a definite verdict. if purpose == "farm" then return true, nil end local deadline = math.max(os.clock() + 0.05, S.stageSlotAt - 0.03) local gain = 0 repeat local popupChanged = (S.winPopupSerial or 0) > beforePopupSerial and (S.lastWinPopupAt or 0) >= started - 0.01 if popupChanged then gain = tonumber(S.lastWinPopupAmount) or 0 if gain > 0 then return true, {success = true, gain = gain, source = "popup"} end end local nowWins = number("Wins") if nowWins > beforeWins then gain = nowWins - beforeWins return true, {success = true, gain = gain, source = "wins"} end if not waitActive(0.02, token) then return false, {success = false, gain = 0} end until os.clock() >= deadline return true, {success = false, gain = 0} end local function bootstrapCandidate() local stages = getStages() if #stages == 0 then return nil end local good = math.max(0, math.floor(S.searchGood or 0)) local bad = math.min(#stages + 1, math.floor(S.searchBad or (#stages + 1))) if bad - good <= 1 then return nil end -- Upper midpoint biases the search toward farther pads while still finding -- the farthest monotonic claimable stage in <= ceil(log2(14)) probes. local index = math.floor((good + bad + 1) / 2) index = math.clamp(index, 1, #stages) return entryByStage(index) end local function finishBootstrap() S.maxAccessibleStage = math.max(0, S.searchGood or 0) S.nextCandidateStage = S.maxAccessibleStage + 1 S.stageMode = C.Stage > 0 and "manual" or "farm" local best = bestRewardEntryUpTo(S.maxAccessibleStage) if best then setTarget( best, string.format( "Best pad found • Stage %d • %.4g wins", best.n, tonumber(best.reward) or 0 ) ) else S.targetStage = nil S.targetReward = 0 S.status = "No claimable win pad found yet" end S.lastProbeLevel = number("Level") S.lastProbeSpeed = number("Speed") S.lastProbeAt = os.clock() S.nextStageProbeEarliestAt = os.clock() + 1.50 S.nextStageProbeAt = os.clock() + C.ProbeFallback end local function runBootstrapStep(token) if C.Stage > 0 then S.stageMode = "manual" local selected = entryByStage(C.Stage) if selected then setTarget(selected, "Manual stage " .. C.Stage) else S.status = "Manual stage " .. C.Stage .. " not found" end return selected end local candidate = bootstrapCandidate() if not candidate then finishBootstrap() return entryByStage(S.targetStage) end armWorld(token, false) if not active(token) then return nil end S.probeCount = (S.probeCount or 0) + 1 S.status = string.format( "Finding max pad • testing Stage %d • %.4g wins", candidate.n, tonumber(candidate.reward) or 0 ) local _, verdict = touchStage(candidate, token, "bootstrap") if not active(token) then return nil end if verdict and verdict.success then S.searchGood = math.max(S.searchGood or 0, candidate.n) S.probeSuccesses = (S.probeSuccesses or 0) + 1 S.maxAccessibleStage = math.max(S.maxAccessibleStage or 0, candidate.n) local best = bestRewardEntryUpTo(S.searchGood) if best then setTarget(best) end else S.searchBad = math.min(S.searchBad or (#getStages() + 1), candidate.n) end if (S.searchBad or 14) - (S.searchGood or 0) <= 1 then finishBootstrap() end return entryByStage(S.targetStage) end local function progressionChangedEnough() local level = number("Level") local speed = number("Speed") local lastLevel = tonumber(S.lastProbeLevel) or 0 local lastSpeed = tonumber(S.lastProbeSpeed) or 0 if level >= lastLevel + 1 then return true end if lastSpeed <= 0 then return speed > 0 end return speed >= lastSpeed * 1.04 end local function shouldProbeNextStage() if C.Stage > 0 or S.stageMode ~= "farm" then return false end local candidate = entryByStage(S.nextCandidateStage) if not candidate then return false end local now = os.clock() if now >= (S.nextStageProbeAt or 0) then return true end -- A rapidly increasing Level must not turn every legal farm slot into a -- failed next-pad probe. Progression can accelerate a recheck only after -- this small minimum spacing; the fallback deadline still guarantees a -- retry even when the exposed Speed/Level stats are quiet. return now >= (S.nextStageProbeEarliestAt or 0) and progressionChangedEnough() end local function probeNextStage(token) local candidate = entryByStage(S.nextCandidateStage) if not candidate then return false end S.probeCount = (S.probeCount or 0) + 1 S.status = string.format( "Checking farther pad • Stage %d • %.4g wins", candidate.n, tonumber(candidate.reward) or 0 ) local _, verdict = touchStage(candidate, token, "upgrade") if not active(token) then return true end S.lastProbeLevel = number("Level") S.lastProbeSpeed = number("Speed") S.lastProbeAt = os.clock() if verdict and verdict.success then S.probeSuccesses = (S.probeSuccesses or 0) + 1 S.maxAccessibleStage = math.max(S.maxAccessibleStage or 0, candidate.n) S.searchGood = math.max(S.searchGood or 0, candidate.n) S.nextCandidateStage = candidate.n + 1 -- Choose by PAYOUT among every now-known-accessible stage rather -- than assuming stage names are the reward source of truth. local best = bestRewardEntryUpTo(S.maxAccessibleStage) if best then local oldStage = S.targetStage local oldReward = tonumber(S.targetReward) or 0 setTarget(best) if best.n ~= oldStage or (tonumber(best.reward) or 0) > oldReward then S.status = string.format( "PAD UPGRADED • Stage %d • %.4g wins", best.n, tonumber(best.reward) or 0 ) else S.status = string.format( "Stage %d reachable • keeping better payout Stage %d", candidate.n, best.n ) end end -- A success means the next farther stage might already work, so test it -- at the very next legal slot instead of waiting several seconds. S.nextStageProbeEarliestAt = S.stageSlotAt S.nextStageProbeAt = S.stageSlotAt else S.nextStageProbeEarliestAt = os.clock() + 1.50 S.nextStageProbeAt = os.clock() + C.ProbeFallback S.status = string.format( "Stage %d not ready • farming Stage %s", candidate.n, tostring(S.targetStage or "?") ) end return true end local function selectedStageEntry() if C.Stage > 0 then return entryByStage(C.Stage) end return entryByStage(S.targetStage) end local function farmWins(token) if not C.Wins then S.status = C.XP and "Training XP" or "Autofarm enabled" waitActive(0.08, token) return end if #getStages() == 0 then S.status = "Waiting for stage hitboxes" waitActive(0.20, token) return end if C.Stage == 0 and S.stageMode == "bootstrap" then runBootstrapStep(token) return end if C.Stage > 0 and S.stageMode ~= "manual" then S.stageMode = "manual" elseif C.Stage == 0 and S.stageMode == "manual" then invalidateRoute("Auto stage enabled • recalculating best pad") return end if shouldProbeNextStage() then probeNextStage(token) return end local entry = selectedStageEntry() if not entry then if C.Stage > 0 then S.status = "Selected stage " .. C.Stage .. " is unavailable" waitActive(0.20, token) else invalidateRoute("Lost stage target • recalculating") end return end if not waitForStageSlot(token) then return end S.status = string.format( "FAST FARM • W%d Stage %d • %.4g wins • %.2fs", currentWorld(), entry.n, tonumber(entry.reward) or 0, stageInterval() ) local touched = touchStage(entry, token, "farm") if touched then S.attemptsSinceGain = S.attemptsSinceGain + 1 end -- Only invalidate after several legal, cooldown-separated attempts fail. -- A single delayed stat replication can no longer destroy a good route. local staleFor = os.clock() - (S.lastWinGainAt or os.clock()) if S.attemptsSinceGain >= 5 and staleFor >= math.max(5.0, stageInterval() * 4.5) then S.stageMisses = (S.stageMisses or 0) + 1 invalidateRoute("Pad stopped paying • rebuilding stage route") end end ------------------------------------------------------------------------ -- World progression ------------------------------------------------------------------------ local function desiredWorld() if C.World > 0 then return math.clamp(C.World, 1, 5) end if not C.Worlds then return currentWorld() end return math.clamp(1 + math.floor(number("Rebirths") / 6), 1, 5) end local function tryWorldTravel(token) local current = currentWorld() local target = desiredWorld() if target == current then return false end local requiredRebirths = (target - 1) * 6 if number("Rebirths") < requiredRebirths then S.status = "World " .. target .. " locked • need " .. requiredRebirths .. " rebirths" return false end if not due("worldTravel", 1.25) then return false end S.status = "Travelling to world " .. target fire("World" .. target .. "Teleport") waitActive(0.22, token) if currentWorld() ~= current then invalidateRoute("World changed • relearning route") end return true end ------------------------------------------------------------------------ -- Suit / mount upgrades ------------------------------------------------------------------------ local function owned(kind) local result = {} local name = "Owned" .. tostring(kind) -- Mounts are replicated as a Folder directly under the Player. local folder = LocalPlayer:FindFirstChild(name) if folder then for _, item in ipairs(folder:GetChildren()) do result[tostring(item.Name)] = true end end -- Trails/Auras are replicated by this game as JSON StringValues inside -- HiddenStats (OwnedTrails / OwnedAuras). Support either representation. local hidden = LocalPlayer:FindFirstChild("HiddenStats") local valueObject = hidden and hidden:FindFirstChild(name) if valueObject and valueObject:IsA("ValueBase") then local raw = valueObject.Value if type(raw) == "string" and raw ~= "" then local ok, decoded = pcall(HttpService.JSONDecode, HttpService, raw) if ok and type(decoded) == "table" then for key, value in pairs(decoded) do if type(key) == "number" then result[tostring(value)] = true elseif value == true then result[tostring(key)] = true end end end end end return result end local function getMount(id) if type(MountConfig.GetById) == "function" then local ok, value = pcall(MountConfig.GetById, tostring(id)) if ok and value then return value end end return {} end local function mountIdForPad(world, index) if world <= 1 then return tostring(index) end local fn = MountConfig["W" .. world .. "IdForPad"] if type(fn) == "function" then local ok, value = pcall(fn, index) if ok and value ~= nil then return tostring(value) end end end local function planSuit() if not C.Suits then return nil end local world = currentWorld() local folder = workspace:FindFirstChild("SpeedPads" .. worldSuffix(world)) if not folder then return nil end local has = owned("Mounts") local currentId = tostring(stat("MountId", "")) if currentId ~= "" then has[currentId] = true end local currentPower = tonumber(getMount(currentId).XpPerSecond) or 0 local ids = {} for id in pairs(has) do ids[#ids + 1] = id end local best = nil local bestPower = currentPower local wins = number("Wins") for _, pad in ipairs(folder:GetChildren()) do if pad:IsA("BasePart") then local index = tonumber(pad.Name:match("^P(%d+)$")) local id = index and mountIdForPad(world, index) if id then local data = getMount(id) local power = tonumber(data.XpPerSecond) or 0 local canUse = has[id] == true if not canUse then local cost = tonumber(data.Cost) if cost and cost <= wins and type(MountConfig.CanBuyMount) == "function" then local ok, allowed = pcall(MountConfig.CanBuyMount, id, {OwnedMounts = ids}) canUse = ok and allowed == true end end if canUse and power > bestPower and os.clock() >= (S.failures["suit:" .. id] or 0) then best = {id = id, pad = pad, power = power} bestPower = power end end end end return best end local function trySuit(token) if not C.Suits or os.clock() < S.spendLockUntil then return false end if S.pendingSuit then local id = S.pendingSuit.id local isOwned = owned("Mounts")[id] == true local equipped = tostring(stat("MountId", "")) == id if isOwned or equipped then S.pendingSuit = nil elseif os.clock() - S.pendingSuit.at < 0.85 then return false else S.failures["suit:" .. id] = os.clock() + 3 S.pendingSuit = nil end end if not due("suitScan", 0.35) then return false end local best = planSuit() if not best then return false end S.status = "Upgrading suit • " .. best.id S.pendingSuit = {id = best.id, at = os.clock()} S.spendLockUntil = os.clock() + 0.20 fastTouch(best.pad, token) return true end ------------------------------------------------------------------------ -- Trail / aura upgrades ------------------------------------------------------------------------ local function planAccessory(kind) local entries = ProductConfig[kind .. "s"] if type(entries) ~= "table" then return nil end local current = tostring(stat(kind .. "Id", "")) local currentData = entries[current] local currentPower = currentData and tonumber(currentData.SpeedMultiplier) or 1 local has = owned(kind .. "s") if current ~= "" then has[current] = true end local wins = number("Wins") local bestId = nil local bestPower = currentPower for id, data in pairs(entries) do local power = tonumber(data.SpeedMultiplier) or 1 local cost = tonumber(data.Wins) local canUse = has[tostring(id)] == true or (cost and cost <= wins) if canUse and power > bestPower and os.clock() >= (S.failures[kind .. ":" .. tostring(id)] or 0) then bestId = tostring(id) bestPower = power end end if bestId then return bestId, bestPower end end local function tryAccessory(kind, enabled) if not enabled or os.clock() < S.spendLockUntil then return false end local key = "accessory:" .. kind if not due(key, 0.65) then return false end local id = planAccessory(kind) if not id then return false end S.status = "Buying/equipping " .. string.lower(kind) .. " • " .. id S.spendLockUntil = os.clock() + 0.28 local ok = fire(kind .. "EquipRequest", id) if ok then -- Short retry guard; the next scan re-evaluates live equipped/owned data. S.failures[kind .. ":" .. id] = os.clock() + 0.75 end return ok end ------------------------------------------------------------------------ -- XP + rebirth ------------------------------------------------------------------------ local function requiredLevelForRebirth() if type(GameConfig.RequiredLevelForRebirth) == "function" then local ok, value = pcall(GameConfig.RequiredLevelForRebirth, number("Rebirths")) if ok and tonumber(value) then return tonumber(value) end end return number("Rebirths") * 20 + 10 end local function progressionWorker() while S.alive do if active() then local token = S.epoch local ok, err = pcall(function() local character, humanoid = body() -- The stock JumpXpClient sends about every 0.5s while actually -- moving. Teleport farming normally has MoveDirection == 0, so -- pulse the same zero-argument event only when stock movement -- is not already doing it. if character and C.XP and due("xpPulse", 0.505) then if humanoid.MoveDirection.Magnitude < 0.001 then fire("JumpXpEvent") end end if character and C.Rebirth and due("rebirthCheck", 0.10) then local now = os.clock() local needed = requiredLevelForRebirth() if number("Level") >= needed and now >= (S.rebirthRetryAt or 0) then S.status = "Rebirthing at level " .. math.floor(number("Level")) S.spendLockUntil = now + 0.35 -- One request, then give replication time to confirm it. -- Retry only if the server still has not advanced rebirths. if fire("RebirthButtonEvent") then S.rebirthRetryAt = now + 0.80 else S.rebirthRetryAt = now + 0.20 end end end if due("accessoryTurn", 0.30) then S.accessoryTurn = not S.accessoryTurn if S.accessoryTurn then tryAccessory("Trail", C.Trails) else tryAccessory("Aura", C.Auras) end end end) if not ok and due("progressionError", 5) then warn("PuckAFK progression:", err) end if not active(token) then task.wait(0.05) end else task.wait(0.10) end task.wait(0.025) end end ------------------------------------------------------------------------ -- Rewards ------------------------------------------------------------------------ local function listHasClaim(container, id) if type(container) ~= "table" then return false end return table.find(container, id) ~= nil or container[id] == true or container[tostring(id)] == true end local QUEST_TRACKS = { Daily = { Speed = "SpeedToday", Wins = "WinsToday", Playtime = "PlaytimeToday", }, Weekly = { Speed = "SpeedThisWeek", Wins = "WinsThisWeek", Playtime = "PlaytimeThisWeek", }, Event = { Speed = "SpeedToday", Wins = "WinsToday", CriminalKills = "CriminalKills", }, Race = { RaceWins = "RaceWins", RaceTop3 = "RaceTop3", RaceFinished = "RaceFinished", }, } local function claimQuests() if not active() or not C.Rewards then return end local state = S.questState if type(state) ~= "table" then return end for _, category in ipairs({"Daily", "Weekly", "Event", "Race"}) do local claimed = state[category .. "Claimed"] or {} local mapping = QUEST_TRACKS[category] or {} for _, quest in ipairs(QuestConfig[category] or {}) do local progressKey = mapping[quest.Track] or quest.Track local progress = tonumber(state[progressKey]) or 0 local goal = tonumber(quest.Goal) if goal and progress >= goal and not listHasClaim(claimed, quest.Id) and due("quest:" .. tostring(quest.Id), 4) then fire("QuestClaimRequest", quest.Id) end end end end local function claimPlaytime(state) if not active() or not C.Rewards or type(state) ~= "table" then return end S.playtimeState = state local played = tonumber(state.Playtime) or 0 local claimed = state.Claimed or {} for _, tier in ipairs(PlaytimeRewards.Tiers or {}) do if played >= (tonumber(tier.Seconds) or math.huge) and not listHasClaim(claimed, tier.Id) and due("playtime:" .. tostring(tier.Id), 4) then fire("PlaytimeClaimRequest", tier.Id) end end end event("QuestProgressUpdate", function(value) if type(value) == "table" then S.questState = value claimQuests() end end) event("PlaytimeUpdate", function(value) if type(value) == "table" then claimPlaytime(value) end end) local function checkGroupReward() if not C.Freebies or LocalPlayer:GetAttribute("GroupSpeedClaimed") == true then return end if not S.groupChecked then S.groupChecked = true task.spawn(function() local groupId = tonumber(GameConfig.GroupId) if not groupId or groupId <= 0 then return end local ok, inGroup = pcall(function() return LocalPlayer:IsInGroup(groupId) end) S.inGroup = ok and inGroup == true end) return end -- Never fire the request for non-members; this avoids causing the stock -- client to open its group-join prompt. if S.inGroup and due("groupReward", 10) then fire("RequestGroupSpeedAdd") end end local function rewardWorker() while S.alive do if active() then local ok, err = pcall(function() claimQuests() if C.Rewards and due("rewardRefresh", 8) then request("QuestDataRequest", function(value) if type(value) == "table" then S.questState = value claimQuests() end end) request("DailyRewardState", function(value) if C.Rewards and type(value) == "table" and value.CanClaim == true then fire("ClaimDailyReward") end end) request("PlaytimeDataRequest", function(value) claimPlaytime(value) end) end if C.Freebies then if not S.offlineChecked then S.offlineChecked = true request("GetOfflineEarnings", function(amount) if C.Freebies and (tonumber(amount) or 0) > 0 then -- Free 1x offline claim only. Never Claimx10. fire("ClaimOffline") end end) end if workspace.DistributedGameTime >= 61 and LocalPlayer:GetAttribute("LeaveRewardClaimed") ~= true and due("dontLeave", 8) then fire("ClaimDontLeaveWin") end checkGroupReward() end end) if not ok and due("rewardError", 8) then warn("PuckAFK rewards:", err) end else task.wait(0.10) end task.wait(0.10) end end ------------------------------------------------------------------------ -- Monitors ------------------------------------------------------------------------ local function monitorWorker() S.observedWins = number("Wins") S.lastRebirths = number("Rebirths") S.lastWorld = currentWorld() while S.alive do if active() then local wins = number("Wins") if wins ~= S.observedWins then if wins > S.observedWins then local gain = wins - S.observedWins S.confirmedWins = S.confirmedWins + gain S.confirmedCycles = S.confirmedCycles + 1 S.lastWinGainAt = os.clock() S.attemptsSinceGain = 0 else -- Rebirth/reset: do not interpret the decrease as a route failure. S.lastWinGainAt = os.clock() S.attemptsSinceGain = 0 end S.observedWins = wins end local rebirths = number("Rebirths") if rebirths ~= S.lastRebirths then S.lastRebirths = rebirths S.rebirthRetryAt = 0 S.spendLockUntil = os.clock() + 0.25 invalidateRoute("Rebirth confirmed • refreshing route") end local world = currentWorld() if world ~= S.lastWorld then S.lastWorld = world invalidateRoute("World " .. world .. " loaded • finding best stage") end end task.wait(0.04) end end ------------------------------------------------------------------------ -- Main movement worker ------------------------------------------------------------------------ local function movementWorker() while S.alive do if active() then local token = S.epoch local ok, err = pcall(function() if not body() then S.status = "Waiting for respawn" if due("respawn", 1.5) then fire("ReviveRespawnEvent") end waitActive(0.15, token) return end if tryWorldTravel(token) then return end -- One movement action per pass. Suit pads are prioritized because -- better suits immediately accelerate XP/rebirth progression. if trySuit(token) then return end farmWins(token) end) if not ok then S.status = "Recovered from farm error" if due("movementError", 3) then warn("PuckAFK movement:", err) end task.wait(0.10) end else task.wait(0.10) end task.wait(0.01) end end ------------------------------------------------------------------------ -- Anti-AFK / lifecycle ------------------------------------------------------------------------ connect(LocalPlayer.Idled, function() if not S.alive or not C.AntiAFK then return end pcall(function() VirtualUser:CaptureController() VirtualUser:ClickButton2(Vector2.new(0, 0)) end) end) connect(LocalPlayer.CharacterAdded, function() S.epoch = S.epoch + 1 invalidateRoute("Respawned • restoring autofarm") end) connect(LocalPlayer:GetAttributeChangedSignal("World"), function() if S.alive then invalidateRoute("World changed • refreshing route") end end) ------------------------------------------------------------------------ -- PuckUI ------------------------------------------------------------------------ local Window = PuckUI:CreateWindow({ Name = "PuckAFK | +1 Web Swing Escape", GuiName = "PuckAFK_WebSwingEscape", Width = 560, Height = 600, ConfigId = "WebSwingEscape", Configs = { DefaultProfile = "default", AutoSave = true, AutoLoad = true, }, }) S.ui = Window local FarmTab = Window:CreateTab("Farm") local UpgradeTab = Window:CreateTab("Upgrades") local RewardsTab = Window:CreateTab("Rewards") local SettingsTab = Window:CreateTab("Settings") FarmTab:CreateSection("Autofarm") local StatusParagraph = FarmTab:CreateParagraph({ Title = "Status", Content = "Ready", Height = 88, }) local function setEnabled(value) local enabled = value == true if S.enabled == enabled then return end S.enabled = enabled S.epoch = S.epoch + 1 S.nextWinTouch = 0 S.stageSlotAt = 0 S.lastWinGainAt = os.clock() S.attemptsSinceGain = 0 if enabled then table.clear(S.failures) S.status = "Starting fast autofarm" S.observedWins = number("Wins") S.lastRebirths = number("Rebirths") S.lastWorld = currentWorld() S.offlineChecked = false S.groupChecked = false S.inGroup = false invalidateRoute("Finding best stage") PuckUI:Notify({ Title = "PuckAFK", Content = "Fast autofarm enabled", Duration = 2, }) else S.status = "Stopped" S.pendingSuit = nil local _, humanoid = body() if humanoid then humanoid:Move(Vector3.zero) end PuckUI:Notify({ Title = "PuckAFK", Content = "Autofarm stopped", Duration = 2, }) end end FarmTab:CreateToggle({ Name = "Auto Farm", CurrentValue = false, Flag = "AutoFarm", Callback = setEnabled, }) FarmTab:CreateToggle({ Name = "Farm Stage Wins", CurrentValue = C.Wins, Flag = "FarmWins", Callback = function(value) C.Wins = value == true invalidateRoute("Stage farming setting changed") end, }) FarmTab:CreateToggle({ Name = "Train XP", CurrentValue = C.XP, Flag = "TrainXP", Callback = function(value) C.XP = value == true end, }) FarmTab:CreateToggle({ Name = "Auto Rebirth", CurrentValue = C.Rebirth, Flag = "AutoRebirth", Callback = function(value) C.Rebirth = value == true end, }) FarmTab:CreateSection("Route") local stageOptions = {"Auto • best reachable payout"} do -- Build the manual list from StageConfig itself so the UI is correct even -- when it opens before StageWinPaths has finished replicating. local rewards = stageRewardTable(currentWorld()) local count = math.max(#rewards, 13) for index = 1, count do local reward = numericReward(rewards[index]) stageOptions[#stageOptions + 1] = reward > 0 and string.format("Stage %d • %.4g wins", index, reward) or ("Stage " .. index) end end FarmTab:CreateDropdown({ Name = "Stage", Options = stageOptions, CurrentOption = stageOptions[1], Flag = "Stage", Callback = function(value) local selected = type(value) == "table" and value[1] or value local index = tonumber(tostring(selected):match("(%d+)")) C.Stage = index or 0 table.clear(S.failures) invalidateRoute("Stage selection changed") end, }) FarmTab:CreateSlider({ Name = "Win Interval", Range = {MIN_WIN_INTERVAL, math.max(1.50, MIN_WIN_INTERVAL)}, Increment = 0.01, CurrentValue = C.WinInterval, Suffix = "s", Flag = "WinInterval", Callback = function(value) C.WinInterval = math.max(MIN_WIN_INTERVAL, tonumber(value) or C.WinInterval) S.nextWinTouch = 0 S.stageSlotAt = 0 end, }) FarmTab:CreateSlider({ Name = "Locked Pad Fallback Recheck", Range = {3, 15}, Increment = 1, CurrentValue = C.ProbeFallback, Suffix = "s", Flag = "ProbeFallback", Callback = function(value) C.ProbeFallback = math.clamp(tonumber(value) or 6, 3, 15) S.nextStageProbeAt = math.min(S.nextStageProbeAt or math.huge, os.clock() + C.ProbeFallback) end, }) FarmTab:CreateLabel( string.format( "Game touch cooldown: %.2fs • minimum interval: %.2fs", GAME_TOUCH_COOLDOWN, MIN_WIN_INTERVAL ) ) FarmTab:CreateLabel("Auto bootstrap uses a monotonic binary scan: ~4 legal pad touches for 13 stages.") FarmTab:CreateButton({ Name = "Test Next Farther Pad Now", Callback = function() S.nextStageProbeAt = 0 S.lastProbeLevel = -math.huge S.lastProbeSpeed = 0 S.status = "Next farther pad test queued" end, }) FarmTab:CreateButton({ Name = "Full Re-scan Best Stage", Callback = function() table.clear(S.failures) invalidateRoute("Manual full stage scan") end, }) UpgradeTab:CreateSection("Progression") UpgradeTab:CreateToggle({ Name = "Buy / Equip Better Suits", CurrentValue = C.Suits, Flag = "AutoSuits", Callback = function(value) C.Suits = value == true S.pendingSuit = nil table.clear(S.failures) end, }) UpgradeTab:CreateToggle({ Name = "Buy / Equip Better Trails", CurrentValue = C.Trails, Flag = "AutoTrails", Callback = function(value) C.Trails = value == true end, }) UpgradeTab:CreateToggle({ Name = "Buy / Equip Better Auras", CurrentValue = C.Auras, Flag = "AutoAuras", Callback = function(value) C.Auras = value == true end, }) UpgradeTab:CreateToggle({ Name = "Advance Unlocked Worlds", CurrentValue = C.Worlds, Flag = "AutoWorlds", Callback = function(value) C.Worlds = value == true end, }) local worldOptions = {"Auto • highest unlocked", "World 1", "World 2", "World 3", "World 4", "World 5"} UpgradeTab:CreateDropdown({ Name = "World", Options = worldOptions, CurrentOption = worldOptions[1], Flag = "World", Callback = function(value) local selected = type(value) == "table" and value[1] or value local index = tonumber(tostring(selected):match("(%d+)")) C.World = index or 0 invalidateRoute("World selection changed") end, }) UpgradeTab:CreateLabel("World unlocks follow rebirth progression: W2 6 • W3 12 • W4 18 • W5 24.") UpgradeTab:CreateButton({ Name = "Retry Purchases Now", Callback = function() S.pendingSuit = nil S.spendLockUntil = 0 for key in pairs(S.failures) do if tostring(key):find("suit:", 1, true) or tostring(key):find("Trail:", 1, true) or tostring(key):find("Aura:", 1, true) then S.failures[key] = nil end end S.status = "Purchase retries cleared" end, }) RewardsTab:CreateSection("Free Progress") RewardsTab:CreateToggle({ Name = "Claim Quests / Daily / Playtime", CurrentValue = C.Rewards, Flag = "AutoRewards", Callback = function(value) C.Rewards = value == true end, }) RewardsTab:CreateToggle({ Name = "Claim Free Offline / Stay / Group Rewards", CurrentValue = C.Freebies, Flag = "AutoFreebies", Callback = function(value) C.Freebies = value == true if value then S.offlineChecked = false S.groupChecked = false end end, }) RewardsTab:CreateParagraph({ Title = "Safety", Content = "Only free reward remotes are used. The paid 10x offline reward / Robux purchase path is never fired.", Height = 58, }) RewardsTab:CreateButton({ Name = "Refresh Reward State", Callback = function() S.last.rewardRefresh = -math.huge S.offlineChecked = false S.status = "Reward refresh queued" end, }) SettingsTab:CreateSection("Script") SettingsTab:CreateToggle({ Name = "Anti-AFK", CurrentValue = C.AntiAFK, Flag = "AntiAFK", Callback = function(value) C.AntiAFK = value == true end, }) SettingsTab:CreateParagraph({ Title = "Fast Mode", Content = "Stage Engine v3 uses one shared cooldown clock for farming and probes. It binary-searches the farthest reachable pad, farms the best payout, then tests the next farther pad only on a legal server touch slot.", Height = 68, }) SettingsTab:CreateButton({ Name = "Unload PuckAFK Script", Callback = function() if S.Unload then S.Unload() end end, }) local unloading = false function S.Unload() if unloading then return end unloading = true S.alive = false S.enabled = false S.epoch = S.epoch + 1 for _, connection in ipairs(S.connections) do pcall(function() connection:Disconnect() end) end table.clear(S.connections) if Window and type(Window.Destroy) == "function" then pcall(function() Window:Destroy() end) elseif Window and Window.ScreenGui then pcall(function() Window.ScreenGui:Destroy() end) end if ENV.PuckWebFarm == S then ENV.PuckWebFarm = nil end end Window.CloseCallback = S.Unload ------------------------------------------------------------------------ -- Live status ------------------------------------------------------------------------ task.spawn(function() while S.alive do local level = number("Level") local rebirths = number("Rebirths") local wins = number("Wins") local stage = S.targetStage or (C.Stage > 0 and C.Stage) or "?" local world = currentWorld() local reward = tonumber(S.targetReward) or 0 local nextStage = C.Stage > 0 and "-" or tostring(S.nextCandidateStage or "?") local content = string.format( "%s\nW%d • Stage %s • Reward %.4g • Reachable ≤ %d\nLevel %.0f • Rebirths %.0f • Speed %.4g\nWins %.4g • Gains %.4g • Attempts %d • Next %s", S.status, world, tostring(stage), reward, tonumber(S.maxAccessibleStage) or 0, level, rebirths, number("Speed"), wins, S.confirmedWins, S.winAttempts, nextStage ) pcall(function() StatusParagraph:Set({ Title = S.enabled and "Autofarm • RUNNING" or "Autofarm • STOPPED", Content = content, }) end) task.wait(0.20) end end) ------------------------------------------------------------------------ -- Start workers ------------------------------------------------------------------------ task.spawn(movementWorker) task.spawn(progressionWorker) task.spawn(rewardWorker) task.spawn(monitorWorker) PuckUI:Notify({ Title = "PuckAFK", Content = "Web Swing Escape Stage Engine v3 loaded • PuckUI " .. tostring(PuckUI.Version or ""), Duration = 3, })