-- PuckAFK | Drain Water Per Click v1.6 HYPER -- Built from place 103883942725157 and the supplied September 7 remote log. -- Client dump contains no authoritative server source. Live-game testing is still required. -- PuckUI supplies shared Settings, config profiles, autosave and autoload. if not game:IsLoaded() then game.Loaded:Wait() end if game.PlaceId ~= 103883942725157 then error("PuckAFK: this build targets the main Drain Water Per Click place (103883942725157).") end local ENV = (getgenv and getgenv()) or _G if ENV.PuckDrain and ENV.PuckDrain.Stop then pcall(ENV.PuckDrain.Stop) end local Players = game:GetService("Players") local RS = game:GetService("ReplicatedStorage") local WS = game:GetService("Workspace") local TeleportService = game:GetService("TeleportService") local GuiService = game:GetService("GuiService") local Player = Players.LocalPlayer local State = {alive = true, connections = {}, pending = {}, cache = {}, stages = {}, cooldowns = {}, status = "Ready", errors = {}, sent = 0, harvestStage = nil, activeDrainStage = nil, clickBase = 100000000 + math.floor((os.clock() % 100000) * 1000), clickSeq = 0, clickAccepted = 0, clickRejected = 0, questTargets = {}, eventState = nil, ticketState = nil, fishShowState = nil, petData = nil, ranking = nil, titleState = nil} ENV.PuckDrain = State local Opt = {Farm = false, Click = true, Collect = true, Sell = true, Pumps = true, Auras = true, Backpack = true, SpeedUpgrade = true, FishDisplayUpgrade = true, Pets = true, PetMerge = true, Rewards = true, PassiveRewards = true, BestDisplay = true, Rebirth = true, AntiAFK = true, AutoReconnect = true, QuestFish = true, IndexOverride = false, CPS = 20, SmartCPS = true, SellAt = 100, Lock = "OFF", Mode = "Progress stages", Stage = 1, DeepFirst = true, CheapFilter = true, MinRarity = "Rare", ValueCutoff = 45, MinFishCash = 0, AlwaysSpecial = true, HarvestSeconds = 1, FastProgress = true, HyperProgress = true, DirectSell = true, PowerReserve = 75} local function child(parent, name) return parent and parent:FindFirstChild(name) end local function value(folder, name, default) local obj = child(child(Player, folder), name) return (obj and tonumber(obj.Value)) or default or 0 end local function character() local c = Player.Character local h = c and c:FindFirstChildOfClass("Humanoid") local r = child(c, "HumanoidRootPart") if h and r and h.Health > 0 then return c, h, r end end local function connect(signal, fn) local c = signal:Connect(fn); table.insert(State.connections, c); return c end local function err(key, detail) State.errors[key] = tostring(detail) warn("[PuckAFK Drain] " .. key .. ": " .. tostring(detail)) end local function remote(kind, folder, name) return child(child(child(child(RS, "Remote"), kind), folder), name) end local function fire(folder, name, ...) if not State.alive then return false end local r = remote("Event", folder, name) if not r then State.errors[folder .. name] = "Remote unavailable"; return false end local ok, why = pcall(r.FireServer, r, ...) if not ok then err(folder .. name, why) end return ok end -- One outstanding invocation per remote, including after a timeout. No growing retry queue. local function invoke(folder, name, ...) local key = folder .. name if not State.alive or State.pending[key] then return nil end local r = remote("Function", folder, name) if not r then State.errors[key] = "Remote unavailable"; return nil end local args = table.pack(...) local job = {done = false}; State.pending[key] = job task.spawn(function() local ok, result = pcall(r.InvokeServer, r, table.unpack(args, 1, args.n)) job.ok, job.result, job.done = ok, result, true State.pending[key] = nil if not ok and State.alive then err(key, result) end end) local untilTime = os.clock() + 3.5 repeat task.wait(0.02) until job.done or not State.alive or os.clock() >= untilTime if job.done and job.ok and State.alive then return job.result end if not job.done then State.errors[key] = "Server response timeout" end end local function ready(key, seconds) local now = os.clock() if now < (State.cooldowns[key] or 0) then return false end State.cooldowns[key] = now + seconds; return true end local function helper(name) local m = child(child(RS, "Config"), name) if not m then error("Missing game module: " .. name) end return require(m) end local ok, modules = pcall(function() return {Pump = helper("PumpHelper"), Aura = helper("AuraHelper"), Upgrade = helper("UpgradeHelper"), Stage = helper("StageHelper"), Fish = helper("FishHelper"), Training = helper("TrainingAreaHelper"), Rarity = helper("RarityHelper"), World = helper("WorldConfig")} end) if not ok then State.alive = false; error("PuckAFK startup: " .. tostring(modules)) end local H = modules local uiOK, UI = pcall(function() return loadstring(game:HttpGet("https://raw.githubusercontent.com/PuckAFK/Puck-Loader/main/ui/PuckUI.lua"))() end) if not uiOK or type(UI) ~= "table" then State.alive = false; error("PuckUI failed to load: " .. tostring(UI)) end local Window = UI:CreateWindow({Name = "PuckAFK | Drain Water Per Click", GuiName = "PuckAFK_DrainWater", ConfigId = "DrainWaterPerClick", Width = 540, Height = 620}) local Main = Window:CreateTab("Autofarm") local FishTab = Window:CreateTab("Fish") local Shop = Window:CreateTab("Upgrades") local PetTab = Window:CreateTab("Pets") local RewardTab = Window:CreateTab("Rewards") local EventTab = Window:CreateTab("Event") local ProgressTab = Window:CreateTab("Progress") local Settings = Window:CreateTab("Settings") local StatusLabel = Main:CreateLabel("Ready — enable Start autofarm") local StatsLabel = Main:CreateLabel("Waiting for player data") local DetailLabel = Main:CreateLabel("v1.6 HYPER | event-driven progression + direct selling") local function toggle(tab, name, key, noConfig) return tab:CreateToggle({Name = name, CurrentValue = Opt[key], Flag = key == "Rebirth" and "Rebirth_v12" or key, NoConfig = noConfig, Callback = function(v) Opt[key] = v end}) end toggle(Main, "Start autofarm", "Farm", true) Main:CreateDropdown({Name = "Farm mode", Options = {"Progress stages", "Selected stage", "Train water only"}, CurrentOption = {Opt.Mode}, Flag = "FarmMode", Callback = function(v) Opt.Mode = type(v) == "table" and v[1] or v end}) local stageIDs = H.Stage.GetStageIds() Main:CreateSlider({Name = "Selected stage", Range = {1, stageIDs[#stageIDs] or 21}, Increment = 1, CurrentValue = 1, Flag = "SelectedStage", Callback = function(v) Opt.Stage = v end}) toggle(Main, "Auto click", "Click") Main:CreateSlider({Name = "Clicks per second", Range = {1, 20}, Increment = 1, CurrentValue = Opt.CPS, Flag = "CPS", Callback = function(v) Opt.CPS = v end}) toggle(Main, "Smart click sequence / recovery", "SmartCPS") Main:CreateParagraph({Title = "Core progression", Content = "Turbo mode clicks and drains continuously while affordable upgrades are purchased in the background. Only seller/rebirth/shop-fallback movement may pause stage movement."}) toggle(FishTab, "Collect exposed valuable fish", "Collect") toggle(FishTab, "Depth-first (ignore old shallow fish)", "DeepFirst") toggle(FishTab, "Skip cheap fish", "CheapFilter") local pickupRarities = H.Rarity.GetOrderedRarities() FishTab:CreateDropdown({Name = "Minimum pickup rarity", Options = pickupRarities, CurrentOption = {Opt.MinRarity}, Flag = "MinPickupRarity", Callback = function(v) Opt.MinRarity = type(v) == "table" and v[1] or v end}) FishTab:CreateSlider({Name = "Value cutoff (% of best fish)", Range = {0, 95}, Increment = 5, CurrentValue = Opt.ValueCutoff, Flag = "FishValueCutoff", Callback = function(v) Opt.ValueCutoff = v end}) FishTab:CreateSlider({Name = "Absolute minimum fish cash", Range = {0, 5000}, Increment = 25, CurrentValue = Opt.MinFishCash, Flag = "MinFishCash", Callback = function(v) Opt.MinFishCash = v end}) FishTab:CreateSlider({Name = "Harvest window after clearing stage", Range = {1, 10}, Increment = 1, CurrentValue = Opt.HarvestSeconds, Flag = "HarvestSeconds", Callback = function(v) Opt.HarvestSeconds = v end}) toggle(FishTab, "Always grab special fish", "AlwaysSpecial") toggle(FishTab, "Quest-target fish bypass value filter", "QuestFish") toggle(FishTab, "Collect new fish index entries even if cheap", "IndexOverride") toggle(FishTab, "Auto place best fish in display", "BestDisplay") FishTab:CreateParagraph({Title = "Value rules", Content = "Cheap-fish filtering is strict by default. Special fish and enabled quest targets may bypass it. Index override is OFF by default so one-time collection progress never silently fills your bag with cheap fish."}) FishTab:CreateButton({Name = "Put best fish in display now", Callback = function() task.spawn(function() local result = invoke("FishShow", "[C-S]BestFishUI") if type(result) == "table" and type(result.state) == "table" then State.fishShowState = result.state end end) end}) toggle(FishTab, "Auto sell carried fish", "Sell") FishTab:CreateSlider({Name = "Sell at bag %", Range = {25, 100}, Increment = 5, CurrentValue = 100, Flag = "SellAt", Callback = function(v) Opt.SellAt = v end}) FishTab:CreateDropdown({Name = "Protect this rarity and above", Options = H.Rarity.GetSellLockOptions(), CurrentOption = {"OFF"}, Flag = "RarityLock", Callback = function(v) Opt.Lock = type(v) == "table" and v[1] or v end}) FishTab:CreateParagraph({Title = "Selling", Content = "Returns home to bank carried fish, opens the real seller prompt, sells eligible stored fish, verifies cash/inventory change, then resumes the exact progression state."}) toggle(Shop, "Buy and equip better cash pumps", "Pumps") toggle(Shop, "Buy and equip better cash auras", "Auras") toggle(Shop, "Buy backpack upgrades", "Backpack") toggle(Shop, "Buy speed upgrades", "SpeedUpgrade") toggle(Shop, "Buy fish display upgrades", "FishDisplayUpgrade") toggle(Shop, "Turbo progression / chain-buy", "FastProgress") toggle(Shop, "HYPER event-driven progression", "HyperProgress") toggle(Shop, "Direct sell before seller fallback", "DirectSell") Shop:CreateSlider({Name = "Save for next pump/aura (%)", Range = {0, 95}, Increment = 5, CurrentValue = Opt.PowerReserve, Flag = "PowerReserve", Callback = function(v) Opt.PowerReserve = v end}) Shop:CreateParagraph({Title = "HYPER progression", Content = "Cash/state events wake the buyer immediately. Power upgrades compound first, cheap backpack levels reduce seller trips, low-impact upgrades wait when a pump/aura is close, and seller/shop travel is only a fallback when the direct server action is not confirmed."}) toggle(PetTab, "Equip best owned pets", "Pets") toggle(PetTab, "Merge duplicate pets automatically", "PetMerge") local PetLabel = PetTab:CreateLabel("Pets: waiting for data") PetTab:CreateParagraph({Title = "Pet loop", Content = "Merge All and Equip Best use the exact zero-argument remotes captured from the game. After merging, best pets are equipped again automatically."}) PetTab:CreateButton({Name = "Merge pets now", Callback = function() fire("Pet", "MergeAll"); task.delay(0.35, function() if State.alive then fire("Pet", "EquipBest") end end) end}) PetTab:CreateButton({Name = "Equip best pets now", Callback = function() fire("Pet", "EquipBest") end}) toggle(RewardTab, "Claim daily rewards and free daily spin", "Rewards") toggle(RewardTab, "Claim fish display timed rewards", "PassiveRewards") toggle(RewardTab, "Auto rebirth when level requirement is met", "Rebirth") RewardTab:CreateParagraph({Title = "Rewards", Content = "Claims normal daily sign rewards, the free daily spin when available, both timed fish-display cash rewards, and rebirths only after carried fish are banked."}) RewardTab:CreateButton({Name = "Claim fish display rewards now", Callback = function() task.spawn(function() local a = invoke("FishShow", "[C-S]ClaimBestFishReward") local b = invoke("FishShow", "[C-S]ClaimBestValueFishReward") UI:Notify({Title = "Display rewards", Content = "Best fish: " .. tostring(type(a) == "table" and a.amount or "not ready") .. " | Best value: " .. tostring(type(b) == "table" and b.amount or "not ready"), Duration = 5}) end) end}) local EventLabel = EventTab:CreateLabel("Event: waiting for Cthulhu state") local TicketLabel = EventTab:CreateLabel("Daily tickets: waiting for state") EventTab:CreateParagraph({Title = "Event safety", Content = "The capture proves event quest/ticket state updates but not their claim-action argument signatures. This build farms named fish targets automatically and shows claimable state; it does not guess unknown claim remotes."}) EventTab:CreateButton({Name = "Print detected event remotes (F9)", Callback = function() for _, folderName in ipairs({"CthulhuQuest", "CthulhuDraw"}) do for _, kind in ipairs({"Event", "Function"}) do local folder = child(child(child(RS, "Remote"), kind), folderName) if folder then for _, r in ipairs(folder:GetChildren()) do print("[PuckAFK Event Remote]", kind, folderName, r.Name, r.ClassName) end end end end end}) local ProgressLabel = ProgressTab:CreateLabel("Progress: waiting for ranking/title data") local IndexLabel = ProgressTab:CreateLabel("Fish index: waiting for fish data") ProgressTab:CreateParagraph({Title = "Tracked progression", Content = "Tracks fish index completion/multiplier, ranking bests, title unlock progress, fish display slots, upgrades, pets, stages, and rebirth state without adding unsupported write calls."}) ProgressTab:CreateButton({Name = "Refresh progression now", Callback = function() task.spawn(function() local ranking = invoke("Ranking", "GetYourBestAll"); if type(ranking) == "table" then State.ranking = ranking end local title = invoke("Title", "[C-S]GetState"); if type(title) == "table" then State.titleState = title end local fs = invoke("FishShow", "[C-S]GetUIState"); if type(fs) == "table" then State.fishShowState = fs end local fish = invoke("Fish", "[C-S]GetFishData"); if type(fish) == "table" and type(fish.Items) == "table" then State.storage = fish end end) end}) toggle(Settings, "Anti AFK", "AntiAFK") toggle(Settings, "Auto reconnect on disconnect", "AutoReconnect") local function scene() return child(child(WS, "主场景"), "验证场景") end local function completed(id) local a = Player:GetAttribute("StageCompleted_" .. tostring(id)) if a ~= nil then return a == true end return State.stages[id] and State.stages[id].completed == true end local function setStage(s) if type(s) ~= "table" then return end local id = tonumber(s.stageId) if not id then return end local previous = State.stages[id] local wasCompleted = previous and previous.completed == true State.stages[id] = s if s.completed == true and not wasCompleted and State.activeDrainStage == id and Opt.Farm and Opt.Mode ~= "Train water only" then State.harvestStage = id State.harvestReadyAt = os.clock() + 0.08 State.harvestDeadline = os.clock() + math.clamp(tonumber(Opt.HarvestSeconds) or 1, 1, 10) State.purchaseWake = true State.purchaseWakeAt = 0 elseif s.completed ~= true and State.harvestStage == id then State.harvestStage = nil end end for _, spec in ipairs({{"Pump", "[S-C]PumpDataChanged"}, {"Aura", "[S-C]AuraDataChanged"}, {"Upgrade", "[S-C]UpgradeDataChanged"}}) do local folder = spec[1] local r = remote("Event", folder, spec[2]) if r then connect(r.OnClientEvent, function(s) if type(s) == "table" then State.cache[folder] = s end State.purchaseWake = true State.purchaseWakeAt = 0 end) end end local stageEvent = remote("Event", "Stage", "[S-C]StageStateChanged") if stageEvent then connect(stageEvent.OnClientEvent, setStage) end local function rebuildQuestTargets(state) local targets = {} if type(state) == "table" then for _, bucket in ipairs({state.rotating, state.event}) do if type(bucket) == "table" then for _, quest in pairs(bucket) do if type(quest) == "table" and quest.claimed ~= true then local progress = tonumber(quest.rawProgress or quest.progress) or 0 local target = tonumber(quest.rawTarget or quest.target) or math.huge local text = tostring(quest.text or "") if progress < target then local fish = text:match("[Cc]atch%s+[%d,]+%s+(.+)") if fish then fish = fish:gsub("%s+in%s+[Cc]thulhu%s+[Ss]tages.*$", "") fish = fish:gsub("^%s+", ""):gsub("%s+$", "") local low = string.lower(fish) if low ~= "" and low ~= "fish" then targets[low] = true end end end end end end end end State.questTargets = targets end local cthulhuStateEvent = remote("Event", "CthulhuQuest", "[S-C]StateChanged") if cthulhuStateEvent then connect(cthulhuStateEvent.OnClientEvent, function(data) if type(data) == "table" then State.eventState = data; rebuildQuestTargets(data) end end) end local ticketEvent = remote("Event", "CthulhuDraw", "[S-C]DailyTicketStateChanged") if ticketEvent then connect(ticketEvent.OnClientEvent, function(data) if type(data) == "table" then State.ticketState = data end end) end local function claimDisplayRewardFast(name, stateKey, amountKey) if not State.alive or not Opt.Farm or not Opt.PassiveRewards or State[stateKey] == false or State.pending["FishShow" .. name] then return end task.spawn(function() local result = invoke("FishShow", name) if type(result) == "table" and result.success == true then State[amountKey] = result.amount State[stateKey] = false State.purchaseWake = true State.purchaseWakeAt = 0 end end) end local bestFishReadyEvent = remote("Event", "FishShow", "[S-C]BestFishRewardReady") if bestFishReadyEvent then connect(bestFishReadyEvent.OnClientEvent, function(v) State.bestFishRewardReady = v ~= false if State.bestFishRewardReady then claimDisplayRewardFast("[C-S]ClaimBestFishReward", "bestFishRewardReady", "lastBestFishReward") end end) end local bestValueReadyEvent = remote("Event", "FishShow", "[S-C]BestValueFishRewardReady") if bestValueReadyEvent then connect(bestValueReadyEvent.OnClientEvent, function(v) State.bestValueRewardReady = v ~= false if State.bestValueRewardReady then claimDisplayRewardFast("[C-S]ClaimBestValueFishReward", "bestValueRewardReady", "lastBestValueReward") end end) end local clickResultEvent = remote("Event", "Level", "[S-C]ClickResult") if clickResultEvent then connect(clickResultEvent.OnClientEvent, function(success, amount, sequence) local seq = tonumber(sequence) if seq then State.clickSeq = math.max(State.clickSeq or 0, seq) end if success == true then State.clickAccepted = (State.clickAccepted or 0) + 1 State.lastClickAmount = tonumber(amount) or State.lastClickAmount State.clickPenaltyUntil = nil else State.clickRejected = (State.clickRejected or 0) + 1 State.clickPenaltyUntil = os.clock() + 1 end end) end local function moveTo(pos, tolerance) local c, h, r = character() if not c or not Opt.Farm then return false end if (r.Position - pos).Magnitude > (tolerance or 4) then h.Sit = false r.CFrame = CFrame.new(pos) * r.CFrame.Rotation r.AssemblyLinearVelocity = Vector3.zero r.AssemblyAngularVelocity = Vector3.zero return false end return true end local function bestTrainingArea() local rebirths = value("Rebirth", "rebirth") if State.trainingCache and State.trainingCache.rebirths == rebirths and State.trainingCache.part and State.trainingCache.part.Parent then return State.trainingCache.part, State.trainingCache.multiplier end local areas = child(WS, "ExerciseArea") local best, multi = nil, 0 for _, cfg in pairs(H.Training.GetAllTrainingAreaConfig()) do if cfg.rebirthRequired ~= nil and rebirths >= cfg.rebirthRequired then local p = child(areas, tostring(cfg.id)) if p and p:IsA("BasePart") and (tonumber(cfg.multiplier) or 0) > multi then best, multi = p, tonumber(cfg.multiplier) or 0 end end end State.trainingCache = {rebirths = rebirths, part = best, multiplier = multi} return best, multi end local function train() local best, multi = bestTrainingArea() if best then moveTo(best.Position, 1.5) State.status = "FAST training | " .. tostring(multi) .. "x best unlocked area" else State.status = "Training area not loaded; clicking in place" end end -- CarryFishChanged is the run backpack; FishDataChanged is banked inventory. -- Selling banked fish from a water stage cannot empty the run backpack. local function acceptCarry(data) if type(data) == "table" and type(data.Items) == "table" then State.carry = data end end local function acceptStorage(data) if type(data) == "table" and type(data.Items) == "table" then State.storage = data end end for _, spec in ipairs({{"[S-C]CarryFishChanged", acceptCarry}, {"[S-C]FishDataChanged", acceptStorage}}) do local r = remote("Event", "Fish", spec[1]) if r then connect(r.OnClientEvent, spec[2]) end end local function bag() if State.carry then return tonumber(State.carry.Count) or #State.carry.Items, tonumber(State.carry.Capacity) or value("BackpackData", "capacity") end return value("BackpackData", "amount"), value("BackpackData", "capacity") end local function sellerPrompt() local point = child(child(WS, "UIOPEN"), "sell") local prompt = point and point:FindFirstChild("OpenSell", true) if prompt and prompt:IsA("ProximityPrompt") then local parent = prompt.Parent if parent:IsA("Attachment") then return prompt, parent.WorldPosition end if parent:IsA("BasePart") then return prompt, parent.Position end end end local function saleActive(job) return State.alive and Opt.Farm and Opt.Sell and State.saleFlow == job and character() ~= nil end local function finishSale(message, retry) State.saleReason = message State.saleFlow = nil State.forceSell = false State.sellAfter = os.clock() + (retry and 0.8 or 0.03) State.status = message or "Sale verified; resuming farm" end local function startSale() if State.saleFlow or os.clock() < (State.sellAfter or 0) then return end State.saleFlow = {phase = "home", started = os.clock(), attempts = 0, directAttempts = 0} State.saleReason = nil end local function sell() local job = State.saleFlow if not job or not saleActive(job) then return end if os.clock() - job.started > 12 then finishSale("Sale timed out; retrying shortly (F9 diagnostics)", true) return end if os.clock() < (job.at or 0) then return end if job.phase == "home" then State.status = "FAST sell: banking carried fish" fire("Stage", "[C-S]StandingWater", nil) fire("Fish", "[C-S]DisplayFish", nil) fire("Level", "[C-S]ReturnHome") job.homeAt = os.clock() job.phase, job.at = "bank", os.clock() + 0.08 return end if job.phase == "bank" then local count = bag() if count > 0 then if os.clock() - (job.homeAt or 0) < 0.45 then State.status = "FAST sell: waiting for " .. count .. " fish to bank" job.at = os.clock() + 0.05 return end local carry = invoke("Fish", "[C-S]GetCarryFishData") if not saleActive(job) then return end acceptCarry(carry) count = bag() if count > 0 then fire("Fish", "[C-S]DisplayFish", nil) fire("Level", "[C-S]ReturnHome") job.homeAt = os.clock() job.at = os.clock() + 0.08 return end end job.phase, job.at = Opt.DirectSell and "directSale" or "sellerTravel", 0 return end if job.phase == "directSale" then job.directAttempts = (job.directAttempts or 0) + 1 State.status = "FAST sell: direct SellAllFish" local result = invoke("Fish", "[C-S]SellAllFish", Opt.Lock, false) if not saleActive(job) then return end job.result = result State.lastSaleResult = result if type(result) == "table" and result.success == true then State.lastVerifiedSale = os.time() State.purchaseWake = true State.purchaseWakeAt = 0 finishSale(string.format("FAST sold %s fish for %.3g", tostring(result.soldCount or "?"), tonumber(result.amount) or 0), false) return end local reason = type(result) == "table" and tostring(result.reason or "") or "no response" if reason == "AllFishRarityLocked" or reason == "AllFishProtected" or reason == "OnlyPreciousFish" or reason == "FavoriteFish" or reason == "FishLocked" or reason == "FishDisplayed" then finishSale("Bag banked; protected fish kept (" .. reason .. ")", false) return end -- If direct selling is position/UI-gated, fall back to the real seller immediately. State.saleReason = reason ~= "" and reason or "direct sale unconfirmed" job.phase, job.at = "sellerTravel", 0 return end local prompt, position = sellerPrompt() if not prompt then State.status = "FAST sell fallback: waiting for seller" job.at = os.clock() + 0.08 return end local _, _, root = character() local range = math.max(1, math.min(5, prompt.MaxActivationDistance - 1)) if (root.Position - position).Magnitude > range then State.status = "FAST sell fallback: moving to seller" moveTo(position + Vector3.new(0, math.min(1, range / 3), 0), 0.5) job.at = os.clock() + 0.05 return end if job.phase == "sellerTravel" then if not prompt.Enabled then job.at = os.clock() + 0.05; return end if fireproximityprompt then local good, why = pcall(fireproximityprompt, prompt, prompt.HoldDuration) if not good then State.errors.SellerPrompt = tostring(why) end else prompt:InputHoldBegin() local untilTime = os.clock() + prompt.HoldDuration + 0.04 while saleActive(job) and prompt.Parent and os.clock() < untilTime do task.wait(0.02) end pcall(function() prompt:InputHoldEnd() end) end job.phase, job.at = "sellerSale", os.clock() + 0.05 return end if job.phase == "sellerSale" then job.attempts = (job.attempts or 0) + 1 State.status = "FAST sell fallback: selling" local result = invoke("Fish", "[C-S]SellAllFish", Opt.Lock, false) if not saleActive(job) then return end job.result = result State.lastSaleResult = result if type(result) == "table" and result.success == true then State.lastVerifiedSale = os.time() State.purchaseWake = true State.purchaseWakeAt = 0 finishSale(string.format("Sold %s fish for %.3g", tostring(result.soldCount or "?"), tonumber(result.amount) or 0), false) return end local reason = type(result) == "table" and tostring(result.reason or "") or "no response" if reason == "AllFishRarityLocked" or reason == "AllFishProtected" or reason == "OnlyPreciousFish" or reason == "FavoriteFish" or reason == "FishLocked" or reason == "FishDisplayed" then finishSale("Bag banked; protected fish kept (" .. reason .. ")", false) return end if job.attempts >= 2 then finishSale("Sale rejected: " .. tostring(reason), true) else job.at = os.clock() + 0.12 end end end FishTab:CreateButton({Name = "Sell now / retry seller", Callback = function() if not Opt.Farm or not Opt.Sell then UI:Notify({Title = "Selling", Content = "Enable Start autofarm and Auto sell first.", Duration = 4}) return end State.sellAfter = 0; State.forceSell = true end}) local fishCooldown = setmetatable({}, {__mode = "k"}) local targetFish, targetSince local function nextDrainStage() local id if Opt.Mode == "Selected stage" then id = Opt.Stage else for _, n in ipairs(stageIDs) do if not completed(n) then id = n; break end end end if not id or completed(id) then return nil end -- The server unlock path is sequential: never skip an unfinished earlier stage. for _, n in ipairs(stageIDs) do if n < id and not completed(n) then return n end end return id end local function deepestCompletedStage() local best for _, id in ipairs(stageIDs) do if completed(id) then best = id end end return best end local function fishPrice(fish) local price = tonumber(fish:GetAttribute("Price")) if price and price > 0 then return price end local fishId = tonumber(fish:GetAttribute("FishId")) if fishId and H.Fish and H.Fish.GetBasePrice then local okPrice, base = pcall(H.Fish.GetBasePrice, fishId) if okPrice then return math.max(0, tonumber(base) or 0) end end return 0 end local function resolveFishName(fish) local attr = fish:GetAttribute("FishName") or fish:GetAttribute("Name") if type(attr) == "string" and attr ~= "" then return attr end local fishId = tonumber(fish:GetAttribute("FishId")) if fishId and H.Fish and H.Fish.GetFishName then local okName, resolved = pcall(H.Fish.GetFishName, fishId) if okName and type(resolved) == "string" and resolved ~= "" then return resolved end end return fish.Name end local function questFishMatch(name, mutation) if not Opt.QuestFish or type(State.questTargets) ~= "table" then return false end local hay = string.lower(tostring(name or "") .. " " .. tostring(mutation or "")) for target in pairs(State.questTargets) do local core = target:gsub("%s+fish$", "") if core ~= "" and (hay:find(target, 1, true) or hay:find(core, 1, true)) then return true end end return false end local function isNewIndexFish(fish) if not Opt.IndexOverride then return false end local id = tonumber(fish:GetAttribute("FishId")) local collected = State.storage and State.storage.Collected if not id or type(collected) ~= "table" then return false end return collected[id] ~= true and collected[tostring(id)] ~= true end local function currentCollectionStage() if Opt.Mode == "Selected stage" then return completed(Opt.Stage) and Opt.Stage or nil end if not Opt.DeepFirst then return nil end if State.harvestStage and completed(State.harvestStage) then return State.harvestStage end if nextDrainStage() then return false end -- scan only high-priority overrides while a deeper stage remains. return deepestCompletedStage() end local function collect() if not Opt.Collect or State.saleFlow then return false end local count, capacity = bag() if capacity <= 0 or count >= capacity then return false end local _, _, root = character(); if not root then return false end local fishes = child(scene(), "WorldFish"); if not fishes then return false end local exactStage = currentCollectionStage() local overrideOnly = exactStage == false if overrideOnly then exactStage = nil end if State.harvestStage and exactStage == State.harvestStage and os.clock() < (State.harvestReadyAt or 0) then State.status = "Stage " .. exactStage .. " cleared | exposing valuable fish before going deeper" return true end local rows = {} local bestRegularPrice = 0 local minRank = H.Rarity.GetRank(Opt.MinRarity) or 2 local stageSeen, promptPending = 0, false for _, fish in ipairs(fishes:GetChildren()) do local id = tonumber(fish:GetAttribute("StageId")) local rootPart = child(fish, "FishRoot") local prompt = child(rootPart, "PickupPrompt") local stageAllowed if exactStage then stageAllowed = id == exactStage else stageAllowed = (fish:GetAttribute("FishRain") == true) or (id and completed(id)) if Opt.Mode == "Selected stage" then stageAllowed = id == Opt.Stage end end if fish:GetAttribute("WorldFish") == true and fish:GetAttribute("Claimed") ~= true and stageAllowed and rootPart and prompt and prompt:IsA("ProximityPrompt") then stageSeen = stageSeen + 1 local rarity = tostring(fish:GetAttribute("Rarity") or "Common") local rank = H.Rarity.GetRank(rarity) or 0 local price = fishPrice(fish) local special = fish:GetAttribute("Special") == true local mutation = tostring(fish:GetAttribute("Mutation") or "Normal") local name = resolveFishName(fish) local quest = questFishMatch(name, mutation) local newIndex = isNewIndexFish(fish) local rarityOK = rank >= minRank if not special and not quest and not newIndex and rarityOK and price > bestRegularPrice then bestRegularPrice = price end table.insert(rows, { fish = fish, part = rootPart, prompt = prompt, id = id, rarity = rarity, rank = rank, price = price, special = special, quest = quest, newIndex = newIndex, rarityOK = rarityOK, name = name, mutation = mutation, distance = (root.Position - rootPart.Position).Magnitude, depthY = tonumber(fish:GetAttribute("FishBottomY")) or rootPart.Position.Y, }) end end local cutoff = Opt.CheapFilter and math.clamp(tonumber(Opt.ValueCutoff) or 0, 0, 95) / 100 or 0 local threshold = math.max(bestRegularPrice * cutoff, math.max(0, tonumber(Opt.MinFishCash) or 0)) local best, skipped = nil, 0 local function better(a, b) if not b then return true end if a.special ~= b.special then return a.special end if a.quest ~= b.quest then return a.quest end if a.newIndex ~= b.newIndex then return a.newIndex end if a.price ~= b.price then return a.price > b.price end if a.rank ~= b.rank then return a.rank > b.rank end if (a.id or 0) ~= (b.id or 0) then return (a.id or 0) > (b.id or 0) end if a.depthY ~= b.depthY then return a.depthY < b.depthY end return a.distance < b.distance end for _, row in ipairs(rows) do local qualifies = false if row.special and Opt.AlwaysSpecial then qualifies = true elseif row.quest and Opt.QuestFish then qualifies = true elseif row.newIndex and Opt.IndexOverride then qualifies = true elseif not overrideOnly then if not Opt.CheapFilter then qualifies = true else qualifies = row.rarityOK and row.price >= threshold end end if qualifies then if row.prompt.Enabled and os.clock() >= (fishCooldown[row.fish] or 0) then if better(row, best) then best = row end elseif not row.prompt.Enabled then promptPending = true end else skipped = skipped + 1 end end if not best then targetFish = nil if exactStage and State.harvestStage == exactStage then if promptPending and os.clock() < (State.harvestDeadline or 0) then State.status = "Stage " .. exactStage .. " cleared | waiting for valuable fish prompts" return true end State.harvestStage = nil State.harvestReadyAt, State.harvestDeadline = nil, nil State.status = "Stage " .. exactStage .. " harvest done | skipped " .. skipped .. " cheap fish | going deeper" elseif exactStage and stageSeen > 0 then State.status = "Deepest stage " .. exactStage .. " | no fish pass value filter (" .. skipped .. " skipped)" end return false end local fish = best.fish if fish ~= targetFish then targetFish, targetSince = fish, os.clock() end if os.clock() - targetSince > 1.5 then fishCooldown[fish] = os.clock() + 15 targetFish = nil return false end local reason = best.special and "SPECIAL" or best.quest and "QUEST" or best.newIndex and "INDEX" or "VALUE" State.status = string.format("Collecting [%s] %s | %s | %.3g cash | stage %d", reason, tostring(best.name), best.rarity, best.price, best.id or 0) local prompt = best.prompt if moveTo(best.part.Position + Vector3.new(0, 2, 0), math.max(2, math.min(5, prompt.MaxActivationDistance - 1))) then if ready("pickup", math.max(0.12, prompt.HoldDuration + 0.04)) then if fireproximityprompt then local good, why = pcall(fireproximityprompt, prompt, prompt.HoldDuration) if not good then State.errors.Pickup = tostring(why) end else prompt:InputHoldBegin() local pickupStart = os.clock() while State.alive and Opt.Farm and prompt.Parent and os.clock() - pickupStart < prompt.HoldDuration + 0.1 do task.wait(0.05) end pcall(function() prompt:InputHoldEnd() end) end end end return true end local function drain() local folder = scene() local id = nextDrainStage() if not id then State.activeDrainStage = nil train() return end State.activeDrainStage = id local water = child(child(folder, "关卡" .. tostring(id)), "水面") if not water or not water:IsA("BasePart") then State.status = "Waiting for stage " .. id .. " to load"; return end local _, humanoid, root = character(); if not root then return end -- Follow the game's moving water surface downward. StageClient lowers this part as fraction falls. local pos = Vector3.new(water.Position.X, water.Position.Y + water.Size.Y / 2 + humanoid.HipHeight + root.Size.Y / 2, water.Position.Z) moveTo(pos, 1.5) local localPos = water.CFrame:PointToObjectSpace(root.Position) if math.abs(localPos.X) <= water.Size.X / 2 + 2 and math.abs(localPos.Z) <= water.Size.Z / 2 + 2 and math.abs(root.Position.Y - pos.Y) < 4 and ready("standing", 0.12) then fire("Stage", "[C-S]StandingWater", id) end local s = State.stages[id] local fraction = s and tonumber(s.fraction) local depthText = fraction and (" | " .. math.floor((1 - math.clamp(fraction, 0, 1)) * 100 + 0.5) .. "% deep") or "" State.status = "Going deeper: stage " .. id .. depthText .. (s and s.remaining and (" | " .. string.format("%.3g", s.remaining) .. " water left") or "") -- Show lack of server progress explicitly; retry positioning, never claim success locally. local progress = s and s.remaining if State.drainID ~= id or State.drainProgress ~= progress then State.drainID, State.drainProgress, State.progressTime = id, progress, os.clock() elseif os.clock() - (State.progressTime or os.clock()) > 4 then State.status = "Stage " .. id .. " waiting for server progress | re-locking to deep water surface" if ready("recover", 2) then moveTo(pos + Vector3.new(1, 0, 0), 0.1) end end end local PurchaseLabel = Shop:CreateLabel("Progression: waiting for game data") local RebirthLabel = RewardTab:CreateLabel("Rebirth: checking level requirement") State.purchaseBlocked = {} State.purchaseWake = true State.purchaseWakeAt = 0 local function wakePurchases(delaySeconds) State.purchaseWake = true local at = os.clock() + math.max(0, tonumber(delaySeconds) or 0) if not State.purchaseWakeAt or State.purchaseWakeAt == 0 then State.purchaseWakeAt = at else State.purchaseWakeAt = math.min(State.purchaseWakeAt, at) end end local function owns(data, id) return type(data) == "table" and type(data.Owned) == "table" and (data.Owned[tostring(id)] == true or data.Owned[id] == true) end local function refreshKind(kind) local data = invoke(kind, "[C-S]Get" .. kind .. "Data") if type(data) == "table" then State.cache[kind] = data; return data end end local function multiplier(kind, id, owned) if kind == "Pump" and H.Pump.GetOwnedMultiplier then return H.Pump.GetOwnedMultiplier(id, owned) end return H[kind].GetMultiplier(id) end local function purchase(kind) local h, data = H[kind], State.cache[kind] if not data or type(data.Owned) ~= "table" then State.purchase = kind .. ": syncing ownership"; return end local all = kind == "Pump" and h.GetAllPumpConfig() or h.GetAllAuraConfig() local bestOwned, bestMult, baseMult = nil, 0, 0 for _, cfg in pairs(all) do if owns(data, cfg.id) then local mult = multiplier(kind, cfg.id, data.Owned) if mult > bestMult then bestOwned, bestMult = cfg.id, mult end if not cfg.dynamicMultiplier then baseMult = math.max(baseMult, h.GetMultiplier(cfg.id)) end end end if bestOwned and tonumber(data.Equipped) ~= tonumber(bestOwned) and ready("equip" .. kind, 0.6) then fire(kind, "[C-S]Equip" .. kind, bestOwned) end if kind == "Pump" and data.UseBestMultiplier ~= true and ready("bestMultiplier", 1.5) then fire("Pump", "[C-S]SetUseBestMultiplier", true) end local candidate, score for _, cfg in pairs(all) do local price = cfg.cashPrice ~= nil and tonumber(h.GetCashPrice(cfg.id)) or nil local locked = kind == "Pump" and h.RequiresWorld2(cfg.id) and value("Rebirth", "rebirth") < H.World.SecondWorldRequiredRebirths local key = kind .. tostring(cfg.id) local mult = h.GetMultiplier(cfg.id) local improvement = mult > bestMult or (kind == "Pump" and not cfg.dynamicMultiplier and mult > baseMult) if not locked and not owns(data, cfg.id) and price and price > 0 and price <= value("Cash", "cash") and improvement and os.clock() >= (State.purchaseBlocked[key] or 0) then -- Buy the strongest thing we can afford now; this skips obsolete cheap tiers. if not score or mult > score or (mult == score and price < candidate.price) then candidate = {kind = kind, id = cfg.id, price = price, key = key, name = cfg.name or key, beforeOwned = false, mult = mult} score = mult end end end return candidate end local function nextPowerPrice(kind) local h, data = H[kind], State.cache[kind] if not data or type(data.Owned) ~= "table" then return nil end local all = kind == "Pump" and h.GetAllPumpConfig() or h.GetAllAuraConfig() local bestMult, baseMult = 0, 0 for _, cfg in pairs(all) do if owns(data, cfg.id) then bestMult = math.max(bestMult, multiplier(kind, cfg.id, data.Owned)) if not cfg.dynamicMultiplier then baseMult = math.max(baseMult, h.GetMultiplier(cfg.id)) end end end local target for _, cfg in pairs(all) do local locked = kind == "Pump" and h.RequiresWorld2(cfg.id) and value("Rebirth", "rebirth") < H.World.SecondWorldRequiredRebirths local price = cfg.cashPrice ~= nil and tonumber(h.GetCashPrice(cfg.id)) or nil local mult = h.GetMultiplier(cfg.id) local improvement = mult > bestMult or (kind == "Pump" and not cfg.dynamicMultiplier and mult > baseMult) if not locked and not owns(data, cfg.id) and improvement and price and price > 0 then if not target or price < target then target = price end end end return target end local function upgradeEnabled(id) if id == "Backpack" then return Opt.Backpack end if id == "Speed" then return Opt.SpeedUpgrade end if id == "FishDisplay" then return Opt.FishDisplayUpgrade end return false end local function upgradeCandidate(id) if not upgradeEnabled(id) then return end local row = State.cache.Upgrade and State.cache.Upgrade[id] if type(row) ~= "table" or row.isMax == true then return end local level = tonumber(row.level) or 0 local okMax, maxLevel = pcall(H.Upgrade.GetMaxLevel, id) maxLevel = okMax and tonumber(maxLevel) or tonumber(row.maxLevel) if maxLevel and level >= maxLevel then return end local okPrice, rawPrice = pcall(H.Upgrade.GetCashPrice, id, level) local price = okPrice and tonumber(rawPrice) or nil local key = "Upgrade" .. id if price and price > 0 and price <= value("Cash", "cash") and os.clock() >= (State.purchaseBlocked[key] or 0) then return {kind = "Upgrade", id = id, price = price, before = level, key = key, name = id .. " level " .. (level + 1)} end end local function purchaseSucceeded(job, data) if not job or type(data) ~= "table" then return false end if job.kind == "Upgrade" then local row = data[job.id] return type(row) == "table" and ((tonumber(row.level) or 0) > (tonumber(job.before) or -1) or row.isMax == true) end return owns(data, job.id) end local function stopPurchase(message, failed, blockSeconds) local job = State.purchaseFlow if job then local seconds = tonumber(blockSeconds) if failed and not seconds then seconds = 3 end if seconds and seconds > 0 then State.purchaseBlocked[job.key] = os.clock() + seconds end end State.purchase = message State.purchaseFlow = nil State.lastPurchaseAt = os.clock() State.purchaseWake = true State.purchaseWakeAt = os.clock() + (failed and 0.15 or 0.01) end local function purchaseActive(job) local enabled = job.kind == "Pump" and Opt.Pumps or job.kind == "Aura" and Opt.Auras or job.kind == "Upgrade" and upgradeEnabled(job.id) return State.alive and Opt.Farm and enabled and State.purchaseFlow == job and character() ~= nil end local function purchaseBlocksFarm() local job = State.purchaseFlow return job ~= nil and job.mode == "fallback" end local function beginPurchaseFallback(job, reason) if not purchaseActive(job) then return end job.mode, job.phase, job.at = "fallback", "visit", 0 job.fallbackReason = reason job.started = os.clock() State.purchase = "Fast buy needs shop fallback: " .. job.name .. (reason and (" | " .. tostring(reason)) or "") end local function purchaseStep() local job = State.purchaseFlow if not job then return end if not purchaseActive(job) then stopPurchase("Purchase paused", false); return end if os.clock() - job.started > (job.mode == "fallback" and 12 or 3) then if job.mode ~= "fallback" then beginPurchaseFallback(job, "no fast confirmation") else stopPurchase("Shop fallback timed out: " .. job.name, true, 2) end return end if os.clock() < (job.at or 0) then return end local cached = State.cache[job.kind] if purchaseSucceeded(job, cached) then if job.kind == "Pump" then fire("Pump", "[C-S]SetUseBestMultiplier", true) end if job.kind ~= "Upgrade" then fire(job.kind, "[C-S]Equip" .. job.kind, job.id) end stopPurchase("HYPER bought " .. job.name, false, 0.03) return end if job.mode ~= "fallback" then if job.phase == "fastWait" then if (job.attempts or 0) >= 2 then beginPurchaseFallback(job, State.shopMessage or "no direct confirmation") return end job.phase = "fast" end if value("Cash", "cash") < (tonumber(job.price) or math.huge) then stopPurchase("Saving cash for " .. job.name, false, 0.18) return end job.attempts = (job.attempts or 0) + 1 State.shopMessage = nil job.phase, job.at = "fastWait", os.clock() + ((job.attempts or 1) == 1 and 0.12 or 0.18) State.purchase = "HYPER buying " .. job.name fire(job.kind, "[C-S]BuyCash" .. job.kind, job.id) return end local point = child(child(WS, "UIOPEN"), job.kind) local prompt = point and point:FindFirstChild("Open" .. job.kind, true) if not prompt or not prompt:IsA("ProximityPrompt") then State.status = "Fast fallback: waiting for " .. job.kind .. " shop" job.at = os.clock() + 0.15 return end local parent = prompt.Parent local position = parent:IsA("Attachment") and parent.WorldPosition or parent:IsA("BasePart") and parent.Position if not position then return end local _, _, root = character() local range = math.max(1, math.min(5, prompt.MaxActivationDistance - 1)) if (root.Position - position).Magnitude > range then State.status = "Fast fallback: visiting " .. job.kind .. " shop" fire("Stage", "[C-S]StandingWater", nil) moveTo(position, 0.5) job.at = os.clock() + 0.08 return end if job.phase == "visit" then if not prompt.Enabled then job.at = os.clock() + 0.08; return end if fireproximityprompt then pcall(fireproximityprompt, prompt, prompt.HoldDuration) else prompt:InputHoldBegin() local deadline = os.clock() + prompt.HoldDuration + 0.05 while purchaseActive(job) and prompt.Parent and os.clock() < deadline do task.wait(0.02) end pcall(function() prompt:InputHoldEnd() end) end job.phase, job.at = "fallbackBuy", os.clock() + 0.08 return end if job.phase == "fallbackBuy" then if value("Cash", "cash") < (tonumber(job.price) or math.huge) then stopPurchase("Saving cash for " .. job.name, false, 0.2); return end job.fallbackAttempts = (job.fallbackAttempts or 0) + 1 State.shopMessage = nil fire(job.kind, "[C-S]BuyCash" .. job.kind, job.id) job.phase, job.at = "fallbackWait", os.clock() + 0.25 return end if job.phase == "fallbackWait" then if purchaseSucceeded(job, State.cache[job.kind]) then if job.kind == "Pump" then fire("Pump", "[C-S]SetUseBestMultiplier", true) end if job.kind ~= "Upgrade" then fire(job.kind, "[C-S]Equip" .. job.kind, job.id) end stopPurchase("Bought " .. job.name .. " via shop fallback", false, 0.03) elseif (job.fallbackAttempts or 0) >= 2 then stopPurchase("Purchase rejected: " .. job.name .. " | " .. tostring(State.shopMessage or job.fallbackReason or "no confirmation"), true, 2) else job.phase, job.at = "fallbackBuy", os.clock() + 0.08 end end end local function progressionCandidate() local cash = value("Cash", "cash") local count, cap = bag() local candidate -- 1) Raw water/cash power compounds every click, so buy the strongest affordable pump first. if Opt.Pumps then candidate = purchase("Pump") end if candidate then return candidate end local nextPump = Opt.Pumps and nextPowerPrice("Pump") or nil local nextAura = Opt.Auras and nextPowerPrice("Aura") or nil local nextPower = nextPump and nextAura and math.min(nextPump, nextAura) or nextPump or nextAura -- 2) A tiny bag creates repeated return-home/seller downtime. Buy cheap bag levels before secondary power. local backpack = upgradeCandidate("Backpack") local bagUrgent = cap > 0 and cap <= 8 local bagCheap = backpack and nextPower and backpack.price <= nextPower * (cap <= 12 and 0.35 or 0.18) if backpack and (bagUrgent or bagCheap or count >= math.max(1, cap - 1)) then return backpack end -- 3) Aura is the next compounding multiplier. if Opt.Auras then candidate = purchase("Aura") end if candidate then return candidate end -- 4) Once close to the next multiplier, preserve cash instead of spending it on convenience upgrades. local reserve = math.clamp(tonumber(Opt.PowerReserve) or 75, 0, 95) / 100 local savingForPower = Opt.FastProgress and nextPower and cash >= nextPower * reserve and cap > 8 if savingForPower then if ready("savingPowerStatus", 0.5) then State.purchase = string.format("HYPER saving %.3g / %.3g for next pump/aura", cash, nextPower) end return nil end -- 5) Buy secondary upgrades only when they are cheap relative to the next multiplier. if backpack then return backpack end local display = upgradeCandidate("FishDisplay") if display and (not nextPower or display.price <= nextPower * 0.35) then return display end local speed = upgradeCandidate("Speed") if speed and (not nextPower or speed.price <= nextPower * 0.12) then return speed end -- If power is maxed/unavailable, finish all remaining upgrades. if not nextPower then if display then return display end if speed then return speed end end end for _, kind in ipairs({"Pump", "Aura", "Upgrade"}) do local folder = kind local event = remote("Event", folder, "[S-C]" .. folder .. "Message") if event then connect(event.OnClientEvent, function(message, success) State.shopMessage = tostring(message) local job = State.purchaseFlow if not job or job.kind ~= folder then return end if success == true then if folder == "Pump" then fire("Pump", "[C-S]SetUseBestMultiplier", true) end if folder ~= "Upgrade" then fire(folder, "[C-S]Equip" .. folder, job.id) end stopPurchase("HYPER bought " .. job.name, false, 0.03) return end if success == false then local low = string.lower(tostring(message or "")) if low:find("enough cash", 1, true) or low:find("not enough", 1, true) then stopPurchase("Saving for " .. job.name, false, 0.08) elseif job.mode ~= "fallback" then beginPurchaseFallback(job, message) else State.purchase = folder .. ": " .. tostring(message) end end end) end end local rebirthEvent = remote("Event", "Rebirth", "[S-C]RebirthUIChange") if rebirthEvent then connect(rebirthEvent.OnClientEvent, function(success, message) State.rebirthMessage = tostring(message or (success == true and "Rebirth accepted" or "Level requirement not met")) end) end local function rebirthReady() return Opt.Rebirth and value("Level", "level") >= value("Rebirth", "rebirthNeed", math.huge) end local function tryRebirthRemote() local names = {"[C - S]TryRebirth", "[C-S]TryRebirth", "TryRebirth"} for _, name in ipairs(names) do local r = remote("Event", "Rebirth", name) if r then local ok, why = pcall(r.FireServer, r) if not ok then err("Rebirth" .. name, why) end return ok end end local folder = child(child(child(RS, "Remote"), "Event"), "Rebirth") if folder then for _, r in ipairs(folder:GetChildren()) do local low = string.lower(r.Name) if r:IsA("RemoteEvent") and low:find("rebirth", 1, true) and (low:find("try", 1, true) or low:find("c%-s")) then local ok, why = pcall(r.FireServer, r) if not ok then err("RebirthAutoDetect", why) end return ok end end end State.errors.RebirthRemote = "No client-to-server rebirth remote found" return false end local function rebirthStep() if not Opt.Rebirth then State.rebirthFlow = nil; return false end local job = State.rebirthFlow if job and value("Rebirth", "rebirth") > job.before then State.rebirthMessage = "Rebirth verified: " .. value("Rebirth", "rebirth") State.rebirthFlow = nil; State.stages = {}; State.loadedStages = false State.harvestStage, State.activeDrainStage = nil, nil State.harvestReadyAt, State.harvestDeadline = nil, nil State.trainingCache = nil State.purchaseWake = true State.purchaseWakeAt = 0 return true end if not job and not rebirthReady() then return false end -- Owns the main farm loop: collection cannot refill the bag between sale and rebirth. if bag() > 0 then if Opt.Sell then State.forceSell = true; startSale() else State.status = "Rebirth ready; enable Auto sell to bank carried fish" end return true end if not job then if not ready("rebirthRequest", 1.25) then State.status = State.rebirthMessage or "Waiting to retry rebirth"; return true end State.rebirthFlow = {before = value("Rebirth", "rebirth"), at = os.clock()} State.rebirthMessage = "Rebirth requested; waiting for rebirth count" fire("Stage", "[C-S]StandingWater", nil) tryRebirthRemote() elseif os.clock() - job.at > 2.25 then State.rebirthFlow = nil State.rebirthMessage = "Rebirth unconfirmed: " .. (State.rebirthMessage or "server did not update count") if not rebirthReady() then return false end end State.status = State.rebirthMessage return true end local function loop(name, seconds, fn) task.spawn(function() while State.alive do local good, why = pcall(fn) if not good and ready("error" .. name, 10) then err(name, why) end task.wait(seconds) end end) end function State.Stop() if not State.alive then return end fire("Stage", "[C-S]StandingWater", nil) Opt.Farm = false; State.alive = false for _, c in ipairs(State.connections) do pcall(function() c:Disconnect() end) end pcall(function() Window:Destroy() end) end Window:SetCloseCallback(State.Stop) Settings:CreateButton({Name = "Unload script", Callback = State.Stop}) Settings:CreateButton({Name = "Print diagnostics (F9)", Callback = function() print("[PuckAFK Drain v1.6 HYPER]", State.status, "place", game.PlaceId, "water", value("Level", "water"), "bag", bag()) print("Clicks", "sent", State.sent, "accepted", State.clickAccepted, "rejected", State.clickRejected, "last seq", State.clickSeq, "last amount", State.lastClickAmount) print("Purchase", State.purchase, "wake", State.purchaseWake, "shop response", State.shopMessage, "rebirth", State.rebirthMessage, "level", value("Level", "level"), "required", value("Rebirth", "rebirthNeed")) print("Sale phase", State.saleFlow and State.saleFlow.phase or "none", "carry", bag(), "reason", State.saleReason) if type(State.lastSaleResult) == "table" then for key, item in pairs(State.lastSaleResult) do print("Sale result", key, tostring(item)) end end if type(State.petData) == "table" then print("Pets", "slots", State.petData.MaxSlot, "bp", State.petData.BpCapacity, "best", State.petData.BestPetID) end if type(State.storage) == "table" then print("Index", State.storage.CollectedCount, "/", State.storage.TotalFish, "mult", State.storage.Multiplier) end if type(State.fishShowState) == "table" then print("Fish display", State.fishShowState.displayedCount, "/", State.fishShowState.unlockedSlots, "cash x", State.fishShowState.cashMultiplier) end for key, why in pairs(State.errors) do warn(key, why) end end}) connect(Window.ScreenGui.Destroying, function() if State.alive then State.Stop() end end) connect(Player.Idled, function() if not State.alive or not Opt.AntiAFK then return end local vu = game:GetService("VirtualUser") pcall(function() vu:CaptureController(); vu:ClickButton2(Vector2.zero) end) end) connect(Player.CharacterAdded, function() targetFish = nil State.saleFlow = nil State.purchaseFlow = nil State.carry = nil State.harvestStage = nil State.activeDrainStage = nil State.progressTime = os.clock() State.reconnectPending = nil end) local cashValue = child(child(Player, "Cash"), "cash") if cashValue and cashValue.Changed then connect(cashValue.Changed, function() State.purchaseWake = true State.purchaseWakeAt = 0 end) end local rebirthValue = child(child(Player, "Rebirth"), "rebirth") if rebirthValue and rebirthValue.Changed then connect(rebirthValue.Changed, function() State.trainingCache = nil State.purchaseWake = true State.purchaseWakeAt = 0 end) end local rebirthNeedValue = child(child(Player, "Rebirth"), "rebirthNeed") if rebirthNeedValue and rebirthNeedValue.Changed then connect(rebirthNeedValue.Changed, function() State.purchaseWake = true State.purchaseWakeAt = 0 end) end pcall(function() connect(GuiService.ErrorMessageChanged, function(message) if not State.alive or not Opt.AutoReconnect or State.reconnectPending then return end local text = tostring(message or "") if text == "" then return end State.reconnectPending = true State.status = "Disconnected | reconnecting automatically" task.delay(3, function() if not State.alive or not Opt.AutoReconnect then return end local okTeleport, why = pcall(TeleportService.Teleport, TeleportService, game.PlaceId, Player) if not okTeleport then State.reconnectPending = nil err("AutoReconnect", why) end end) end) end) loop("click", 0.01, function() if not (Opt.Farm and Opt.Click and not State.saleFlow and not purchaseBlocksFarm() and not State.rebirthFlow and character()) then return end local requested = math.clamp(tonumber(Opt.CPS) or 20, 1, 20) if Opt.SmartCPS and State.clickPenaltyUntil and os.clock() < State.clickPenaltyUntil then requested = math.min(requested, 5) end if not ready("click", 1 / requested) then return end State.sent = State.sent + 1 -- Captured game traffic proves this argument is a monotonically increasing client click sequence, not water amount. -- A high unique range avoids colliding with the stock UI sequence when the script starts mid-session. local sequence = (State.clickBase or 100000000) + State.sent if State.clickSeq and State.clickSeq >= (State.clickBase or 100000000) then sequence = math.max(sequence, State.clickSeq + 1) end fire("Level", "[C-S]Click", sequence) end) loop("farm", 0.04, function() if not Opt.Farm then State.saleFlow = nil; State.purchaseFlow = nil; State.rebirthFlow = nil; State.forceSell = false State.status = "Paused"; return end if not character() then State.saleFlow = nil; State.purchaseFlow = nil; State.rebirthFlow = nil State.status = "Waiting for respawn"; return end if not Opt.Sell then State.saleFlow = nil; State.forceSell = false end local count, cap = bag() if cap <= 0 then State.status = "Waiting for backpack data"; return end local threshold = math.max(1, math.ceil(cap * Opt.SellAt / 100)) if Opt.Sell and (State.forceSell or count >= threshold) then startSale() end if State.saleFlow then targetFish = nil sell() return end if rebirthStep() then return end if purchaseBlocksFarm() then targetFish = nil; return end if count >= cap then State.status = Opt.Sell and (State.saleReason or "Bag full; waiting to retry seller") or "Bag full; enable Auto sell" return end if Opt.Mode == "Train water only" then train(); return end if collect() then return end drain() end) loop("purchases", 0.02, function() if not Opt.Farm or State.saleFlow or State.rebirthFlow or rebirthReady() then return end if State.purchaseFlow then purchaseStep(); return end if Opt.HyperProgress and not State.purchaseWake and os.clock() < (State.purchaseWakeAt or 0) then return end if Opt.HyperProgress and not State.purchaseWake and not ready("purchaseSafetySweep", 0.35) then return end State.purchaseWake = false State.purchaseWakeAt = 0 local candidate = progressionCandidate() if candidate then candidate.mode, candidate.phase, candidate.started, candidate.at = "fast", "fast", os.clock(), 0 State.purchaseFlow = candidate purchaseStep() end end) loop("pets", 5, function() if not Opt.Farm or State.saleFlow or purchaseBlocksFarm() or State.rebirthFlow then return end if Opt.PetMerge and ready("petMerge", 10) then fire("Pet", "MergeAll") task.wait(0.15) end if Opt.Pets then fire("Pet", "EquipBest") end local data = invoke("Pet", "GetPlayerPetData") if type(data) == "table" then State.petData = data end end) loop("carry sync", 6, function() if not State.carry or Opt.Farm then acceptCarry(invoke("Fish", "[C-S]GetCarryFishData")) end end) loop("state", 8, function() for _, kind in ipairs({"Pump", "Aura", "Upgrade"}) do if not State.cache[kind] or Opt.Farm then local data = invoke(kind, "[C-S]Get" .. kind .. "Data") if type(data) == "table" then State.cache[kind] = data end end end if not State.loadedStages or Opt.Farm then local stageFunction = remote("Function", "Stage", "[C-S]GetStageState") if stageFunction then local data = invoke("Stage", "[C-S]GetStageState") if type(data) == "table" then for _, stage in pairs(data) do setStage(stage) end; State.loadedStages = true end end end local storage = invoke("Fish", "[C-S]GetFishData") if type(storage) == "table" then acceptStorage(storage) end end) loop("display", 10, function() if not Opt.Farm then return end if Opt.BestDisplay then local result = invoke("FishShow", "[C-S]BestFishUI") if type(result) == "table" then State.lastBestDisplayResult = result if type(result.state) == "table" then State.fishShowState = result.state end end else local data = invoke("FishShow", "[C-S]GetUIState") if type(data) == "table" then State.fishShowState = data end end end) loop("daily rewards", 30, function() if not Opt.Farm or not Opt.Rewards then return end local daily = invoke("DailySign", "[C-S]GetDailySignData") if type(daily) == "table" then State.dailySign = daily end for day = 1, 7 do if not State.alive or not Opt.Farm or not Opt.Rewards then break end if invoke("DailySign", "canClaim", day) == true then fire("DailySign", "[C-S]PlayerTryDailySign", day) end end local spin = invoke("Spin", "[C-S]GetSpinData") if type(spin) == "table" then State.spinData = spin if (tonumber(spin.lastClaimTime) or 0) + 86400 <= os.time() then local claimed = invoke("Spin", "[C-S]ClaimDailySpin") if type(claimed) == "table" then spin = claimed; State.spinData = claimed end end if (tonumber(spin.total) or 0) > 0 then local spun = invoke("Spin", "[C-S]TrySpin") if type(spun) == "table" then State.spinData = spun end end end end) loop("passive rewards", 3, function() if not Opt.Farm or not Opt.PassiveRewards then return end if State.bestFishRewardReady ~= false then local best = invoke("FishShow", "[C-S]ClaimBestFishReward") if type(best) == "table" and best.success == true then State.lastBestFishReward = best.amount State.bestFishRewardReady = false end end if State.bestValueRewardReady ~= false then local valueReward = invoke("FishShow", "[C-S]ClaimBestValueFishReward") if type(valueReward) == "table" and valueReward.success == true then State.lastBestValueReward = valueReward.amount State.bestValueRewardReady = false end end end) loop("progress sync", 20, function() if not Opt.Farm then return end local ranking = invoke("Ranking", "GetYourBestAll") if type(ranking) == "table" then State.ranking = ranking end local title = invoke("Title", "[C-S]GetState") if type(title) == "table" then State.titleState = title end local fishShow = invoke("FishShow", "[C-S]GetUIState") if type(fishShow) == "table" then State.fishShowState = fishShow end end) -- Prime progression state immediately; live change events keep these caches hot afterwards. task.spawn(function() for _, kind in ipairs({"Pump", "Aura", "Upgrade"}) do if not State.alive then return end local data = invoke(kind, "[C-S]Get" .. kind .. "Data") if type(data) == "table" then State.cache[kind] = data end end local carry = invoke("Fish", "[C-S]GetCarryFishData"); if type(carry) == "table" then acceptCarry(carry) end local storage = invoke("Fish", "[C-S]GetFishData"); if type(storage) == "table" then acceptStorage(storage) end local stages = invoke("Stage", "[C-S]GetStageState") if type(stages) == "table" then for _, stage in pairs(stages) do setStage(stage) end; State.loadedStages = true end local pets = invoke("Pet", "GetPlayerPetData"); if type(pets) == "table" then State.petData = pets end State.purchaseWake = true State.purchaseWakeAt = 0 end) local function questSummary() local state = State.eventState if type(state) ~= "table" then return "Event: waiting for Cthulhu state" end local parts = {} local claimable = 0 for _, bucket in ipairs({state.rotating, state.event}) do if type(bucket) == "table" then for _, q in pairs(bucket) do if type(q) == "table" and q.claimed ~= true then if q.claimable == true then claimable = claimable + 1 end if #parts < 2 then table.insert(parts, string.format("%s %s/%s", tostring(q.id or "?"), tostring(q.displayProgress or q.rawProgress or 0), tostring(q.displayTarget or q.rawTarget or "?"))) end end end end end local named = 0; for _ in pairs(State.questTargets or {}) do named = named + 1 end return "Event quests: " .. (#parts > 0 and table.concat(parts, " | ") or "none pending") .. " | claimable " .. claimable .. " | named fish targets " .. named end local function titleProgress() local state = State.titleState local entries = type(state) == "table" and state.entries or nil if type(entries) ~= "table" then return 0, 0 end local unlocked, total = 0, 0 for _, row in pairs(entries) do total = total + 1 if type(row) == "table" and row.unlocked == true then unlocked = unlocked + 1 end end return unlocked, total end loop("status", 1, function() StatusLabel:Set(State.status) local count, cap = bag() StatsLabel:Set(string.format("Water %.3g | Cash %.3g | Carry %d/%d | Rebirth %d", value("Level", "water"), value("Cash", "cash"), count, cap, value("Rebirth", "rebirth"))) local up = State.cache.Upgrade or {} local function lvl(id) return type(up[id]) == "table" and tostring(up[id].level or 0) or "?" end PurchaseLabel:Set(State.purchase or string.format("Cash %.3g | Backpack L%s | Display L%s | Speed L%s", value("Cash", "cash"), lvl("Backpack"), lvl("FishDisplay"), lvl("Speed"))) RebirthLabel:Set((Opt.Rebirth and "ON" or "OFF") .. " | Level " .. value("Level", "level") .. "/" .. value("Rebirth", "rebirthNeed") .. " | " .. (State.rebirthMessage or "waiting for requirement")) if type(State.petData) == "table" then local equipped = type(State.petData.EquipPet) == "table" and #State.petData.EquipPet or 0 PetLabel:Set(string.format("Pets: %d/%s equipped | inventory %s | best ID %s", equipped, tostring(State.petData.MaxSlot or "?"), tostring(State.petData.BpCapacity or "?"), tostring(State.petData.BestPetID or "?"))) else PetLabel:Set("Pets: waiting for data") end EventLabel:Set(questSummary()) if type(State.ticketState) == "table" then TicketLabel:Set(string.format("Daily tickets: %s/%s claims | %ss progress | interval %ss", tostring(State.ticketState.claims or 0), tostring(State.ticketState.maxClaims or "?"), tostring(State.ticketState.progressSeconds or 0), tostring(State.ticketState.intervalSeconds or "?"))) else TicketLabel:Set("Daily tickets: waiting for state") end if type(State.storage) == "table" then IndexLabel:Set(string.format("Fish index: %s/%s | multiplier x%s | caught %s", tostring(State.storage.CollectedCount or "?"), tostring(State.storage.TotalFish or "?"), tostring(State.storage.Multiplier or "?"), tostring(State.storage.TotalCaught or "?"))) else IndexLabel:Set("Fish index: waiting for fish data") end local unlocked, totalTitles = titleProgress() local rank = State.ranking or {} local display = State.fishShowState or {} ProgressLabel:Set(string.format("Titles %d/%d | Best cash %s | Best rebirth %s | Display %s/%s", unlocked, totalTitles, tostring(rank.Cash or "?"), tostring(rank.Rebirth or "?"), tostring(display.displayedCount or "?"), tostring(display.unlockedSlots or "?"))) DetailLabel:Set(State.saleReason or State.purchase or string.format("v1.6 HYPER | %s+ | >=%d%% best | %s | clicks %d/%d", tostring(Opt.MinRarity), tonumber(Opt.ValueCutoff) or 0, Opt.DeepFirst and "depth-first" or "all cleared stages", State.clickAccepted or 0, State.clickRejected or 0)) end) UI:Notify({Title = "PuckAFK", Content = "Drain Water v1.6 HYPER loaded: event-driven chain buying, direct-sell fast path, faster rebirths, deepest unlocked training and strict valuable-fish collection.", Duration = 7})