repeat task.wait() until game:IsLoaded() local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Workspace = game:GetService("Workspace") local TeleportService = game:GetService("TeleportService") local HttpService = game:GetService("HttpService") local StarterGui = game:GetService("StarterGui") local GuiService = game:GetService("GuiService") local VirtualInputManager = game:GetService("VirtualInputManager") local VirtualUser = game:GetService("VirtualUser") local player = Players.LocalPlayer or Players.PlayerAdded:Wait() local incomingApiTarget = nil local incomingTradeTarget = nil local function captureTeleportData(data) if type(data) ~= "table" then return end if data.KLApi == true then incomingApiTarget = data end if data.KLTrade == true then incomingTradeTarget = data end end pcall(function() captureTeleportData(TeleportService:GetLocalPlayerTeleportData()) end) pcall(function() local joinData = player:GetJoinData() captureTeleportData(joinData and joinData.TeleportData) end) local Config do local defaults = { TradeReceiverName = "", HopSeaHydra = true, HopGhostShip = true, TradeItems = { ["Hydra's Tail"] = 10, ["Sea King's Fin"] = 10, ["Sea's Wraith"] = 50, ["Sea King's Blood"] = 10000 }, TradeWebhookURL = "", WebhookURL = "https://discord.com/api/webhooks/1475401772364206130/waXG0Gykq3YVvqBxVE0p6CJZlGxYsdpYmozYMeaXACwQC77L3FCPwFMweaac74sSJBwx" } local runtimeDefaults = { Enabled = true; Sea2PlaceId = 6381829480; AutoFindBoss = true; AutoTeleportBoss = true; AutoAimSkill = true; AutoNormalAttack = true; AutoTrackSpawn = true; AutoCollectLoot = true; AutoArmamentHaki = true; AutoObservationHaki = true; HakiRetryInterval = 1; ShowSpawnHUD = true; SpawnHUDUpdateInterval = 0.2; ShowStatusHUD = true; StatusTextSize = 30; AutoHop = true; AutoDodge = true; AutoStoreFruit = false; AutoDropFruit = false; AutoConvertFruit = false; AutoBuyKey = false; AutoOpenKey = false; SpawnWindowMin = 0; SpawnWindowMax = 25; HopRetryDelay = 0.2; StartupGrace = 2; ServerBossScanGrace = 3; ApiEnabled = true; ApiURL = "https://klegacy.online/api.php"; ApiReportCooldown = 0.8; ApiRequestTimeout = 5; ApiMaxPendingRequests = 2; ApiRequestQueueWait = 2; ApiReserveScan = true; ApiChestPollInterval = 0.25; ApiChestHopDelay = 0.05; ApiIncomingChestGrace = 5; BossAcquireTimeout = 5; BossRecentSeenGrace = 6; GameplayReadyStableSeconds = 1.5; AutoPressPlay = true; PlayWaitTimeout = 30; PlayRemoteFallbackDelay = 3; ServerListTimeout = 0.8; ServerListCacheSeconds = 1.5; ScoutReserveAttempts = 8; TeleportWatchdog = 3; TeleportFailureCooldown = 0.35; MaxServerPlayers = 12; BossFollowDistance = 150; BossOffset = Vector3.new(0, -10, 100); BossDeathConfirmSeconds = 1.8; PostLootBossScanDelay = 0.15; SkillInterval = 0.35; MaxCombatToolsPerCycle = 1; SkillHoldTime = 0; PerToolDelay = 0.06; NormalAttackInterval = 0.18; EquipDelay = 0.12; AttackDistance = 18; AttackSurfaceGap = 10; AttackHeight = 0; AttackRepositionThreshold = 8; AttackMaxM1SurfaceDistance = 35; GhostAttackDistance = 70; GhostAttackSurfaceGap = 55; GhostAttackHeight = 12; GhostRepositionThreshold = 10; GhostMaxCombatToolsPerCycle = 2; GhostMaxChests = 6; SkillReleaseArmTimeout = 0.15; SkillReleaseDelay = 0.02; SkillReleaseFallbackDelay = 0.04; SkillReleaseSettleDelay = 0.035; SeaAttackDistance = 3; SeaAttackSurfaceGap = 1.5; SeaAttackHeight = 0; SeaRepositionThreshold = 3; SkillKeyDelay = 0.03; SkillBusyTimeout = 0.2; BossProximityDodgeRadius = 8; DodgeInterval = 0.08; DodgeHeight = 50; DodgeRadius = 100; MaxBossDistance = 190; DodgeStep = 100; LootTimeout = 30; LootMoveDelay = 0.08; LootCycleDelay = 0.03; LootSameChestRetry = 0.12; LootConfirmTimeout = 0.4; LootRewardStableSeconds = 0.06; LootClaimSettleDelay = 0.18; LootInteractionRetries = 3; LootTouchHold = 0.07; SeaHydraJoinGrace = 5; LootQuietSeconds = 0.45; LootSpawnGrace = 5; ServerSuccessCooldown = 300; ServerScanCooldown = 30; ServerHistoryFolder = "KLSeaFarmV3"; LoaderURL = ""; FruitInterval = 1; KeyBuyInterval = 1; KeyOpenInterval = 0.8; TradeInterval = 0.25; TradeInviteInterval = 1; SpawnStatusInterval = 5; SelectedKey = "Copper Key"; BuyKeyAmount = 10; TradeReceiverHeartbeatInterval = 5; TradeReceiverLookupInterval = 2; TradeApiRequestTimeout = 2; TradeIncomingPollInterval = 0.5; TradeJoinRetryDelay = 3; TradeSessionTimeout = 45; TradeOfferRetryInterval = 0.35; TradeReadyStableSeconds = 0.6; TradeAfterSuccessHopDelay = 1; TradeWebhookUsername = "King Legacy Trade"; TradeWebhookRequestTimeout = 3; ExcludedFruitConvert = {DoughFruit = true, GateFruit = true, DragonFruit = true, PhoenixFruit = true, ToyFruit = true, OpFruit = true}; WebhookCheckInterval = 20; WebhookThresholds = {["Sea King's Fin"] = 20, ["Sea's Wraith"] = 50, ["Hydra's Tail"] = 5, ["Sea King's Blood"] = 2000} } getgenv().SeaFarmConfig = getgenv().SeaFarmConfig or {} Config = getgenv().SeaFarmConfig for key, value in pairs(defaults) do Config[key] = value end local visibleKeys = {} for key in pairs(defaults) do visibleKeys[key] = true end for key, value in pairs(runtimeDefaults) do if not visibleKeys[key] then Config[key] = value end end local receiverName = tostring(Config.TradeReceiverName or "") local receiverKey = receiverName:lower() Config.AutoTrade = true Config.AutoAcceptTrade = true Config.TradeReceiverMode = receiverKey ~= "" and player.Name:lower() == receiverKey Config.TradeChannel = receiverKey ~= "" and receiverKey or "default" Config.AllowedTraders = {} Config.TradeWebhookEnabled = type(Config.TradeWebhookURL) == "string" and Config.TradeWebhookURL ~= "" Config.WebhookEnabled = type(Config.WebhookURL) == "string" and Config.WebhookURL ~= "" Config.TradeMaterials = {} Config.TradeTriggerThresholds = {} if type(Config.TradeItems) == "table" then for item, thresholdValue in pairs(Config.TradeItems) do local name = tostring(item or "") local threshold = math.max(0, tonumber(thresholdValue) or 0) if name ~= "" then table.insert(Config.TradeMaterials, name) Config.TradeTriggerThresholds[name] = threshold end end end table.sort(Config.TradeMaterials) end local SpawnState local RUN_TOKEN = HttpService:GenerateGUID(false) getgenv().SeaFarmStandaloneToken = RUN_TOKEN getgenv().SeaFarmStandaloneRunning = true local function running() return getgenv().SeaFarmStandaloneRunning and getgenv().SeaFarmStandaloneToken == RUN_TOKEN and Config.Enabled end local function notify(text, duration) print("[SeaFarm] " .. tostring(text)) pcall(function() StarterGui:SetCore("SendNotification", { Title = "SeaFarm", Text = tostring(text), Duration = duration or 3 }) end) end if game.PlaceId ~= Config.Sea2PlaceId then notify("This standalone is configured for Sea 2 only.", 6) getgenv().SeaFarmStandaloneRunning = false return end local ChestRoot = ReplicatedStorage:WaitForChild("Chest", 15) local Remotes = ChestRoot and ChestRoot:FindFirstChild("Remotes") local Functions = Remotes and Remotes:FindFirstChild("Functions") local SkillAction = Functions and Functions:FindFirstChild("SkillAction") local GetServers = Functions and Functions:FindFirstChild("GetServers") local BuyKey = Functions and Functions:FindFirstChild("BuyKey") local UseKey = Functions and Functions:FindFirstChild("UseKey") local DealFruit = Functions and Functions:FindFirstChild("DealFruit") local TradeRequester = Functions and Functions:FindFirstChild("TradeRequester") local TradeFunction = Functions and Functions:FindFirstChild("TradeFunction") local Events = Remotes and Remotes:FindFirstChild("Events") local TradeEvent = Events and Events:FindFirstChild("TradeEvent") local InventoryEq = Functions and Functions:FindFirstChild("InventoryEq") local FruitFolder = ChestRoot and ChestRoot:FindFirstChild("Fruits") local ModulesRoot = ChestRoot and ChestRoot:FindFirstChild("Modules") local SkillDataRoot = ModulesRoot and ModulesRoot:FindFirstChild("SkillData") local SkillDataFruits = SkillDataRoot and SkillDataRoot:FindFirstChild("Fruits") local SkillDataStyles = SkillDataRoot and SkillDataRoot:FindFirstChild("Styles") local SkillDataSwords = SkillDataRoot and SkillDataRoot:FindFirstChild("Swords") local function getCharacter() local character = player.Character if not character or not character.Parent then return nil, nil, nil end return character, character:FindFirstChildOfClass("Humanoid"), character:FindFirstChild("HumanoidRootPart") end local function waitCharacter() local character = player.Character or player.CharacterAdded:Wait() local humanoid = character:WaitForChild("Humanoid", 10) local root = character:WaitForChild("HumanoidRootPart", 10) return character, humanoid, root end local function getPlayButton() local playerGui = player:FindFirstChild("PlayerGui") if not playerGui then return nil end local loadingGui = playerGui:FindFirstChild("LoadingGUI") if not loadingGui then return nil end local play = loadingGui:FindFirstChild("Play", true) if play and play:IsA("GuiButton") then return play end return nil end local function guiActuallyVisible(gui) if not gui or not gui:IsA("GuiObject") or gui.AbsoluteSize.X <= 0 or gui.AbsoluteSize.Y <= 0 then return false end local current = gui while current and current ~= player do if current:IsA("GuiObject") and not current.Visible then return false end if current:IsA("CanvasGroup") and current.GroupTransparency >= 0.99 then return false end if current:IsA("LayerCollector") and not current.Enabled then return false end current = current.Parent end return true end local function enteredGameReady() if player:FindFirstChild("DataLoaded") then return true end local playerGui = player:FindFirstChild("PlayerGui") if not playerGui then return false end local play = getPlayButton() if guiActuallyVisible(play) then return false end local character, humanoid, root = getCharacter() return character ~= nil and humanoid ~= nil and root ~= nil and character:IsDescendantOf(Workspace) and humanoid.Health > 0 and player:FindFirstChild("PlayerStats") ~= nil and player:FindFirstChild("Backpack") ~= nil end local function firePlayButton(play) if not play or not play.Parent then return false end local fired = false if type(firesignal) == "function" then local ok = pcall(function() firesignal(play.MouseButton1Click) end) if ok then fired = true end end if not fired then local ok = pcall(function() GuiService.SelectedObject = play VirtualInputManager:SendKeyEvent(true, 13, false, game) task.wait() VirtualInputManager:SendKeyEvent(false, 13, false, game) GuiService.SelectedObject = nil end) if ok then fired = true end end if not fired then local ok = pcall(function() local pos = play.AbsolutePosition local size = play.AbsoluteSize local x = math.floor(pos.X + size.X * 0.5) local y = math.floor(pos.Y + size.Y * 0.5) VirtualInputManager:SendMouseButtonEvent(x, y, 0, true, game, 0) task.wait(0.05) VirtualInputManager:SendMouseButtonEvent(x, y, 0, false, game, 0) end) if ok then fired = true end end return fired end local function enterGameRemote() local chest = ReplicatedStorage:FindFirstChild("Chest") local remotes = chest and chest:FindFirstChild("Remotes") local functions = remotes and remotes:FindFirstChild("Functions") local etcFunction = functions and functions:FindFirstChild("EtcFunction") if not etcFunction then return false end local ok = pcall(function() etcFunction:InvokeServer("EnterTheGame", {}) end) return ok end local function ensureEnteredGame() if enteredGameReady() then return true end if not Config.AutoPressPlay then return true end local started = tick() local timeout = math.max(5, tonumber(Config.PlayWaitTimeout) or 30) local fallbackDelay = math.max(1, tonumber(Config.PlayRemoteFallbackDelay) or 3) local clickedAt = nil local remoteUsed = false while running() do if enteredGameReady() then return true end local play = getPlayButton() if guiActuallyVisible(play) then if not clickedAt or tick() - clickedAt >= 2 then clickedAt = tick() firePlayButton(play) end if clickedAt and not remoteUsed and tick() - clickedAt >= fallbackDelay then remoteUsed = true enterGameRemote() end elseif not remoteUsed and tick() - started >= fallbackDelay + 2 then remoteUsed = true enterGameRemote() end if tick() - started >= timeout then started = tick() clickedAt = nil remoteUsed = false notify("Đang thử vào game lại...", 2) enterGameRemote() end task.wait(0.2) end return false end local function isGameplayReady() if not enteredGameReady() then return false end local playerGui = player:FindFirstChild("PlayerGui") if not playerGui then return false end local play = getPlayButton() if guiActuallyVisible(play) then return false end local character, humanoid, root = getCharacter() if not character or not humanoid or not root then return false end if not character:IsDescendantOf(Workspace) or humanoid.Health <= 0 then return false end if not player:FindFirstChild("PlayerStats") or not player:FindFirstChild("Backpack") then return false end return true end local function isWorldReady() if not game:IsLoaded() or not isGameplayReady() then return false end local playerGui = player:FindFirstChild("PlayerGui") local mainGui = playerGui and playerGui:FindFirstChild("MainGui") local starterFrame = mainGui and mainGui:FindFirstChild("StarterFrame") if not mainGui or not starterFrame then return false end local islands = Workspace:FindFirstChild("Island") if not islands or #islands:GetChildren() == 0 then return false end return true end local function waitForGameplayReady() local stableSince = nil local stableSeconds = math.max(3, tonumber(Config.GameplayReadyStableSeconds) or 1.5) while running() do if isWorldReady() then stableSince = stableSince or tick() if tick() - stableSince >= stableSeconds then return true end else stableSince = nil end task.wait(0.25) end return false end local function getModelRoot(model) if not model then return nil end local humanoid = model:FindFirstChildOfClass("Humanoid") if humanoid and humanoid.RootPart then return humanoid.RootPart end return model:FindFirstChild("HumanoidRootPart") or model:FindFirstChild("RootPart") or model.PrimaryPart end local function aliveModel(model) if not model or not model.Parent then return false end local humanoid = model:FindFirstChildOfClass("Humanoid") local root = getModelRoot(model) return humanoid ~= nil and root ~= nil and humanoid.Health > 0 end local function targetModeAllows(kind) if kind == "SeaHydra" then return Config.HopSeaHydra ~= false end if kind == "GhostShip" then return Config.HopGhostShip ~= false end return false end local function tradeGameplayLocked() return Config.TradeReceiverMode == true or (SpawnState and SpawnState.tradeMissionActive == true) end local function getBossKind(model) if not model then return nil end if model.Name == "Ghost Ship" then return "GhostShip" end if model.Name == "SeaKing" or model.Name == "HydraSeaKing" then return "SeaHydra" end return nil end local function bossCandidateState(model) if not model or not model.Parent or not model:IsA("Model") then return "gone" end local humanoid = model:FindFirstChildOfClass("Humanoid") if humanoid and humanoid.Health <= 0 then return "dead" end local root = getModelRoot(model) if humanoid and root and humanoid.Health > 0 then return "alive" end return "loading" end local function getBossCandidateList() local list = {} if tradeGameplayLocked() then return list end local seaFolder = Workspace:FindFirstChild("SeaMonster") local ghostFolder = Workspace:FindFirstChild("GhostMonster") if seaFolder and targetModeAllows("SeaHydra") then for _, model in ipairs(seaFolder:GetChildren()) do if model:IsA("Model") and (model.Name == "SeaKing" or model.Name == "HydraSeaKing") then table.insert(list, model) end end end if ghostFolder and targetModeAllows("GhostShip") then for _, model in ipairs(ghostFolder:GetChildren()) do if model:IsA("Model") and model.Name == "Ghost Ship" then table.insert(list, model) end end end return list end local function getBossCandidate() local _, _, root = getCharacter() local best = nil local bestDistance = math.huge local bestState = nil for _, model in ipairs(getBossCandidateList()) do local state = bossCandidateState(model) if state ~= "dead" and state ~= "gone" then local modelRoot = getModelRoot(model) local distance = 0 if root and modelRoot then distance = (root.Position - modelRoot.Position).Magnitude end if not best or distance < bestDistance then best = model bestDistance = distance bestState = state end end end return best, bestState end local function getBossList() local list = {} if tradeGameplayLocked() then return list end local seaFolder = Workspace:FindFirstChild("SeaMonster") local ghostFolder = Workspace:FindFirstChild("GhostMonster") if seaFolder and targetModeAllows("SeaHydra") then for _, model in ipairs(seaFolder:GetChildren()) do if model:IsA("Model") and (model.Name == "SeaKing" or model.Name == "HydraSeaKing") and aliveModel(model) then table.insert(list, model) end end end if ghostFolder and targetModeAllows("GhostShip") then for _, model in ipairs(ghostFolder:GetChildren()) do if model:IsA("Model") and model.Name == "Ghost Ship" and aliveModel(model) then table.insert(list, model) end end end return list end local function getNearestAlive(list) local _, _, root = getCharacter() local best local bestDistance = math.huge for _, model in ipairs(list) do if aliveModel(model) then local targetRoot = getModelRoot(model) if targetRoot then local distance = root and (root.Position - targetRoot.Position).Magnitude or 0 if distance < bestDistance then bestDistance = distance best = model end end end end return best end local function getBoss() return getNearestAlive(getBossList()) end local lockedBoss = nil local function getMobTarget() if lockedBoss then if aliveModel(lockedBoss) then return lockedBoss end return nil end local boss = getBoss() if boss then return boss end local mobFolder = Workspace:FindFirstChild("MOB") if not mobFolder then return nil end local list = {} for _, model in ipairs(mobFolder:GetChildren()) do if model:IsA("Model") and aliveModel(model) then table.insert(list, model) end end return getNearestAlive(list) end local function parseTimer(text) if type(text) ~= "string" then return nil end local h, m, s = text:match("(%d+):(%d+):(%d+)") if not h then return nil end return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s) end local function formatTimer(seconds) if not seconds then return "unknown" end local h = math.floor(seconds / 3600) local m = math.floor((seconds % 3600) / 60) local s = seconds % 60 return string.format("%02d:%02d:%02d", h, m, s) end SpawnState = { SeaText = nil, SeaSeconds = nil, SeaUpdatedAt = 0, GhostText = nil, GhostSeconds = nil, GhostUpdatedAt = 0, Hydra = false } SpawnState.armamentRequestedCharacter = nil SpawnState.observationRequestedCharacter = nil SpawnState.tradeMissionActive = incomingTradeTarget ~= nil and Config.TradeReceiverMode ~= true SpawnState.tradeMissionStartedAt = 0 SpawnState.tradeRetryAfter = 0 SpawnState.tradeSuccessSerial = 0 SpawnState.tradeCancelSerial = 0 SpawnState.tradeIncomingSenders = {} SpawnState.tradeIncomingUpdatedAt = 0 SpawnState.tradeLastReceiverPingAt = 0 SpawnState.tradeLastLookupAt = 0 SpawnState.tradeIncomingLastAttemptAt = 0 SpawnState.tradeReceiverPingBusy = false SpawnState.tradeIncomingBusy = false SpawnState.tradeWebhookLast = {} SpawnState.tradeOfferStableSignature = "" SpawnState.tradeOfferStableSince = 0 SpawnState.tradeActivePartner = nil SpawnState.tradeLastAcceptAttemptAt = 0 SpawnState.tradeLastAcceptName = "" SpawnState.tradeReceiverSessionPartner = "" SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false local function ensureHaki() local character = player.Character if not character or not character.Parent then return end local function pressKey(keyCode) pcall(function() VirtualInputManager:SendKeyEvent(true, keyCode, false, game) task.wait(0.05) VirtualInputManager:SendKeyEvent(false, keyCode, false, game) end) end local services = character:FindFirstChild("Services") if Config.AutoArmamentHaki then local hakiValue = (services and services:FindFirstChild("Haki")) or character:FindFirstChild("Haki") local active = character:FindFirstChild("ArmamentBodyGroup") ~= nil local known = active or (hakiValue and hakiValue:IsA("ValueBase")) if hakiValue and hakiValue:IsA("ValueBase") then active = active or hakiValue.Value == true or (tonumber(hakiValue.Value) or 0) > 0 end if active then SpawnState.armamentRequestedCharacter = character elseif known or SpawnState.armamentRequestedCharacter ~= character then SpawnState.armamentRequestedCharacter = character local fired = false local playerStats = player:FindFirstChild("PlayerStats") local busoValue = playerStats and playerStats:FindFirstChild("BusoShopValue") if busoValue and busoValue:IsA("ValueBase") then pcall(function() busoValue.Value = "BusoHaki" end) end local events = Remotes and Remotes:FindFirstChild("Events") local armament = events and events:FindFirstChild("Armament") if armament and armament:IsA("RemoteEvent") then fired = pcall(function() armament:FireServer() end) end if not fired then pressKey(Enum.KeyCode.T) end end end if Config.AutoObservationHaki then local kenValue = character:FindFirstChild("KenOpen") or (services and services:FindFirstChild("KenHaki")) local known = kenValue and kenValue:IsA("ValueBase") local active = false if known then active = kenValue.Value == true or (tonumber(kenValue.Value) or 0) > 0 end if active then SpawnState.observationRequestedCharacter = character elseif known or SpawnState.observationRequestedCharacter ~= character then SpawnState.observationRequestedCharacter = character pressKey(Enum.KeyCode.Y) end end end local function getLegacyPoseSecondSea() local playerGui = player:FindFirstChild("PlayerGui") local mainGui = playerGui and playerGui:FindFirstChild("MainGui") local starterFrame = mainGui and mainGui:FindFirstChild("StarterFrame") local legacyPoseFrame = starterFrame and starterFrame:FindFirstChild("LegacyPoseFrame") return legacyPoseFrame and legacyPoseFrame:FindFirstChild("SecondSea") end local function updateSeaSpawnState() local text = ReplicatedStorage:GetAttribute("SeaMonsterSpawnText") local seconds = parseTimer(text) if type(text) ~= "string" or text == "" then local secondSea = getLegacyPoseSecondSea() local label = secondSea and secondSea:FindFirstChild("SKTimeLabel") local guiText = label and label.Text or nil local guiSeconds = parseTimer(guiText) if guiSeconds ~= nil then text = guiText seconds = guiSeconds end end SpawnState.SeaText = text SpawnState.SeaSeconds = seconds SpawnState.SeaUpdatedAt = tick() SpawnState.Hydra = ReplicatedStorage:GetAttribute("Hydra") == true end local function updateGhostSpawnState() local text = ReplicatedStorage:GetAttribute("GhostShipSpawnText") local seconds = parseTimer(text) if type(text) ~= "string" or text == "" then local secondSea = getLegacyPoseSecondSea() local label = secondSea and secondSea:FindFirstChild("GSTimeLabel") local guiText = label and label.Text or nil local guiSeconds = parseTimer(guiText) if guiSeconds ~= nil then text = guiText seconds = guiSeconds end end SpawnState.GhostText = text SpawnState.GhostSeconds = seconds SpawnState.GhostUpdatedAt = tick() end updateSeaSpawnState() updateGhostSpawnState() pcall(function() ReplicatedStorage:GetAttributeChangedSignal("SeaMonsterSpawnText"):Connect(updateSeaSpawnState) ReplicatedStorage:GetAttributeChangedSignal("GhostShipSpawnText"):Connect(updateGhostSpawnState) ReplicatedStorage:GetAttributeChangedSignal("Hydra"):Connect(updateSeaSpawnState) end) local function displayedSpawnSeconds(seconds, updatedAt) if type(seconds) ~= "number" then return nil end local elapsed = math.max(0, tick() - (updatedAt or tick())) return math.max(0, seconds - math.floor(elapsed)) end local function getSpawnTimers() if SpawnState.SeaSeconds == nil then updateSeaSpawnState() end if SpawnState.GhostSeconds == nil then updateGhostSpawnState() end return displayedSpawnSeconds(SpawnState.SeaSeconds, SpawnState.SeaUpdatedAt), displayedSpawnSeconds(SpawnState.GhostSeconds, SpawnState.GhostUpdatedAt), SpawnState.SeaText, SpawnState.GhostText end local function getTargetSpawnSeconds() local sk, gs = getSpawnTimers() local allowSea = targetModeAllows("SeaHydra") local allowGhost = targetModeAllows("GhostShip") if allowSea and not allowGhost then return sk, "SeaHydra" end if allowGhost and not allowSea then return gs, "GhostShip" end if not allowSea and not allowGhost then return nil, nil end if sk ~= nil and gs ~= nil then if sk <= gs then return sk, "SeaHydra" end return gs, "GhostShip" end if sk ~= nil then return sk, "SeaHydra" end return gs, "GhostShip" end SpawnState.spawnHudGui = nil SpawnState.spawnHudSeaTitle = nil SpawnState.spawnHudSeaValue = nil SpawnState.spawnHudGhostTitle = nil SpawnState.spawnHudGhostValue = nil local function makeHudLabel(parent, name, size, position, font, textSize, alignment) local label = Instance.new("TextLabel") label.Name = name label.BackgroundTransparency = 1 label.BorderSizePixel = 0 label.Size = size label.Position = position label.Font = font label.TextSize = textSize label.TextColor3 = Color3.fromRGB(255, 255, 255) label.TextStrokeColor3 = Color3.fromRGB(0, 0, 0) label.TextStrokeTransparency = 0 label.TextXAlignment = alignment or Enum.TextXAlignment.Left label.TextYAlignment = Enum.TextYAlignment.Center label.RichText = false label.Parent = parent return label end local function createSpawnHUD() if not Config.ShowSpawnHUD then return nil end local playerGui = player:FindFirstChild("PlayerGui") if not playerGui then return nil end local existing = playerGui:FindFirstChild("SeaFarmSpawnHUD") if existing and existing:IsA("ScreenGui") then SpawnState.spawnHudGui = existing local seaPanel = existing:FindFirstChild("SeaPanel") local ghostPanel = existing:FindFirstChild("GhostPanel") SpawnState.spawnHudSeaTitle = seaPanel and seaPanel:FindFirstChild("Title") SpawnState.spawnHudSeaValue = seaPanel and seaPanel:FindFirstChild("Value") SpawnState.spawnHudGhostTitle = ghostPanel and ghostPanel:FindFirstChild("Title") SpawnState.spawnHudGhostValue = ghostPanel and ghostPanel:FindFirstChild("Value") existing.Enabled = true return existing end local gui = Instance.new("ScreenGui") gui.Name = "SeaFarmSpawnHUD" gui.ResetOnSpawn = false gui.IgnoreGuiInset = true gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling gui.DisplayOrder = 999999 gui.Parent = playerGui local seaPanel = Instance.new("Frame") seaPanel.Name = "SeaPanel" seaPanel.BackgroundColor3 = Color3.fromRGB(0, 0, 0) seaPanel.BackgroundTransparency = 0.4 seaPanel.BorderSizePixel = 0 seaPanel.Size = UDim2.new(0, 180, 0, 42) seaPanel.Position = UDim2.new(0, 18, 0, 56) seaPanel.Parent = gui local seaCorner = Instance.new("UICorner") seaCorner.CornerRadius = UDim.new(0, 8) seaCorner.Parent = seaPanel local seaStroke = Instance.new("UIStroke") seaStroke.Color = Color3.fromRGB(255, 215, 120) seaStroke.Transparency = 0.15 seaStroke.Thickness = 1 seaStroke.Parent = seaPanel SpawnState.spawnHudSeaTitle = makeHudLabel(seaPanel, "Title", UDim2.new(1, -12, 0, 14), UDim2.new(0, 8, 0, 3), Enum.Font.GothamBold, 10, Enum.TextXAlignment.Left) SpawnState.spawnHudSeaTitle.TextColor3 = Color3.fromRGB(255, 231, 170) SpawnState.spawnHudSeaValue = makeHudLabel(seaPanel, "Value", UDim2.new(1, -12, 0, 22), UDim2.new(0, 8, 0, 16), Enum.Font.GothamBlack, 22, Enum.TextXAlignment.Left) SpawnState.spawnHudSeaValue.TextColor3 = Color3.fromRGB(255, 255, 255) local ghostPanel = Instance.new("Frame") ghostPanel.Name = "GhostPanel" ghostPanel.BackgroundColor3 = Color3.fromRGB(0, 0, 0) ghostPanel.BackgroundTransparency = 0.4 ghostPanel.BorderSizePixel = 0 ghostPanel.AnchorPoint = Vector2.new(1, 0) ghostPanel.Size = UDim2.new(0, 180, 0, 42) ghostPanel.Position = UDim2.new(1, -18, 0, 56) ghostPanel.Parent = gui local ghostCorner = Instance.new("UICorner") ghostCorner.CornerRadius = UDim.new(0, 8) ghostCorner.Parent = ghostPanel local ghostStroke = Instance.new("UIStroke") ghostStroke.Color = Color3.fromRGB(200, 200, 255) ghostStroke.Transparency = 0.15 ghostStroke.Thickness = 1 ghostStroke.Parent = ghostPanel SpawnState.spawnHudGhostTitle = makeHudLabel(ghostPanel, "Title", UDim2.new(1, -12, 0, 14), UDim2.new(0, 8, 0, 3), Enum.Font.GothamBold, 10, Enum.TextXAlignment.Right) SpawnState.spawnHudGhostTitle.TextColor3 = Color3.fromRGB(220, 220, 255) SpawnState.spawnHudGhostValue = makeHudLabel(ghostPanel, "Value", UDim2.new(1, -12, 0, 22), UDim2.new(0, 4, 0, 16), Enum.Font.GothamBlack, 22, Enum.TextXAlignment.Right) SpawnState.spawnHudGhostValue.TextColor3 = Color3.fromRGB(255, 255, 255) SpawnState.spawnHudGui = gui return gui end local function setSpawnHUDEnabled(state) local gui = SpawnState.spawnHudGui or createSpawnHUD() if gui then gui.Enabled = state and Config.ShowSpawnHUD or false end end local function updateSpawnHUD() if not Config.ShowSpawnHUD then setSpawnHUDEnabled(false) return end local gui = SpawnState.spawnHudGui or createSpawnHUD() if not gui or not SpawnState.spawnHudSeaTitle or not SpawnState.spawnHudSeaValue or not SpawnState.spawnHudGhostTitle or not SpawnState.spawnHudGhostValue then return end gui.Enabled = true local seaSeconds, ghostSeconds = getSpawnTimers() local seaName = SpawnState.Hydra and "HydraSeaKing" or "SeaKing" SpawnState.spawnHudSeaTitle.Text = seaName SpawnState.spawnHudSeaValue.Text = formatTimer(seaSeconds) SpawnState.spawnHudGhostTitle.Text = "Ghost Ship" SpawnState.spawnHudGhostValue.Text = formatTimer(ghostSeconds) end SpawnState.statusHudGui = nil SpawnState.statusHudLabel = nil SpawnState.statusPriority = 0 SpawnState.statusPriorityUntil = 0 SpawnState.statusText = "" local function createStatusHUD() if not Config.ShowStatusHUD then return nil end local playerGui = player:FindFirstChild("PlayerGui") if not playerGui then return nil end local existing = playerGui:FindFirstChild("SeaFarmStatusHUD") if existing and existing:IsA("ScreenGui") then SpawnState.statusHudGui = existing SpawnState.statusHudLabel = existing:FindFirstChild("Status") existing.Enabled = true return existing end local gui = Instance.new("ScreenGui") gui.Name = "SeaFarmStatusHUD" gui.ResetOnSpawn = false gui.IgnoreGuiInset = true gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling gui.DisplayOrder = 1000000 gui.Parent = playerGui local label = Instance.new("TextLabel") label.Name = "Status" label.AnchorPoint = Vector2.new(0.5, 0.5) label.Position = UDim2.new(0.5, 0, 0.5, 0) label.Size = UDim2.new(0, 760, 0, 64) label.BackgroundTransparency = 1 label.BorderSizePixel = 0 label.Text = "" label.TextColor3 = Color3.fromRGB(255, 255, 255) label.TextStrokeColor3 = Color3.fromRGB(0, 0, 0) label.TextStrokeTransparency = 0 label.Font = Enum.Font.GothamBlack label.TextSize = math.max(18, tonumber(Config.StatusTextSize) or 30) label.TextWrapped = false label.TextXAlignment = Enum.TextXAlignment.Center label.TextYAlignment = Enum.TextYAlignment.Center label.ZIndex = 1000000 label.Parent = gui SpawnState.statusHudGui = gui SpawnState.statusHudLabel = label return gui end local function setStatus(value, priority, duration) if not Config.ShowStatusHUD then if SpawnState.statusHudGui then SpawnState.statusHudGui.Enabled = false end return end local gui = SpawnState.statusHudGui or createStatusHUD() if not gui or not SpawnState.statusHudLabel then return end local now = tick() local newPriority = tonumber(priority) or 0 if (SpawnState.lootBusy or SpawnState.lootPriority) and newPriority < 100 then return end if now < SpawnState.statusPriorityUntil and newPriority < SpawnState.statusPriority then return end SpawnState.statusPriority = newPriority SpawnState.statusPriorityUntil = duration and duration > 0 and now + duration or now SpawnState.statusText = tostring(value or "") SpawnState.statusHudLabel.Text = SpawnState.statusText SpawnState.statusHudLabel.TextSize = math.max(18, tonumber(Config.StatusTextSize) or 30) gui.Enabled = true end local function setStatusEnabled(state) local gui = SpawnState.statusHudGui or createStatusHUD() if gui then gui.Enabled = state and Config.ShowStatusHUD or false end end local function ensureFreeze(state) local _, _, root = getCharacter() if not root then return end local freeze = root:FindFirstChild("SeaFarmFreeze") if state then if not freeze then freeze = Instance.new("BodyVelocity") freeze.Name = "SeaFarmFreeze" freeze.MaxForce = Vector3.new(math.huge, math.huge, math.huge) freeze.Velocity = Vector3.new(0, 0, 0) freeze.Parent = root end elseif freeze then freeze:Destroy() end end local function remoteInvoke(remote, ...) if not remote then return false end local args = table.pack(...) local ok = pcall(function() remote:InvokeServer(table.unpack(args, 1, args.n)) end) return ok end local function getGameGlobal() if type(getrenv) == "function" then local ok, env = pcall(getrenv) if ok and type(env) == "table" and type(env._G) == "table" then return env._G end end return _G end local function setGameMouseHit(targetCFrame) if typeof(targetCFrame) ~= "CFrame" then return end pcall(function() _G.MouseHit = targetCFrame end) local gameGlobal = getGameGlobal() if gameGlobal and gameGlobal ~= _G then pcall(function() gameGlobal.MouseHit = targetCFrame end) end end local function getCombatToolInfo(tool) if not tool or not tool:IsA("Tool") then return nil end local dataName = tool.Name .. "_Data" if SkillDataFruits then local dataModule = SkillDataFruits:FindFirstChild(dataName) if dataModule then return { Kind = "Fruit", Prefix = "DF", SelectionStat = "DFName", RequirementStat = "DF", DataModule = dataModule } end end if SkillDataStyles then local dataModule = SkillDataStyles:FindFirstChild(dataName) if dataModule then return { Kind = "FightingStyle", Prefix = "FS", SelectionStat = "FightingStyle", RequirementStat = "Melee", DataModule = dataModule } end end if SkillDataSwords then local dataModule = SkillDataSwords:FindFirstChild(dataName) if dataModule then return { Kind = "Sword", Prefix = "SW", SelectionStat = "SwordName", RequirementStat = "sword", DataModule = dataModule } end end return nil end local function selectedCombatTool(tool, info) if not tool or not info then return false end local stats = player:FindFirstChild("PlayerStats") local selected = stats and stats:FindFirstChild(info.SelectionStat) return selected ~= nil and tostring(selected.Value) == tool.Name end local function equipTool(tool, info) if not tool or not tool:IsA("Tool") then return false end info = info or getCombatToolInfo(tool) if info and not selectedCombatTool(tool, info) then if info.Kind == "Sword" and InventoryEq then pcall(function() InventoryEq:InvokeServer(tool.Name) end) local started = tick() while running() and tick() - started < 0.8 do if selectedCombatTool(tool, info) then break end task.wait(0.05) end end end if info and not selectedCombatTool(tool, info) then return false end local character, humanoid = getCharacter() if not character or not humanoid or humanoid.Health <= 0 then return false end local current = character:FindFirstChildOfClass("Tool") if current and current ~= tool then pcall(function() humanoid:UnequipTools() end) task.wait(0.04) end if tool.Parent ~= character then pcall(function() humanoid:EquipTool(tool) end) task.wait(math.max(0.05, tonumber(Config.EquipDelay) or 0.12)) end return tool.Parent == character and character:FindFirstChildOfClass("Tool") == tool end local function findTool(name) local character = player.Character local backpack = player:FindFirstChild("Backpack") return character and character:FindFirstChild(name) or backpack and backpack:FindFirstChild(name) end local skillDataCache = setmetatable({}, {__mode = "k"}) local function getSkillData(info) if not info or not info.DataModule then return nil end if skillDataCache[info.DataModule] ~= nil then return skillDataCache[info.DataModule] or nil end local ok, data = pcall(require, info.DataModule) if ok and type(data) == "table" then skillDataCache[info.DataModule] = data return data end skillDataCache[info.DataModule] = false return nil end local function gameCooldownActive(prefix, key) local gameGlobal = getGameGlobal() local cooldowns = gameGlobal and gameGlobal.Cooldowns return type(cooldowns) == "table" and cooldowns[tostring(prefix) .. tostring(key)] ~= nil end local function meetsSkillRequirement(info, data, key) if not info or type(data) ~= "table" then return true end local requirement = tonumber(data[key .. "Require"]) if not requirement then return true end local stats = player:FindFirstChild("PlayerStats") local stat = stats and stats:FindFirstChild(info.RequirementStat) if not stat then return false end return (tonumber(stat.Value) or 0) >= requirement end local function getUnlockedSkillKeys(info) local data = getSkillData(info) if not data then return {} end local result = {} for _, key in ipairs({"Z", "X", "C", "V", "B", "E"}) do if data[key] ~= nil and meetsSkillRequirement(info, data, key) and not gameCooldownActive(info.Prefix, key) then table.insert(result, key) end end return result end local function waitCombatReady(timeout) local started = tick() local maxWait = math.max(0.1, tonumber(timeout) or 0.85) repeat local character, humanoid, root = getCharacter() if character and humanoid and root and humanoid.Health > 0 and humanoid.WalkSpeed > 0 and not root.Anchored and not character:FindFirstChild("TotalDoing") then return true end task.wait(0.04) until not running() or tick() - started >= maxWait local character, humanoid, root = getCharacter() return character ~= nil and humanoid ~= nil and root ~= nil and humanoid.Health > 0 and not root.Anchored end local clientSkillCache = {} local function matchesSkillFolder(instance, info) if not instance or not info then return false end local wantedFolder if info.Kind == "Fruit" then wantedFolder = "FruitSkills" elseif info.Kind == "Sword" then wantedFolder = "SwordSkills" elseif info.Kind == "FightingStyle" then wantedFolder = "FightingStyleSkills" else return false end local parent = instance.Parent while parent do if parent.Name == wantedFolder then return true end parent = parent.Parent end return false end local function findClientSkillModule(tool, info) if not tool or not info then return nil end local cacheKey = info.Kind .. "|" .. tool.Name local cached = clientSkillCache[cacheKey] if cached and cached.Instance and cached.Instance.Parent and type(cached.Library) == "table" then return cached.Library end local moduleName = tool.Name .. "_Client" local roots = { player:FindFirstChild("PlayerScripts"), player:FindFirstChild("PlayerGui"), player.Character, ChestRoot } for _, root in ipairs(roots) do if root then for _, descendant in ipairs(root:GetDescendants()) do if descendant:IsA("ModuleScript") and descendant.Name == moduleName and matchesSkillFolder(descendant, info) then local ok, library = pcall(require, descendant) if ok and type(library) == "table" then clientSkillCache[cacheKey] = { Instance = descendant, Library = library } return library end end end end end clientSkillCache[cacheKey] = nil return nil end local function invokeClientSkill(tool, info, key, targetCFrame) local library = findClientSkillModule(tool, info) if not library or type(library[key]) ~= "function" then return false end setGameMouseHit(targetCFrame) if key == "M1" then local ok = pcall(function() library[key]() end) return ok end local finished = false local succeeded = false task.spawn(function() succeeded = pcall(function() library[key]() end) finished = true end) local armStarted = tick() local armTimeout = math.max(0.05, tonumber(Config.SkillReleaseArmTimeout) or 0.15) local armed = false repeat if gameCooldownActive(info.Prefix, key) then armed = true break end if finished and not succeeded then return false end task.wait(0.005) until not running() or tick() - armStarted >= armTimeout if not running() then return false end if armed then task.wait(math.max(0.005, tonumber(Config.SkillReleaseDelay) or 0.02)) else task.wait(math.max(0.01, tonumber(Config.SkillReleaseFallbackDelay) or 0.04)) end setGameMouseHit(targetCFrame) if type(library.Deactive) == "function" then pcall(function() library.Deactive(key) end) end task.wait(math.max(0, tonumber(Config.SkillReleaseSettleDelay) or 0.035)) return succeeded or not finished end local function keyCodeForSkill(key) if key == "Z" then return Enum.KeyCode.Z elseif key == "X" then return Enum.KeyCode.X elseif key == "C" then return Enum.KeyCode.C elseif key == "V" then return Enum.KeyCode.V elseif key == "B" then return Enum.KeyCode.B elseif key == "E" then return Enum.KeyCode.E end return nil end local function aimAtTarget(target) local targetRoot = getModelRoot(target) if not targetRoot then return nil end local cf = targetRoot.CFrame setGameMouseHit(cf) return cf end local function pressSkillKey(tool, info, key, target) if not tool or not info or not key or not target or not aliveModel(target) then return false end if tool.Parent ~= player.Character or not selectedCombatTool(tool, info) then return false end if gameCooldownActive(info.Prefix, key) then return false end local targetCFrame = aimAtTarget(target) if not targetCFrame then return false end if invokeClientSkill(tool, info, key, targetCFrame) then task.wait(math.max(0, tonumber(Config.SkillKeyDelay) or 0.03)) return true end if not SkillAction then return false end local remoteName = tostring(info.Prefix) .. "_" .. tool.Name .. "_" .. tostring(key) local downOk = pcall(function() SkillAction:InvokeServer(remoteName, { MouseHit = targetCFrame, Type = "Down" }) end) if not downOk then return false end task.wait(math.max(0.01, tonumber(Config.SkillReleaseFallbackDelay) or 0.04)) setGameMouseHit(targetCFrame) pcall(function() SkillAction:InvokeServer(remoteName, { MouseHit = targetCFrame, Type = "Up" }) end) task.wait(math.max(0, tonumber(Config.SkillKeyDelay) or 0.03)) return true end local function pressM1(tool, info, target) if not tool or not info or not target or not aliveModel(target) then return false end if tool.Parent ~= player.Character or not selectedCombatTool(tool, info) then return false end local targetCFrame = aimAtTarget(target) if not targetCFrame then return false end if invokeClientSkill(tool, info, "M1", targetCFrame) then return true end local camera = Workspace.CurrentCamera local viewport = camera and camera.ViewportSize or Vector2.new(800, 600) local x = math.floor(viewport.X * 0.5) local y = math.floor(viewport.Y * 0.5) local ok = pcall(function() setGameMouseHit(targetCFrame) VirtualInputManager:SendMouseButtonEvent(x, y, 0, true, game, 0) task.wait(0.035) setGameMouseHit(targetCFrame) VirtualInputManager:SendMouseButtonEvent(x, y, 0, false, game, 0) end) if not ok then ok = pcall(function() setGameMouseHit(targetCFrame) VirtualUser:ClickButton1(Vector2.new(x, y)) end) end return ok end local function insideRoom(position, room) if not room or not room:IsA("BasePart") then return true end local localPos = room.CFrame:PointToObjectSpace(position) local half = room.Size * 0.5 return math.abs(localPos.X) <= half.X and math.abs(localPos.Z) <= half.Z end local function getAttackProfile(target) local kind = getBossKind(target) if kind == "GhostShip" then return { Distance = math.max(20, tonumber(Config.GhostAttackDistance) or 70), SurfaceGap = math.max(10, tonumber(Config.GhostAttackSurfaceGap) or 55), Height = tonumber(Config.GhostAttackHeight) or 12, RepositionThreshold = math.max(3, tonumber(Config.GhostRepositionThreshold) or 10), UseM1 = false } end if kind == "SeaHydra" then return { Distance = math.max(1, tonumber(Config.SeaAttackDistance) or 3), SurfaceGap = math.max(0.5, tonumber(Config.SeaAttackSurfaceGap) or 1.5), Height = tonumber(Config.SeaAttackHeight) or 0, RepositionThreshold = math.max(1, tonumber(Config.SeaRepositionThreshold) or 3), UseM1 = true } end return { Distance = math.max(3, tonumber(Config.AttackDistance) or 18), SurfaceGap = math.max(2, tonumber(Config.AttackSurfaceGap) or 10), Height = tonumber(Config.AttackHeight) or 0, RepositionThreshold = math.max(2, tonumber(Config.AttackRepositionThreshold) or 8), UseM1 = true } end local function shouldUseM1(target) return getAttackProfile(target).UseM1 end local function getAttackCFrame(target) local targetRoot = getModelRoot(target) if not targetRoot then return nil end local profile = getAttackProfile(target) local rootRadius = math.max(targetRoot.Size.X, targetRoot.Size.Z) * 0.5 local distance = math.max( profile.Distance, rootRadius + profile.SurfaceGap ) local localOffset = CFrame.new(0, profile.Height, distance) local position = (targetRoot.CFrame * localOffset).Position if (position - targetRoot.Position).Magnitude < 1 then position = targetRoot.Position + Vector3.new(0, profile.Height, distance) end return CFrame.lookAt(position, targetRoot.Position) end local function positionForCombat(target) local _, _, root = getCharacter() local targetRoot = getModelRoot(target) local desired = getAttackCFrame(target) if not root or not targetRoot or not desired then return false end local profile = getAttackProfile(target) if (root.Position - desired.Position).Magnitude > profile.RepositionThreshold then ensureFreeze(true) root.CFrame = desired task.wait(0.05) else root.CFrame = CFrame.lookAt(root.Position, targetRoot.Position) end setGameMouseHit(targetRoot.CFrame) return true end local function ensureOpeRoom(target) local ope = findTool("OpOp") if not ope or not target then return end local info = getCombatToolInfo(ope) if not info or not selectedCombatTool(ope, info) or not equipTool(ope, info) then return end local room = Workspace:FindFirstChild("OpeRoom" .. player.Name) local targetRoot = getModelRoot(target) local needsRoom = room == nil if room and targetRoot then needsRoom = not insideRoom(targetRoot.Position, room) end if needsRoom and targetRoot then pressSkillKey(ope, info, "Z", target) task.wait(0.12) end end local function getUsableTools() local result = {} local seen = {} local character = player.Character local backpack = player:FindFirstChild("Backpack") local function add(container) if not container then return end for _, item in ipairs(container:GetChildren()) do if item:IsA("Tool") and not seen[item] and item.Name ~= "LegacyPose" and not item.Name:match(" Key$") then local info = getCombatToolInfo(item) if info and selectedCombatTool(item, info) then seen[item] = true table.insert(result, { Tool = item, Info = info }) end end end end add(character) add(backpack) local priority = { Sword = 1, FightingStyle = 2, Fruit = 3 } table.sort(result, function(a, b) local pa = priority[a.Info.Kind] or 99 local pb = priority[b.Info.Kind] or 99 if pa ~= pb then return pa < pb end return a.Tool.Name < b.Tool.Name end) return result end SpawnState.lastDodgeAt = 0 SpawnState.fruitBusy = false SpawnState.lootBusy = false SpawnState.hopBusy = false SpawnState.tradeBusy = false SpawnState.combatBusy = false SpawnState.lastTradeInviteAt = 0 SpawnState.scriptStartedAt = 0 local function getDangerParts() local result = {} local effects = Workspace:FindFirstChild("Effects") if not effects then return result end for _, part in ipairs(effects:GetChildren()) do if part:IsA("BasePart") and (part.BrickColor == BrickColor.new("Really red") or part.Color == Color3.fromRGB(255, 0, 0)) then table.insert(result, part) end end return result end local function positionDangerous(position, parts) for _, part in ipairs(parts) do local radius = math.max(part.Size.X, part.Size.Z) * 0.5 + Config.DodgeRadius local flatA = Vector3.new(position.X, 0, position.Z) local flatB = Vector3.new(part.Position.X, 0, part.Position.Z) if (flatA - flatB).Magnitude <= radius then return true end end return false end local function validDodgePosition(position, boss, room, dangerParts) if room and not insideRoom(position, room) then return false end if positionDangerous(position, dangerParts) then return false end if boss and aliveModel(boss) then local bossRoot = getModelRoot(boss) if bossRoot and (position - bossRoot.Position).Magnitude > Config.MaxBossDistance then return false end end return true end local function findDodgePosition(root, boss, room, dangerParts) local origin = root.Position local baseDirection = Vector3.new(1, 0, 0) if boss and aliveModel(boss) then local bossRoot = getModelRoot(boss) local delta = Vector3.new(origin.X - bossRoot.Position.X, 0, origin.Z - bossRoot.Position.Z) if delta.Magnitude > 0.1 then baseDirection = delta.Unit end end for i = 0, 11 do local angle = math.rad(i * 30) local x = baseDirection.X * math.cos(angle) - baseDirection.Z * math.sin(angle) local z = baseDirection.X * math.sin(angle) + baseDirection.Z * math.cos(angle) local candidate = origin + Vector3.new(x, 0, z) * Config.DodgeStep if validDodgePosition(candidate, boss, room, dangerParts) then return candidate end end return origin end local function dodgeIfNeeded() if tradeGameplayLocked() or SpawnState.lootBusy or SpawnState.fruitBusy or not Config.AutoDodge then return false end local _, _, root = getCharacter() if not root then return false end local boss = lockedBoss and aliveModel(lockedBoss) and lockedBoss or getBoss() local dangerParts = getDangerParts() local inDanger = positionDangerous(root.Position, dangerParts) local tooClose = false if boss then local bossRoot = getModelRoot(boss) local radius = math.max(1, tonumber(Config.BossProximityDodgeRadius) or 8) tooClose = bossRoot and (root.Position - bossRoot.Position).Magnitude <= radius or false end if SpawnState.combatBusy and not inDanger then return false end if not inDanger and not tooClose then return false end local room = Workspace:FindFirstChild("OpeRoom" .. player.Name) local safe = findDodgePosition(root, boss, room, dangerParts) root.CFrame = CFrame.new(safe + Vector3.new(0, Config.DodgeHeight, 0)) SpawnState.lastDodgeAt = tick() return true end local function getTargetPart(instance) if not instance then return nil end if instance:IsA("BasePart") then return instance end if instance:IsA("Model") then return instance:FindFirstChild("Top") or instance.PrimaryPart or instance:FindFirstChildWhichIsA("BasePart", true) end return instance:FindFirstChildWhichIsA("BasePart", true) end local function moveToInstance(instance) local _, _, root = getCharacter() local targetPart = getTargetPart(instance) if root and targetPart then root.CFrame = targetPart.CFrame return true end return false end local function rewardSignature() local stats = player:FindFirstChild("PlayerStats") local beli = stats and stats:FindFirstChild("beli") local gem = stats and stats:FindFirstChild("Gem") local material = stats and stats:FindFirstChild("Material") local materialValue = material and material.Value or "" local backpack = player:FindFirstChild("Backpack") local character = player.Character local count = 0 if backpack then count = count + #backpack:GetChildren() end if character then for _, v in ipairs(character:GetChildren()) do if v:IsA("Tool") then count = count + 1 end end end return tostring(beli and beli.Value or 0) .. "|" .. tostring(gem and gem.Value or 0) .. "|" .. tostring(materialValue) .. "|" .. tostring(count) end local function getChestInteractionParts(chest) local result = {} local seen = {} local function add(part) if part and part:IsA("BasePart") and part.Parent and not seen[part] then seen[part] = true table.insert(result, part) end end if not chest or not chest.Parent then return result end if chest:IsA("BasePart") then add(chest) return result end for _, descendant in ipairs(chest:GetDescendants()) do if descendant:IsA("TouchTransmitter") then add(descendant.Parent) elseif descendant:IsA("ProximityPrompt") or descendant:IsA("ClickDetector") then local parent = descendant.Parent if parent and parent:IsA("BasePart") then add(parent) elseif parent then add(parent:FindFirstChildWhichIsA("BasePart", true)) end end end if chest:IsA("Model") then add(chest.PrimaryPart) end for _, name in ipairs({"RootPart", "Hitbox", "Main", "Base", "Bottom", "Top", "Handle"}) do local found = chest:FindFirstChild(name, true) if found then if found:IsA("BasePart") then add(found) else add(found:FindFirstChildWhichIsA("BasePart", true)) end end end for _, descendant in ipairs(chest:GetDescendants()) do if #result >= 12 then break end add(descendant) end return result end local function getChestInteractionPart(chest) local parts = getChestInteractionParts(chest) return parts[1] end local function fireChestInteraction(chest, beforeSignature, deadline) local _, _, root = getCharacter() if not root or not chest or not chest.Parent then return false, false end local parts = getChestInteractionParts(chest) if #parts == 0 then return false, false end local retries = math.max(1, math.floor(tonumber(Config.LootInteractionRetries) or 3)) local touchHold = math.max(0.03, tonumber(Config.LootTouchHold) or 0.07) local moveDelay = math.max(0.02, tonumber(Config.LootMoveDelay) or 0.08) local prompt = chest:FindFirstChildWhichIsA("ProximityPrompt", true) local clickDetector = chest:FindFirstChildWhichIsA("ClickDetector", true) local attempted = false local function expired() return deadline and tick() >= deadline end local function boundedWait(seconds) if expired() then return false end local waitTime = seconds if deadline then waitTime = math.min(waitTime, math.max(0, deadline - tick())) end if waitTime > 0 then task.wait(waitTime) end return not expired() end for _ = 1, retries do if not running() or not chest.Parent or expired() then break end if prompt and prompt.Parent and prompt.Enabled and type(fireproximityprompt) == "function" then pcall(function() fireproximityprompt(prompt) end) attempted = true if beforeSignature and rewardSignature() ~= beforeSignature then return true, true end end if clickDetector and clickDetector.Parent and type(fireclickdetector) == "function" then pcall(function() fireclickdetector(clickDetector) end) attempted = true if beforeSignature and rewardSignature() ~= beforeSignature then return true, true end end for _, targetPart in ipairs(parts) do if not running() or not chest.Parent or expired() then break end if targetPart and targetPart.Parent and targetPart:IsDescendantOf(Workspace) then local ok = pcall(function() root.AssemblyLinearVelocity = Vector3.new(0, 0, 0) root.AssemblyAngularVelocity = Vector3.new(0, 0, 0) root.CFrame = targetPart.CFrame end) if ok then attempted = true if not boundedWait(touchHold) then break end if type(firetouchinterest) == "function" then pcall(function() firetouchinterest(root, targetPart, 0) task.wait(0.025) firetouchinterest(root, targetPart, 1) end) end if beforeSignature and rewardSignature() ~= beforeSignature then return true, true end pcall(function() root.CFrame = targetPart.CFrame * CFrame.new(0, 0.75, 0) end) if not boundedWait(touchHold) then break end if beforeSignature and rewardSignature() ~= beforeSignature then return true, true end end end end if expired() then break end pcall(function() VirtualUser:ClickButton1(Vector2.new(300, 300)) end) if beforeSignature and rewardSignature() ~= beforeSignature then return true, true end if not boundedWait(moveDelay) then break end end return attempted, false end local function isRealChestModel(model) if not model or not model:IsA("Model") or not model.Parent then return false end local rootPart = model:FindFirstChild("RootPart") local chestAnim = model:FindFirstChild("ChestAnim") local controller = model:FindFirstChildOfClass("AnimationController") local coins = model:FindFirstChild("Coins") local name = tostring(model.Name or ""):lower() if rootPart and rootPart:IsA("BasePart") and chestAnim and controller then return true end if rootPart and rootPart:IsA("BasePart") and coins and name:find("chest", 1, true) then return true end if name:match("^chest%d*$") and getChestInteractionPart(model) then return true end return false end local function canonicalLootChest(chest) if not chest or not chest.Parent then return chest end local current = chest local nearestNamed = nil while current and current ~= Workspace do if current:IsA("Model") then if isRealChestModel(current) then return current end if not nearestNamed then local name = tostring(current.Name or ""):lower() if name:find("chest", 1, true) and getChestInteractionPart(current) then nearestNamed = current end end end current = current.Parent end return nearestNamed or chest end local function lootTargetKey(chest) chest = canonicalLootChest(chest) if not chest or not chest.Parent then return nil end local targetPart = nil if chest:IsA("Model") then local rootPart = chest:FindFirstChild("RootPart") if rootPart and rootPart:IsA("BasePart") then targetPart = rootPart end end targetPart = targetPart or getChestInteractionPart(chest) or getTargetPart(chest) if not targetPart then return "INSTANCE|" .. chest:GetFullName() end local position = targetPart.Position return table.concat({ "POS", tostring(math.floor(position.X * 2 + 0.5)), tostring(math.floor(position.Y * 2 + 0.5)), tostring(math.floor(position.Z * 2 + 0.5)) }, "|") end local function addUniqueChest(result, seen, chest) chest = canonicalLootChest(chest) if not chest or not chest.Parent then return end local key = lootTargetKey(chest) if not key or seen[key] then return end seen[key] = true table.insert(result, chest) end SpawnState.claimedLootKeys = SpawnState.claimedLootKeys or {} SpawnState.claimedLootInstances = SpawnState.claimedLootInstances or {} SpawnState.lootPriority = false local function lootAlreadyHandled(chest) chest = canonicalLootChest(chest) if not chest or not chest.Parent then return true end if SpawnState.claimedLootInstances[chest] == true then return true end local key = lootTargetKey(chest) return key ~= nil and SpawnState.claimedLootKeys[key] == true end local function markLootHandled(chest, key) chest = canonicalLootChest(chest) if chest and chest.Parent then SpawnState.claimedLootInstances[chest] = true end key = key or (chest and chest.Parent and lootTargetKey(chest)) if key then SpawnState.claimedLootKeys[key] = true end end local function stableRewardSignature(maxWait, stableSeconds) local started = tick() local changedAt = started local signature = rewardSignature() repeat task.wait(0.04) local current = rewardSignature() if current ~= signature then signature = current changedAt = tick() end until tick() - changedAt >= stableSeconds or tick() - started >= maxWait return signature end local function lootTargetConsumed(chest, prompt, targetPart) if not chest or not chest.Parent or not chest:IsDescendantOf(Workspace) then return true end if prompt and (not prompt.Parent or not prompt.Enabled) then return true end if targetPart and (not targetPart.Parent or not targetPart:IsDescendantOf(Workspace)) then return true end for _, name in ipairs({"Collected", "Taken"}) do if chest:GetAttribute(name) == true then return true end local marker = chest:FindFirstChild(name, true) if marker and marker:IsA("ValueBase") and (marker.Value == true or (tonumber(marker.Value) or 0) > 0) then return true end end local coins = chest:FindFirstChild("Coins", true) if coins and coins:IsA("BasePart") and coins.Transparency > 0.02 then return true end local rootPart = chest:FindFirstChild("RootPart", true) if rootPart then local openingSound = rootPart:FindFirstChild("OpeningSound") if openingSound and openingSound:IsA("Sound") and openingSound.IsPlaying then return true end local auraSound = rootPart:FindFirstChild("AuraSound") if auraSound and auraSound:IsA("Sound") and auraSound.IsPlaying then return true end end return false end local function isExcludedLootChest(chest) if not chest or not chest.Parent then return true end local current = chest while current and current ~= Workspace do local name = tostring(current.Name or ""):lower() if name:find("gacha", 1, true) or name:find("background", 1, true) then return true end current = current.Parent end return false end local function chestLike(instance) if not instance or not instance.Parent or isExcludedLootChest(instance) then return false end local name = tostring(instance.Name or ""):lower() if not name:find("chest", 1, true) then return false end if instance:IsA("Model") then return isRealChestModel(instance) or getChestInteractionPart(instance) ~= nil end if instance:IsA("BasePart") then local canonical = canonicalLootChest(instance) if canonical and canonical ~= instance and canonical:IsA("Model") then return true end return instance.Parent == Workspace and name:match("^chest%d+$") ~= nil end return false end local function addChestsFromContainer(result, seen, container) if not container or not container.Parent then return end if chestLike(container) then addUniqueChest(result, seen, container) end for _, descendant in ipairs(container:GetDescendants()) do if chestLike(descendant) then local candidate = descendant if descendant:IsA("BasePart") then local ancestor = descendant.Parent while ancestor and ancestor ~= container.Parent and ancestor ~= Workspace do if ancestor:IsA("Model") and chestLike(ancestor) then candidate = ancestor end ancestor = ancestor.Parent end end addUniqueChest(result, seen, candidate) end end end local function islandMatchesLootBoss(island, bossName) if not island then return false end local name = tostring(island.Name or ""):lower() if bossName == "HydraSeaKing" then return name:find("hydra", 1, true) ~= nil or name:find("sea king thunder", 1, true) ~= nil or name:find("sea king water", 1, true) ~= nil or name:find("sea king lava", 1, true) ~= nil end if bossName == "SeaKing" then return name:find("legacy island", 1, true) ~= nil or name:find("legacy", 1, true) ~= nil end return false end local function getLootTargets(kind, bossName) local result = {} local seen = {} if kind == "GhostShip" then for i = 1, math.max(6, tonumber(Config.GhostMaxChests) or 6) do addUniqueChest(result, seen, Workspace:FindFirstChild("Chest" .. i)) end for _, child in ipairs(Workspace:GetChildren()) do if child.Name:match("^Chest%d+$") then addUniqueChest(result, seen, child) end end return result end local islands = Workspace:FindFirstChild("Island") if not islands then return result end for _, island in ipairs(islands:GetChildren()) do if islandMatchesLootBoss(island, bossName) then addChestsFromContainer(result, seen, island) end end if #result == 0 then if bossName == "HydraSeaKing" then for _, oldName in ipairs({"Sea King Thunder", "Sea King Water", "Sea King Lava"}) do local island = islands:FindFirstChild(oldName) if island then addChestsFromContainer(result, seen, island) end end elseif bossName == "SeaKing" then for i = 1, 4 do local island = islands:FindFirstChild("Legacy Island" .. i) if island then addChestsFromContainer(result, seen, island) end end end end return result end local function collectLoot(kind, bossName) if SpawnState.lootBusy or not Config.AutoCollectLoot then return false, 0, false end SpawnState.lootBusy = true SpawnState.lootPriority = true ensureFreeze(false) setStatus("Đang nhặt Chest...", 100, 2) local started = tick() local timeout = math.max(1, tonumber(Config.LootTimeout) or 30) local deadline = started + timeout local spawnGrace = math.max(1, tonumber(Config.LootSpawnGrace) or 5) if incomingApiTarget and tostring(incomingApiTarget.status or "") == "chest" and apiBossName and apiBossName(incomingApiTarget.boss) == apiBossName(bossName) then spawnGrace = math.max(spawnGrace, 5) end local quietSeconds = math.max(0.2, tonumber(Config.LootQuietSeconds) or 0.45) local cycleDelay = math.max(0.01, tonumber(Config.LootCycleDelay) or 0.03) local sameChestRetry = math.max(0.05, tonumber(Config.LootSameChestRetry) or 0.12) local confirmTimeout = math.max(0.1, tonumber(Config.LootConfirmTimeout) or 0.4) local stableSeconds = math.max(0.03, tonumber(Config.LootRewardStableSeconds) or 0.06) local settleDelay = math.max(0.05, tonumber(Config.LootClaimSettleDelay) or 0.18) local attemptedAt = {} local collectedCount = 0 local sawChest = false local emptySince = nil local completed = false local interruptedByBoss = false local cursor = 0 while running() and tick() < deadline do local priorityBoss = getBoss() if priorityBoss and aliveModel(priorityBoss) then interruptedByBoss = true break end local targets = getLootTargets(kind, bossName) local pending = {} local pendingSeen = {} for _, chest in ipairs(targets) do chest = canonicalLootChest(chest) local key = chest and chest.Parent and lootTargetKey(chest) or nil if key and not pendingSeen[key] and not lootAlreadyHandled(chest) then pendingSeen[key] = true sawChest = true table.insert(pending, {Chest = chest, Key = key}) end end table.sort(pending, function(a, b) return a.Key < b.Key end) local remaining = #pending local remainingTime = math.max(0, math.ceil(deadline - tick())) setStatus("Đang nhặt Chest: " .. tostring(collectedCount) .. " | Còn: " .. tostring(remaining) .. " | " .. tostring(remainingTime) .. "s", 100, 1) if remaining > 0 then emptySince = nil local selected = nil for _ = 1, remaining do cursor = (cursor % remaining) + 1 local entry = pending[cursor] local chest = entry and entry.Chest if chest and chest.Parent and (not attemptedAt[entry.Key] or tick() - attemptedAt[entry.Key] >= sameChestRetry) then selected = entry break end end if selected and tick() < deadline then attemptedAt[selected.Key] = tick() local chest = selected.Chest local prompt = chest:FindFirstChildWhichIsA("ProximityPrompt", true) local targetPart = getChestInteractionPart(chest) or getTargetPart(chest) local signatureWait = math.min(sameChestRetry, math.max(0, deadline - tick())) local before = rewardSignature() if signatureWait > 0 then before = stableRewardSignature(signatureWait, math.min(stableSeconds, signatureWait)) end local interacted, instantConfirmed = fireChestInteraction(chest, before, deadline) if interacted and tick() < deadline then local interactionStarted = tick() local interactionDeadline = math.min(deadline, interactionStarted + confirmTimeout) local confirmed = instantConfirmed repeat if confirmed or tick() >= interactionDeadline then break end task.wait(math.min(0.03, math.max(0, interactionDeadline - tick()))) confirmed = rewardSignature() ~= before or lootTargetConsumed(chest, prompt, targetPart) until not running() or tick() >= interactionDeadline if running() and not confirmed and tick() < deadline then local waitTime = math.min(settleDelay, math.max(0, deadline - tick())) if waitTime > 0 then task.wait(waitTime) end confirmed = rewardSignature() ~= before or lootTargetConsumed(chest, prompt, targetPart) end if confirmed then markLootHandled(chest, selected.Key) attemptedAt[selected.Key] = nil collectedCount = collectedCount + 1 end end end if tick() < deadline then task.wait(math.min(cycleDelay, math.max(0, deadline - tick()))) end else if sawChest or collectedCount > 0 then emptySince = emptySince or tick() if tick() - emptySince >= quietSeconds then completed = true break end elseif tick() - started >= spawnGrace then break end if tick() < deadline then task.wait(math.min(0.05, math.max(0, deadline - tick()))) end end end local timedOut = not completed and not interruptedByBoss and tick() >= deadline if timedOut then for _, chest in ipairs(getLootTargets(kind, bossName)) do if chest and chest.Parent and not lootAlreadyHandled(chest) then markLootHandled(chest) end end end if interruptedByBoss then setStatus("Có boss mới | Dừng nhặt Chest để đánh boss", 100, 1) elseif completed then setStatus("Đã xử lý hết Chest: " .. tostring(collectedCount), 100, 2) elseif timedOut then setStatus("Chest timeout 30s | Hop ngay | Đã nhặt: " .. tostring(collectedCount), 100, 2) else setStatus("Không còn Chest chưa nhặt", 100, 2) end SpawnState.lootBusy = false SpawnState.lootPriority = false return completed, collectedCount, timedOut, interruptedByBoss end local historyFolder = tostring(Config.ServerHistoryFolder or "KLSeaFarmV3") local historyFile = historyFolder .. "/server_history_" .. tostring(player.UserId) .. ".json" local apiPendingFile = historyFolder .. "/api_pending_" .. tostring(player.UserId) .. ".json" local tradePendingFile = historyFolder .. "/trade_pending_" .. tostring(player.UserId) .. ".json" local history = {success = {}, scan = {}} local Network = nil pcall(function() local assets = ChestRoot and ChestRoot:FindFirstChild("Assets") local modules = assets and assets:FindFirstChild("Modules") local networkModule = modules and modules:FindFirstChild("Network") if networkModule then Network = require(networkModule) end end) local function canUseFiles() return type(isfile) == "function" and type(readfile) == "function" and type(writefile) == "function" end local function ensureHistoryFolder() if type(isfolder) == "function" and type(makefolder) == "function" and not isfolder(historyFolder) then pcall(makefolder, historyFolder) end end local function cleanHistory() local now = os.time() local successTTL = math.max(60, tonumber(Config.ServerSuccessCooldown) or 300) local scanTTL = math.max(10, tonumber(Config.ServerScanCooldown) or 30) for id, ts in pairs(history.success) do if type(ts) ~= "number" or now - ts >= successTTL then history.success[id] = nil end end for id, ts in pairs(history.scan) do if type(ts) ~= "number" or now - ts >= scanTTL then history.scan[id] = nil end end end local function saveHistory() if not canUseFiles() then return end ensureHistoryFolder() cleanHistory() pcall(function() writefile(historyFile, HttpService:JSONEncode(history)) end) end local function loadHistory() if not canUseFiles() then return end ensureHistoryFolder() if not isfile(historyFile) then saveHistory() return end local ok, raw = pcall(readfile, historyFile) if ok and type(raw) == "string" then local ok2, data = pcall(function() return HttpService:JSONDecode(raw) end) if ok2 and type(data) == "table" then history.success = type(data.success) == "table" and data.success or {} history.scan = type(data.scan) == "table" and data.scan or {} end end cleanHistory() end local function savePendingApiTarget(server) if not canUseFiles() or not server or not server.FromAPI then return end ensureHistoryFolder() pcall(function() writefile(apiPendingFile, HttpService:JSONEncode({ KLApi = true, job_id = tostring(server.JobId or ""), event_id = tostring(server.EventId or ""), boss = tostring(server.TargetKind or ""), status = tostring(server.TargetStatus or "spawn"), verified = server.Verified == true, saved_at = os.time() })) end) end local function clearPendingApiTarget() if not canUseFiles() then return end pcall(function() if type(delfile) == "function" and isfile(apiPendingFile) then delfile(apiPendingFile) elseif isfile(apiPendingFile) then writefile(apiPendingFile, "{}") end end) end local function savePendingTradeTarget(data) if not canUseFiles() or type(data) ~= "table" then return end ensureHistoryFolder() local payload = { KLTrade = true, receiver_name = tostring(data.receiver_name or ""), receiver_id = tostring(data.receiver_id or ""), job_id = tostring(data.job_id or ""), place_id = tonumber(data.place_id) or game.PlaceId, channel = tostring(data.channel or Config.TradeChannel or "default"), saved_at = os.time() } pcall(function() writefile(tradePendingFile, HttpService:JSONEncode(payload)) end) end local function clearPendingTradeTarget() if not canUseFiles() then return end pcall(function() if type(delfile) == "function" and isfile(tradePendingFile) then delfile(tradePendingFile) elseif isfile(tradePendingFile) then writefile(tradePendingFile, "{}") end end) end local function loadPendingTradeTarget() if incomingTradeTarget or Config.TradeReceiverMode == true or not canUseFiles() then return end ensureHistoryFolder() if not isfile(tradePendingFile) then return end local ok, raw = pcall(readfile, tradePendingFile) if not ok or type(raw) ~= "string" or raw == "" then return end local ok2, data = pcall(function() return HttpService:JSONDecode(raw) end) if not ok2 or type(data) ~= "table" or data.KLTrade ~= true then return end local savedAt = tonumber(data.saved_at) or 0 if savedAt > 0 and os.time() - savedAt > 180 then clearPendingTradeTarget() return end if tostring(data.job_id or "") == game.JobId then incomingTradeTarget = data SpawnState.tradeMissionActive = true end end local function loadPendingApiTarget() if incomingApiTarget or not canUseFiles() then return end ensureHistoryFolder() if not isfile(apiPendingFile) then return end local ok, raw = pcall(readfile, apiPendingFile) if not ok or type(raw) ~= "string" or raw == "" then return end local ok2, data = pcall(function() return HttpService:JSONDecode(raw) end) if ok2 and type(data) == "table" and data.KLApi == true and tostring(data.job_id or "") == game.JobId then incomingApiTarget = data clearPendingApiTarget() elseif ok2 and type(data) == "table" and tonumber(data.saved_at) and os.time() - tonumber(data.saved_at) > 120 then clearPendingApiTarget() end end loadPendingApiTarget() loadPendingTradeTarget() local function canJoinServer(jobId) if not jobId or jobId == "" or jobId == game.JobId then return false end cleanHistory() return history.success[jobId] == nil and history.scan[jobId] == nil end local function getLocalExcludedServerJobs() cleanHistory() local ids = {} local seen = {} for id in pairs(history.success) do id = tostring(id or "") if id ~= "" and id ~= game.JobId and not seen[id] then seen[id] = true table.insert(ids, id) end end for id in pairs(history.scan) do id = tostring(id or "") if id ~= "" and id ~= game.JobId and not seen[id] then seen[id] = true table.insert(ids, id) end end table.sort(ids) return ids end local function markServerScan(jobId) if jobId and jobId ~= "" then history.scan[jobId] = os.time() saveHistory() end end local function unmarkServerScan(jobId) if jobId then history.scan[jobId] = nil saveHistory() end end local function markServerSuccess(jobId) if jobId and jobId ~= "" then history.success[jobId] = os.time() history.scan[jobId] = os.time() saveHistory() end end loadHistory() math.randomseed(math.floor(tick() * 1000) % 2147483647) local uptimeWindows = { {4 * 3600 + 21 * 60, 4 * 3600 + 30 * 60}, {8 * 3600 + 52 * 60, 9 * 3600 + 1 * 60}, {59 * 60 + 1, 1 * 3600 + 7 * 60}, {2 * 3600 + 7 * 60, 2 * 3600 + 14 * 60}, {3 * 3600 + 14 * 60, 3 * 3600 + 21 * 60}, {5 * 3600 + 31 * 60, 5 * 3600 + 37 * 60}, {13 * 3600 + 28 * 60, 13 * 3600 + 35 * 60}, {18 * 3600 + 10 * 60, 18 * 3600 + 17 * 60}, {7 * 3600 + 45 * 60, 7 * 3600 + 52 * 60}, {6 * 3600 + 38 * 60, 6 * 3600 + 45 * 60}, {10 * 3600 + 3 * 60, 10 * 3600 + 9 * 60}, {11 * 3600 + 11 * 60, 11 * 3600 + 17 * 60} } local function normalizeServer(server) if type(server) ~= "table" then return nil end local createdAt = tonumber(server.CreatedAt) local serverOsTime = tonumber(server.ServerOsTime) local uptime = nil if createdAt then uptime = math.max(0, os.time() - createdAt) elseif serverOsTime then uptime = math.max(0, os.time() - serverOsTime) end return { JobId = server.JobId or server.jobId or server.id, PlaceId = tonumber(server.PlaceId or server.placeId) or game.PlaceId, PlayerCount = tonumber(server.PlayerCount or server.GetPlayers or server.players or server.playing) or 0, ServerName = server.Name or server.ServerName or server.name, Region = server.Region or server.region, CreatedAt = createdAt or serverOsTime, Uptime = uptime } end SpawnState.serverCache = nil SpawnState.serverCacheAt = 0 SpawnState.serverListInvokes = {} local requestJSON local function timedServerListCall(key, callback) if SpawnState.serverListInvokes[key] then return nil end local finished = false local succeeded = false local result = nil SpawnState.serverListInvokes[key] = true task.spawn(function() succeeded, result = pcall(callback) finished = true SpawnState.serverListInvokes[key] = nil end) local started = tick() local timeout = math.max(0.25, tonumber(Config.ServerListTimeout) or 0.8) repeat task.wait(0.03) until finished or not running() or tick() - started >= timeout if finished and succeeded and type(result) == "table" then return result end return nil end local function getServerList() if SpawnState.serverCache and tick() - SpawnState.serverCacheAt < math.max(0.5, tonumber(Config.ServerListCacheSeconds) or 1.5) then return SpawnState.serverCache end local raw = nil if Network then raw = timedServerListCall("Network", function() return Network:InvokeServer("GetServerLists") end) end if (type(raw) ~= "table" or next(raw) == nil) and GetServers then raw = timedServerListCall("GetServers", function() return GetServers:InvokeServer() end) end if (type(raw) ~= "table" or next(raw) == nil) and requestJSON then local publicServers = requestJSON("GET", "https://games.roblox.com/v1/games/" .. tostring(game.PlaceId) .. "/servers/Public?sortOrder=Asc&excludeFullGames=true&limit=100") if type(publicServers) == "table" and type(publicServers.data) == "table" then raw = publicServers.data end end if type(raw) ~= "table" then return nil end local normalized = {} for _, v in pairs(raw) do local server = normalizeServer(v) if server and server.JobId then table.insert(normalized, server) end end SpawnState.serverCache = normalized SpawnState.serverCacheAt = tick() return normalized end local function currentServerMeta() local list = getServerList() if list then for _, server in ipairs(list) do if server.JobId == game.JobId then return server end end end return { JobId = game.JobId, PlaceId = game.PlaceId, PlayerCount = #Players:GetPlayers(), ServerName = nil, Region = nil, CreatedAt = nil, Uptime = nil } end local TradeFlow = {} local function requestFunction() local env = type(getgenv) == "function" and getgenv() or _G if type(syn) == "table" and type(syn.request) == "function" then return syn.request end if type(env.request) == "function" then return env.request end if type(env.http_request) == "function" then return env.http_request end if type(http_request) == "function" then return http_request end return nil end requestJSON = function(method, url, body, timeoutOverride) if type(url) ~= "string" or url == "" then return nil end local req = requestFunction() if not req then return nil end local timeoutValue = math.max(1, tonumber(timeoutOverride) or tonumber(Config.ApiRequestTimeout) or 5) local maxPending = math.max(1, tonumber(Config.ApiMaxPendingRequests) or 2) local queueWait = math.max(0, tonumber(Config.ApiRequestQueueWait) or 2) local queueStarted = tick() while running() and (tonumber(SpawnState.httpRequestsInFlight) or 0) >= maxPending and tick() - queueStarted < queueWait do task.wait(0.03) end local pending = tonumber(SpawnState.httpRequestsInFlight) or 0 if pending >= maxPending then return nil end local encodedBody = nil if body ~= nil then local encodedOk, encoded = pcall(function() return HttpService:JSONEncode(body) end) if not encodedOk then return nil end encodedBody = encoded end local finished = false local succeeded = false local response = nil SpawnState.httpRequestsInFlight = pending + 1 task.spawn(function() local ok, result = pcall(req, { Url = url, Method = method, Headers = { ["Content-Type"] = "application/json", ["Cache-Control"] = "no-cache" }, Body = encodedBody, Timeout = timeoutValue }) succeeded = ok response = result SpawnState.httpRequestsInFlight = math.max(0, (tonumber(SpawnState.httpRequestsInFlight) or 1) - 1) finished = true end) local started = tick() repeat task.wait(0.03) until finished or not running() or tick() - started >= timeoutValue if not finished or not succeeded then return nil end if type(response) == "string" then if response == "" then return true end local decodedOk, decoded = pcall(function() return HttpService:JSONDecode(response) end) return decodedOk and decoded or true end if type(response) ~= "table" then return nil end local status = tonumber(response.StatusCode or response.Status or response.status_code) or 0 if status ~= 0 and (status < 200 or status >= 300) then return nil end local responseBody = response.Body or response.body if type(responseBody) ~= "string" or responseBody == "" then return true end local decodedOk, decoded = pcall(function() return HttpService:JSONDecode(responseBody) end) return decodedOk and decoded or true end SpawnState.apiLastReportAt = 0 SpawnState.apiLastReportResponse = nil SpawnState.apiReportBusy = false SpawnState.apiCurrentClaimToken = nil SpawnState.apiCurrentClaimJobId = nil SpawnState.apiCurrentClaimEventId = nil SpawnState.apiCurrentClaimBoss = nil SpawnState.pendingTeleportSource = nil local function apiEnabled() return Config.ApiEnabled and type(Config.ApiURL) == "string" and Config.ApiURL ~= "" and not Config.ApiURL:find("YOUR%-DOMAIN", 1, false) end local function apiCall(action, payload) if not apiEnabled() then return nil end payload = type(payload) == "table" and payload or {} payload.action = action return requestJSON("POST", Config.ApiURL, payload) end local function apiBossName(modelOrName) local name = typeof(modelOrName) == "Instance" and modelOrName.Name or tostring(modelOrName or "") if name == "Ghost Ship" or name == "GhostShip" then return "GhostShip" end if name == "HydraSeaKing" then return "HydraSeaKing" end if name == "SeaKing" then return "SeaKing" end return nil end local function seaHydraJoinHoldActive() if not targetModeAllows("SeaHydra") then return false end local startedAt = tonumber(SpawnState.scriptStartedAt) or 0 if startedAt <= 0 then startedAt = tick() end local grace = math.max(5, tonumber(Config.SeaHydraJoinGrace) or 20) if tick() - startedAt >= grace then return false end if type(incomingApiTarget) == "table" then local boss = apiBossName(incomingApiTarget.boss) if boss == "SeaKing" or boss == "HydraSeaKing" then return true end end local islands = Workspace:FindFirstChild("Island") if islands then for _, island in ipairs(islands:GetChildren()) do local name = tostring(island.Name or ""):lower() if name:find("legacy island", 1, true) or name:find("hydra island", 1, true) or name:find("sea king thunder", 1, true) or name:find("sea king water", 1, true) or name:find("sea king lava", 1, true) then return true end end end return false end local function apiAliveBosses() local result = {} local seaFolder = Workspace:FindFirstChild("SeaMonster") local ghostFolder = Workspace:FindFirstChild("GhostMonster") if seaFolder then for _, model in ipairs(seaFolder:GetChildren()) do if model:IsA("Model") and aliveModel(model) and (model.Name == "SeaKing" or model.Name == "HydraSeaKing") then table.insert(result, model.Name) end end end if ghostFolder then for _, model in ipairs(ghostFolder:GetChildren()) do if model:IsA("Model") and aliveModel(model) and model.Name == "Ghost Ship" then table.insert(result, "GhostShip") end end end return result end local function hasUnclaimedLoot(kind, bossName) for _, chest in ipairs(getLootTargets(kind, bossName)) do if chest and chest.Parent and not lootAlreadyHandled(chest) then return true end end return false end local function apiChestBosses() local result = {} if hasUnclaimedLoot("GhostShip", "Ghost Ship") then table.insert(result, "GhostShip") end if hasUnclaimedLoot("SeaHydra", "HydraSeaKing") then table.insert(result, "HydraSeaKing") end if hasUnclaimedLoot("SeaHydra", "SeaKing") then table.insert(result, "SeaKing") end return result end local function getAvailableLootGroup() if tradeGameplayLocked() then return nil, nil, nil end if targetModeAllows("GhostShip") and hasUnclaimedLoot("GhostShip", "Ghost Ship") then return "GhostShip", "Ghost Ship", "GhostShip" end if targetModeAllows("SeaHydra") and hasUnclaimedLoot("SeaHydra", "HydraSeaKing") then return "SeaHydra", "HydraSeaKing", "HydraSeaKing" end if targetModeAllows("SeaHydra") and hasUnclaimedLoot("SeaHydra", "SeaKing") then return "SeaHydra", "SeaKing", "SeaKing" end return nil, nil, nil end local function apiReportCurrent(force) if Config.TradeReceiverMode == true then return nil end if not apiEnabled() then return nil end local cooldown = math.max(0.5, tonumber(Config.ApiReportCooldown) or 2) if not force and tick() - SpawnState.apiLastReportAt < cooldown then return SpawnState.apiLastReportResponse end if SpawnState.apiReportBusy then local waitStarted = tick() local maxWait = math.min(2, math.max(0.5, tonumber(Config.ApiRequestTimeout) or 5)) repeat task.wait(0.03) until not SpawnState.apiReportBusy or not running() or tick() - waitStarted >= maxWait return SpawnState.apiLastReportResponse end SpawnState.apiReportBusy = true updateSeaSpawnState() updateGhostSpawnState() local seaSeconds, ghostSeconds = getSpawnTimers() local response = apiCall("report", { job_id = game.JobId, place_id = game.PlaceId, reporter = tostring(player.UserId), player_count = #Players:GetPlayers(), sea_seconds = seaSeconds, ghost_seconds = ghostSeconds, hydra = SpawnState.Hydra == true, alive_bosses = apiAliveBosses(), chest_bosses = apiChestBosses() }) SpawnState.apiReportBusy = false if response then SpawnState.apiLastReportAt = tick() SpawnState.apiLastReportResponse = response end return response end local function apiEventIdFromReport(response, bossName, eventType) if type(response) ~= "table" then return nil end local boss = apiBossName(bossName) if not boss then return nil end local map = nil if eventType == "spawn" then map = response.spawn_event_ids elseif eventType == "chest" then map = response.chest_event_ids elseif eventType == "waiting" then map = response.waiting_event_ids end if type(map) == "table" and map[boss] then return tostring(map[boss]) end local ids = response.event_ids if type(ids) == "table" and ids[boss] then return tostring(ids[boss]) end local events = response.events if type(events) == "table" then for _, event in pairs(events) do if type(event) == "table" and tostring(event.boss or "") == boss then if not eventType or tostring(event.status or "") == eventType then if event.event_id then return tostring(event.event_id) end end end end end return nil end local function apiVerifyBoss(model) local response = apiReportCurrent(true) return apiEventIdFromReport(response, model, "spawn") end local function apiChestReady(bossName, eventId) local boss = apiBossName(bossName) if not boss then return nil end local response = apiCall("chest", { job_id = game.JobId, place_id = game.PlaceId, reporter = tostring(player.UserId), player_count = #Players:GetPlayers(), boss = boss, event_id = eventId or "" }) if type(response) == "table" and response.event_id then return tostring(response.event_id), response end return eventId, response end local function apiLootDone(bossName, eventId, count, found) local boss = apiBossName(bossName) if not boss then return nil end return apiCall("loot_done", { job_id = game.JobId, reporter = tostring(player.UserId), boss = boss, event_id = eventId or "", count = tonumber(count) or 0, found = found == true }) end local function apiReleaseClaim(jobId, claimToken) if not apiEnabled() or not jobId or jobId == "" then return nil end return apiCall("release", { job_id = jobId, reporter = tostring(player.UserId), claim_token = claimToken or "" }) end local function apiClaimTarget() local response = apiCall("claim", { reporter = tostring(player.UserId), current_job = game.JobId, place_id = game.PlaceId, exclude_jobs = getLocalExcludedServerJobs(), allow_sea_hydra = targetModeAllows("SeaHydra"), allow_ghost_ship = targetModeAllows("GhostShip") }) if type(response) ~= "table" or type(response.target) ~= "table" then return nil end local target = response.target local jobId = tostring(target.job_id or "") if jobId == "" or jobId == game.JobId then return nil end local claimToken = tostring(target.claim_token or target.dispatch_token or "") if not canJoinServer(jobId) then apiReleaseClaim(jobId, claimToken) return nil end SpawnState.apiCurrentClaimToken = claimToken SpawnState.apiCurrentClaimJobId = jobId SpawnState.apiCurrentClaimEventId = target.event_id and tostring(target.event_id) or nil SpawnState.apiCurrentClaimBoss = tostring(target.boss or "") return { JobId = jobId, PlaceId = tonumber(target.place_id) or game.PlaceId, PlayerCount = tonumber(target.player_count) or 0, ServerName = "API", TargetKind = tostring(target.boss or "Boss"), TargetStatus = tostring(target.status or "spawn"), Timer = tonumber(target.seconds_to_spawn) or 0, Verified = target.verified == true, Prejoin = target.prejoin == true, IsChest = tostring(target.status or "") == "chest", EventId = target.event_id, ClaimToken = target.claim_token or target.dispatch_token, FromAPI = true } end local function apiReserveServerScan(server) if not apiEnabled() or not Config.ApiReserveScan then return true end local response = apiCall("reserve_scan", { job_id = server.JobId, place_id = server.PlaceId or game.PlaceId, player_count = server.PlayerCount or 0, reporter = tostring(player.UserId) }) if type(response) ~= "table" or response.reserved == nil then return true end return response.reserved == true end local function apiFailTeleport(jobId, reason, claimToken) if not apiEnabled() or not jobId or jobId == "" then return nil end return apiCall("fail", { job_id = jobId, reporter = tostring(player.UserId), claim_token = claimToken or "", reason = tostring(reason or "Failure") }) end local function apiClearClaim() SpawnState.apiCurrentClaimToken = nil SpawnState.apiCurrentClaimJobId = nil SpawnState.apiCurrentClaimEventId = nil SpawnState.apiCurrentClaimBoss = nil end local function uptimeDistance(uptime) if type(uptime) ~= "number" then return 999999 end local best = 999999 for _, window in ipairs(uptimeWindows) do if uptime >= window[1] and uptime <= window[2] then return 0 end best = math.min(best, math.abs(uptime - window[1]), math.abs(uptime - window[2])) end return best end local function findFallbackServer() local list = getServerList() if not list then return nil end local candidates = {} for _, server in ipairs(list) do if server.JobId ~= game.JobId and server.PlaceId == game.PlaceId and canJoinServer(server.JobId) and server.PlayerCount < tonumber(Config.MaxServerPlayers) then server.Score = uptimeDistance(server.Uptime) + server.PlayerCount * 4 + math.random() table.insert(candidates, server) end end table.sort(candidates, function(a, b) return a.Score < b.Score end) if #candidates == 0 then return nil end local limit = math.min(#candidates, math.max(1, tonumber(Config.ScoutReserveAttempts) or 8)) for i = 1, limit do local server = candidates[i] if apiReserveServerScan(server) then server.FromAPI = false return server end end return nil end SpawnState.pendingTeleportJobId = nil SpawnState.teleportRetryAfter = 0 local function hopServer(force) if Config.TradeReceiverMode == true or SpawnState.tradeMissionActive == true then return false end if Config.AutoTrade and TradeFlow.receiverName and TradeFlow.receiverName() ~= "" and TradeFlow.hasTrigger and TradeFlow.hasTrigger() then SpawnState.tradeMissionActive = true SpawnState.tradeMissionStartedAt = SpawnState.tradeMissionStartedAt > 0 and SpawnState.tradeMissionStartedAt or tick() setStatus("TRADE đủ ngưỡng | Dừng hop để chuyển đồ", 100, 2) return false end if not targetModeAllows("SeaHydra") and not targetModeAllows("GhostShip") then return false end if SpawnState.hopBusy or SpawnState.lootBusy or SpawnState.lootPriority or not Config.AutoHop or not running() or not isGameplayReady() then return false end if lockedBoss and aliveModel(lockedBoss) then return false end local liveBoss = getBoss() if liveBoss then return false end local candidate, candidateState = getBossCandidate() if candidate and candidateState ~= "dead" then return false end local lootKind = Config.AutoCollectLoot and select(1, getAvailableLootGroup()) or nil if lootKind and not force then return false end if not force and seaHydraJoinHoldActive() then return false end if tick() < SpawnState.teleportRetryAfter then return false end SpawnState.hopBusy = true setStatus("Đang cập nhật API...", 95, 2) apiReportCurrent(true) local server = apiClaimTarget() if server then SpawnState.pendingTeleportSource = "api" local apiState = server.IsChest and " CHEST" or (server.Verified and " VERIFIED" or (server.Prejoin and " PRESPAWN" or " SPAWN")) setStatus("API: " .. tostring(server.TargetKind) .. apiState, 100, 4) else SpawnState.pendingTeleportSource = "scout" apiClearClaim() setStatus("API hết boss | Đang scout server...", 95, 2) server = findFallbackServer() end if not server then setStatus("Chưa có server phù hợp, đang thử lại...", 90, 2) SpawnState.hopBusy = false return false end markServerScan(server.JobId) SpawnState.pendingTeleportJobId = server.JobId local pendingClaimToken = server.ClaimToken and tostring(server.ClaimToken) or SpawnState.apiCurrentClaimToken notify("Hop: " .. tostring(server.ServerName or server.JobId) .. (server.TargetKind and (" | " .. server.TargetKind .. " | " .. tostring(server.Timer) .. "s") or ""), 3) setStatus(server.FromAPI and "Đang vào server API..." or "Đang hop scout...", 100, 10) if server.FromAPI then savePendingApiTarget(server) end local ok = false if server.FromAPI then ok = pcall(function() TeleportService:TeleportToPlaceInstance(server.PlaceId or game.PlaceId, server.JobId, player, nil, { KLApi = true, event_id = tostring(server.EventId or ""), boss = tostring(server.TargetKind or ""), status = tostring(server.TargetStatus or "spawn"), verified = server.Verified == true }) end) if not ok then ok = pcall(function() TeleportService:TeleportToPlaceInstance(server.PlaceId or game.PlaceId, server.JobId, player) end) end else ok = pcall(function() TeleportService:TeleportToPlaceInstance(server.PlaceId or game.PlaceId, server.JobId, player) end) end if not ok then apiFailTeleport(server.JobId, "Failure", pendingClaimToken) clearPendingApiTarget() SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil apiClearClaim() SpawnState.serverCache = nil SpawnState.serverCacheAt = 0 SpawnState.teleportRetryAfter = tick() + math.max(0.25, tonumber(Config.TeleportFailureCooldown) or 0.35) setStatus("Hop lỗi, đổi server khác...", 100, math.max(0.25, tonumber(Config.TeleportFailureCooldown) or 0.35)) SpawnState.hopBusy = false return false end local watchdog = math.max(1.5, tonumber(Config.TeleportWatchdog) or 3) task.delay(watchdog, function() if running() and SpawnState.pendingTeleportJobId == server.JobId then apiFailTeleport(server.JobId, "TeleportTimeout", pendingClaimToken) clearPendingApiTarget() SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil apiClearClaim() SpawnState.serverCache = nil SpawnState.serverCacheAt = 0 SpawnState.teleportRetryAfter = tick() + math.max(0.25, tonumber(Config.TeleportFailureCooldown) or 0.35) setStatus("Server không phản hồi | Đổi server khác...", 100, 1) SpawnState.hopBusy = false end end) return true end pcall(function() TeleportService.TeleportInitFailed:Connect(function(failedPlayer, teleportResult) if failedPlayer ~= player then return end local failedJobId = SpawnState.pendingTeleportJobId local reason = tostring(teleportResult or "Failure") if SpawnState.pendingTeleportSource == "trade" then TradeFlow.deliveryDone(SpawnState.tradeTargetReceiverName or TradeFlow.receiverName(), player.Name) clearPendingTradeTarget() SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil SpawnState.hopBusy = false SpawnState.tradeRetryAfter = tick() + math.max(1, tonumber(Config.TradeJoinRetryDelay) or 3) setStatus("TRADE | Teleport lỗi: " .. reason .. " | thử lại", 100, 2) return end if failedJobId then apiFailTeleport(failedJobId, reason, SpawnState.apiCurrentClaimToken) clearPendingApiTarget() end SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil apiClearClaim() SpawnState.serverCache = nil SpawnState.serverCacheAt = 0 SpawnState.teleportRetryAfter = tick() + math.max(0.25, tonumber(Config.TeleportFailureCooldown) or 0.35) setStatus("Teleport lỗi: " .. reason .. " | Đổi server", 100, math.max(0.25, tonumber(Config.TeleportFailureCooldown) or 0.35)) SpawnState.hopBusy = false end) end) local function queueSelf() if type(Config.LoaderURL) ~= "string" or Config.LoaderURL == "" then return end local q = nil if type(queue_on_teleport) == "function" then q = queue_on_teleport elseif type(syn) == "table" and type(syn.queue_on_teleport) == "function" then q = syn.queue_on_teleport end if q then local code = 'loadstring(game:HttpGet(' .. string.format('%q', Config.LoaderURL) .. ', true))()' pcall(q, code) end end queueSelf() SpawnState.availableFruitSet = {} if FruitFolder then for _, fruit in ipairs(FruitFolder:GetChildren()) do SpawnState.availableFruitSet[fruit.Name] = true end end local function refreshFruitSet() if not FruitFolder then return end for _, fruit in ipairs(FruitFolder:GetChildren()) do SpawnState.availableFruitSet[fruit.Name] = true end end local function fruitTools() refreshFruitSet() local result = {} local character = player.Character local backpack = player:FindFirstChild("Backpack") local function add(container) if not container then return end for _, item in ipairs(container:GetChildren()) do if item:IsA("Tool") and SpawnState.availableFruitSet[item.Name] then table.insert(result, item) end end end add(character) add(backpack) return result end local function clickGuiButton(button) if not button then return false end local ok = pcall(function() GuiService.SelectedObject = button VirtualInputManager:SendKeyEvent(true, 13, false, game) task.wait() VirtualInputManager:SendKeyEvent(false, 13, false, game) task.wait() GuiService.SelectedObject = nil end) return ok end local function getFruitDialogButton(name) local playerGui = player:FindFirstChild("PlayerGui") local gui = playerGui and playerGui:FindFirstChild("EatFruitBecky") local dialogue = gui and gui:FindFirstChild("Dialogue") return dialogue and dialogue:FindFirstChild(name) end local function openFruitDialog(tool) if not equipTool(tool) then return false end pcall(function() VirtualUser:ClickButton1(Vector2.new(300, 300)) end) local started = tick() while tick() - started < 2 do if getFruitDialogButton("Collect") or getFruitDialogButton("Drop") then return true end task.wait(0.1) end return false end local function getStoredFruitData() local stats = player:FindFirstChild("PlayerStats") local store = stats and stats:FindFirstChild("FruitStore") local limit = stats and stats:FindFirstChild("FruitStorage") if not store or not limit then return nil, nil end local data local ok = pcall(function() data = HttpService:JSONDecode(store.Value) end) if not ok or type(data) ~= "table" then return nil, nil end return data, tonumber(limit.Value) end local function storeFruits() if SpawnState.fruitBusy or not Config.AutoStoreFruit then return end SpawnState.fruitBusy = true local stored, limit = getStoredFruitData() if stored and limit then for _, tool in ipairs(fruitTools()) do if not running() or not Config.AutoStoreFruit then break end local current = tonumber(stored[tool.Name]) or 0 if current < limit and tool.Parent then if openFruitDialog(tool) then local button = getFruitDialogButton("Collect") if button then clickGuiButton(button) task.wait(0.5) stored[tool.Name] = current + 1 end end end end end SpawnState.fruitBusy = false end local function excludedFruit(name) local data = Config.ExcludedFruitConvert if type(data) ~= "table" then return false end if data[name] ~= nil then return data[name] == true end return table.find(data, name) ~= nil end local function convertFruits() if SpawnState.fruitBusy or not Config.AutoConvertFruit or not DealFruit then return end SpawnState.fruitBusy = true local names = {} local seen = {} for _, tool in ipairs(fruitTools()) do if tool.Parent and not excludedFruit(tool.Name) and not seen[tool.Name] then seen[tool.Name] = true table.insert(names, tool.Name) end end if #names > 0 then remoteInvoke(DealFruit, Config.SelectedKey, names) task.wait(0.8) end SpawnState.fruitBusy = false end local function dropFruits() if SpawnState.fruitBusy or not Config.AutoDropFruit then return end SpawnState.fruitBusy = true for _, tool in ipairs(fruitTools()) do if not running() or not Config.AutoDropFruit then break end if tool.Parent and openFruitDialog(tool) then local button = getFruitDialogButton("Drop") if button then clickGuiButton(button) task.wait(0.5) end end end SpawnState.fruitBusy = false end local function materialInventory() local stats = player:FindFirstChild("PlayerStats") local material = stats and stats:FindFirstChild("Material") if not material then return nil end local data local ok = pcall(function() data = HttpService:JSONDecode(material.Value) end) return ok and type(data) == "table" and data or nil end function TradeFlow.traderNames() if type(Config.AllowedTraders) == "string" then return Config.AllowedTraders ~= "" and {Config.AllowedTraders} or {} end return type(Config.AllowedTraders) == "table" and Config.AllowedTraders or {} end function TradeFlow.nameEquals(a, b) return tostring(a or ""):lower() == tostring(b or ""):lower() end TradeFlow.receiverName = function() return tostring(Config.TradeReceiverName or "") end function TradeFlow.allowedTrader(name) local lower = tostring(name or ""):lower() if lower == "" then return false end for _, trader in ipairs(TradeFlow.traderNames()) do if TradeFlow.nameEquals(trader, name) then return true end end if Config.TradeReceiverMode == true and SpawnState.tradeIncomingSenders[lower] then return true end local receiver = TradeFlow.receiverName() if Config.TradeReceiverMode ~= true and receiver ~= "" and TradeFlow.nameEquals(receiver, name) then return true end return false end function TradeFlow.frame() local playerGui = player:FindFirstChild("PlayerGui") local main = playerGui and playerGui:FindFirstChild("MainGui") local starter = main and main:FindFirstChild("StarterFrame") local trading = starter and starter:FindFirstChild("TradingFrame") return trading and trading:FindFirstChild("MainFrame") end function TradeFlow.readyVisible() local frame = TradeFlow.frame() local ready = frame and frame:FindFirstChild("Ready1") return ready and ready.Visible or false end function TradeFlow.offerFolder(name) local trades = ReplicatedStorage:FindFirstChild("Trades") return trades and trades:FindFirstChild(tostring(name or "")) end function TradeFlow.offeredAmount(ownerName, itemName) local folder = TradeFlow.offerFolder(ownerName) local value = folder and folder:FindFirstChild(itemName) if value and value:IsA("ValueBase") then return tonumber(value.Value) or 0 end return 0 end function TradeFlow.invoke(action, payload) if not TradeFunction then return false, nil end local ok, result = pcall(function() return TradeFunction:InvokeServer(action, payload) end) return ok, result end function TradeFlow.materialList() local source = type(Config.TradeMaterials) == "table" and Config.TradeMaterials or {} local result = {} local seen = {} for _, item in ipairs(source) do item = tostring(item or "") local key = item:lower() if item ~= "" and not seen[key] then seen[key] = true table.insert(result, item) end end return result end function TradeFlow.inventorySnapshot() local inventory = materialInventory() or {} local snapshot = {} for _, item in ipairs(TradeFlow.materialList()) do snapshot[item] = tonumber(inventory[item]) or 0 end return snapshot end function TradeFlow.triggerHits(inventory) inventory = inventory or materialInventory() or {} local hits = {} local thresholds = type(Config.TradeTriggerThresholds) == "table" and Config.TradeTriggerThresholds or {} for _, item in ipairs(TradeFlow.materialList()) do local threshold = math.max(0, tonumber(thresholds[item]) or 0) local amount = tonumber(inventory[item]) or 0 if threshold > 0 and amount >= threshold then table.insert(hits, {name = item, amount = amount, threshold = threshold}) end end table.sort(hits, function(a, b) return a.name < b.name end) return hits end TradeFlow.hasTrigger = function() return #TradeFlow.triggerHits(materialInventory()) > 0 end function TradeFlow.apiCall(action, payload) if not apiEnabled() then return nil end payload = type(payload) == "table" and payload or {} payload.action = action return requestJSON("POST", Config.ApiURL, payload, Config.TradeApiRequestTimeout) end function TradeFlow.receiverPing() return TradeFlow.apiCall("trade_receiver_ping", { receiver_name = player.Name, receiver_id = tostring(player.UserId), job_id = game.JobId, place_id = game.PlaceId, channel = tostring(Config.TradeChannel or "default") }) end function TradeFlow.receiverGet() local name = TradeFlow.receiverName() if name == "" then return nil end local response = TradeFlow.apiCall("trade_receiver_get", { receiver_name = name, channel = tostring(Config.TradeChannel or "default") }) if type(response) == "table" and type(response.receiver) == "table" then return response.receiver end return nil end function TradeFlow.deliveryStart(receiverName) return TradeFlow.apiCall("trade_delivery_start", { receiver_name = tostring(receiverName or ""), sender_name = player.Name, sender_id = tostring(player.UserId), channel = tostring(Config.TradeChannel or "default") }) end function TradeFlow.incoming() return TradeFlow.apiCall("trade_incoming", { receiver_name = player.Name, channel = tostring(Config.TradeChannel or "default") }) end TradeFlow.deliveryDone = function(receiverName, senderName) return TradeFlow.apiCall("trade_delivery_done", { receiver_name = tostring(receiverName or ""), sender_name = tostring(senderName or player.Name) }) end function TradeFlow.refreshIncoming() if Config.TradeReceiverMode ~= true then return false end local response = TradeFlow.incoming() if type(response) ~= "table" or type(response.incoming) ~= "table" then return false end local active = {} for _, delivery in pairs(response.incoming) do if type(delivery) == "table" then local sender = tostring(delivery.sender_name or "") if sender ~= "" then active[sender:lower()] = true end end end SpawnState.tradeIncomingSenders = active SpawnState.tradeIncomingUpdatedAt = tick() return true end function TradeFlow.webhook(title, receiverName, beforeInventory, afterInventory, detail, dedupeKey, cooldown) if not Config.TradeWebhookEnabled or type(Config.TradeWebhookURL) ~= "string" or Config.TradeWebhookURL == "" then return false end local key = tostring(dedupeKey or title or "trade") local now = tick() local waitTime = math.max(0, tonumber(cooldown) or 0) if waitTime > 0 and now - (tonumber(SpawnState.tradeWebhookLast[key]) or 0) < waitTime then return false end local fields = {} if type(beforeInventory) == "table" or type(afterInventory) == "table" then for _, item in ipairs(TradeFlow.materialList()) do local before = tonumber(beforeInventory and beforeInventory[item]) or 0 local after = tonumber(afterInventory and afterInventory[item]) or before local sent = math.max(0, before - after) table.insert(fields, { name = item, value = "Before: " .. tostring(before) .. " | After: " .. tostring(after) .. " | Sent: " .. tostring(sent), inline = false }) end end if type(detail) == "string" and detail ~= "" then table.insert(fields, {name = "Status", value = detail, inline = false}) end local payload = { username = tostring(Config.TradeWebhookUsername or "King Legacy Trade"), embeds = {{ title = tostring(title or "Trade"), description = player.Name .. " -> " .. tostring(receiverName or "") .. " | " .. game.JobId, color = 3447003, fields = fields, timestamp = os.date("!%Y-%m-%dT%H:%M:%S.000Z", os.time()) }} } local result = requestJSON("POST", Config.TradeWebhookURL, payload, Config.TradeWebhookRequestTimeout) if result ~= nil then SpawnState.tradeWebhookLast[key] = now return true end return false end function TradeFlow.receiverLevelOkay(name) local target = Players:FindFirstChild(tostring(name or "")) local stats = target and target:FindFirstChild("PlayerStats") local level = stats and stats:FindFirstChild("lvl") return target ~= nil and level ~= nil and (tonumber(level.Value) or 0) >= 300 end function TradeFlow.offerSignature(ownerName) local folder = TradeFlow.offerFolder(ownerName) if not folder then return "" end local parts = {} for _, child in ipairs(folder:GetChildren()) do if child:IsA("ValueBase") then table.insert(parts, child.Name .. "=" .. tostring(child.Value)) end end table.sort(parts) return table.concat(parts, "|") end function TradeFlow.offerAllMaterials() local inventory = materialInventory() if not inventory then return false, false end local hasAny = false local complete = true for _, item in ipairs(TradeFlow.materialList()) do local desired = math.max(0, tonumber(inventory[item]) or 0) local offered = math.max(0, TradeFlow.offeredAmount(player.Name, item)) if desired > 0 then hasAny = true end if offered < desired then complete = false local addAmount = desired - offered local ok, result = TradeFlow.invoke("PutItem", { ItemName = item, Add = true, Amt = tostring(addAmount) }) if not ok or (result ~= true and type(result) == "string") then task.wait(math.max(0.1, tonumber(Config.TradeOfferRetryInterval) or 0.35)) else task.wait(math.max(0.1, tonumber(Config.TradeOfferRetryInterval) or 0.35)) end end end if hasAny then local refreshed = materialInventory() or inventory complete = true for _, item in ipairs(TradeFlow.materialList()) do local desired = math.max(0, tonumber(refreshed[item]) or 0) local offered = math.max(0, TradeFlow.offeredAmount(player.Name, item)) if offered < desired then complete = false break end end end return hasAny, complete end function TradeFlow.finishSender(receiverName) local beforeInventory = type(SpawnState.tradeBeforeInventory) == "table" and SpawnState.tradeBeforeInventory or TradeFlow.inventorySnapshot() task.wait(0.6) local afterInventory = TradeFlow.inventorySnapshot() TradeFlow.webhook("Trade Success", receiverName, beforeInventory, afterInventory, "Trade completed successfully", "success|" .. tostring(receiverName), 0) TradeFlow.deliveryDone(receiverName, player.Name) clearPendingTradeTarget() incomingTradeTarget = nil SpawnState.tradeMissionActive = false SpawnState.tradeTargetReceiverName = nil SpawnState.tradeMissionStartedAt = 0 SpawnState.tradeSessionStartedAt = nil SpawnState.tradeBeforeInventory = nil SpawnState.tradeBusy = false SpawnState.hopBusy = false if SpawnState.pendingTeleportSource == "trade" then SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil end setStatus("TRADE xong | Quay lại hop boss", 100, 3) task.spawn(function() task.wait(math.max(0.2, tonumber(Config.TradeAfterSuccessHopDelay) or 1)) if running() and Config.AutoHop and not Config.TradeReceiverMode then SpawnState.lastHopAttempt = 0 hopServer(true) end end) end function TradeFlow.teleportReceiver(receiver) if type(receiver) ~= "table" then return false end local receiverName = tostring(receiver.name or receiver.receiver_name or "") local jobId = tostring(receiver.job_id or "") local placeId = tonumber(receiver.place_id) or game.PlaceId if receiverName == "" or jobId == "" then return false end if TradeFlow.nameEquals(receiverName, player.Name) then return false end local delivery = TradeFlow.deliveryStart(receiverName) if type(delivery) == "table" and delivery.queued == true then local position = math.max(1, tonumber(delivery.position) or 1) local count = math.max(position, tonumber(delivery.queue_count) or position) setStatus("TRADE | Đang xếp hàng #" .. tostring(position) .. "/" .. tostring(count) .. " | Chờ acc trước", 100, 2) SpawnState.tradeRetryAfter = tick() + 1 return true end if type(delivery) ~= "table" or delivery.ok ~= true or delivery.active ~= true then setStatus("TRADE | Acc nhận chưa sẵn sàng", 100, 2) SpawnState.tradeRetryAfter = tick() + math.max(1, tonumber(Config.TradeJoinRetryDelay) or 3) return false end if type(delivery.receiver) == "table" then receiver = delivery.receiver receiverName = tostring(receiver.name or receiverName) jobId = tostring(receiver.job_id or jobId) placeId = tonumber(receiver.place_id) or placeId end local target = { KLTrade = true, receiver_name = receiverName, receiver_id = tostring(receiver.user_id or ""), job_id = jobId, place_id = placeId, channel = tostring(Config.TradeChannel or "default") } SpawnState.tradeMissionActive = true SpawnState.tradeTargetReceiverName = receiverName SpawnState.tradeMissionStartedAt = SpawnState.tradeMissionStartedAt > 0 and SpawnState.tradeMissionStartedAt or tick() if jobId == game.JobId then incomingTradeTarget = target clearPendingTradeTarget() setStatus("TRADE | Đã ở cùng server acc nhận", 100, 2) return true end savePendingTradeTarget(target) SpawnState.hopBusy = true SpawnState.pendingTeleportJobId = jobId SpawnState.pendingTeleportSource = "trade" setStatus("TRADE | Join server acc nhận: " .. receiverName, 100, 10) local ok = pcall(function() TeleportService:TeleportToPlaceInstance(placeId, jobId, player, nil, target) end) if not ok then TradeFlow.deliveryDone(receiverName, player.Name) clearPendingTradeTarget() SpawnState.hopBusy = false SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil SpawnState.tradeRetryAfter = tick() + math.max(1, tonumber(Config.TradeJoinRetryDelay) or 3) TradeFlow.webhook("Trade Teleport Error", receiverName, nil, nil, "Could not teleport to receiver server", "teleport_error|" .. receiverName, 60) return false end task.delay(math.max(4, tonumber(Config.TeleportWatchdog) or 3) + 2, function() if running() and SpawnState.pendingTeleportSource == "trade" and SpawnState.pendingTeleportJobId == jobId then TradeFlow.deliveryDone(receiverName, player.Name) clearPendingTradeTarget() SpawnState.hopBusy = false SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil SpawnState.tradeRetryAfter = tick() + math.max(1, tonumber(Config.TradeJoinRetryDelay) or 3) setStatus("TRADE | Teleport timeout, thử lại", 100, 2) end end) return true end function TradeFlow.startMission() if not Config.AutoTrade or Config.TradeReceiverMode == true or TradeFlow.receiverName() == "" then return false end if incomingTradeTarget then SpawnState.tradeMissionActive = true return true end if not SpawnState.tradeMissionActive then local inventory = materialInventory() local hits = TradeFlow.triggerHits(inventory) if #hits == 0 then return false end SpawnState.tradeMissionActive = true SpawnState.tradeMissionStartedAt = tick() local detailParts = {} for _, hit in ipairs(hits) do table.insert(detailParts, hit.name .. "=" .. tostring(hit.amount) .. "/" .. tostring(hit.threshold)) end TradeFlow.webhook("Trade Triggered", TradeFlow.receiverName(), inventory, inventory, table.concat(detailParts, " | "), "trigger", 5) end if tick() < (tonumber(SpawnState.tradeRetryAfter) or 0) then return true end local receiver = TradeFlow.receiverGet() if not receiver then setStatus("TRADE | Đang chờ acc nhận online...", 100, 2) SpawnState.tradeRetryAfter = tick() + math.max(1, tonumber(Config.TradeReceiverLookupInterval) or 2) return true end return TradeFlow.teleportReceiver(receiver) end function TradeFlow.processSenderSession() if Config.TradeReceiverMode == true or not Config.AutoTrade or not incomingTradeTarget then return false end SpawnState.tradeMissionActive = true local receiverName = tostring(incomingTradeTarget.receiver_name or TradeFlow.receiverName()) if receiverName == "" then setStatus("TRADE | Thiếu TradeReceiverName", 100, 2) return true end local receiver = Players:FindFirstChild(receiverName) if not receiver then local latest = TradeFlow.receiverGet() if type(latest) == "table" and tostring(latest.job_id or "") ~= "" and tostring(latest.job_id or "") ~= game.JobId then incomingTradeTarget = nil clearPendingTradeTarget() TradeFlow.teleportReceiver(latest) return true end setStatus("TRADE | Chờ acc nhận vào server...", 100, 2) return true end if not TradeFlow.receiverLevelOkay(receiverName) then setStatus("TRADE | Acc nhận cần Level >= 300", 100, 3) TradeFlow.webhook("Trade Blocked", receiverName, nil, nil, "Receiver level is below 300", "receiver_level|" .. receiverName, 60) return true end if SpawnState.tradeSessionStartedAt then if SpawnState.tradeSuccessSerial > (tonumber(SpawnState.tradeSessionSuccessSerial) or 0) then TradeFlow.finishSender(receiverName) return true end if SpawnState.tradeCancelSerial > (tonumber(SpawnState.tradeSessionCancelSerial) or 0) then TradeFlow.deliveryDone(receiverName, player.Name) clearPendingTradeTarget() incomingTradeTarget = nil SpawnState.tradeSessionStartedAt = nil SpawnState.tradeBeforeInventory = nil SpawnState.tradeBusy = false SpawnState.tradeRetryAfter = tick() + 1 setStatus("TRADE | Bị hủy | Xếp hàng lại", 100, 2) return true end end local tradeWith = player:FindFirstChild("TradeWith") local partner = tradeWith and tostring(tradeWith.Value or "") or "" if SpawnState.tradeSessionStartedAt and partner == "" and type(SpawnState.tradeBeforeInventory) == "table" then local after = TradeFlow.inventorySnapshot() local inventoryReduced = false for _, item in ipairs(TradeFlow.materialList()) do if (tonumber(after[item]) or 0) < (tonumber(SpawnState.tradeBeforeInventory[item]) or 0) then inventoryReduced = true break end end if inventoryReduced then TradeFlow.finishSender(receiverName) return true end end if partner ~= "" and not TradeFlow.nameEquals(partner, receiverName) then TradeFlow.invoke("Decline") return true end if partner == "" then if TradeRequester and tick() - SpawnState.lastTradeInviteAt >= math.max(1, tonumber(Config.TradeInviteInterval) or 4) then SpawnState.lastTradeInviteAt = tick() pcall(function() TradeRequester:InvokeServer("Invite", {TargetName = receiverName}) end) end setStatus("TRADE | Đang mời " .. receiverName, 100, 2) return true end SpawnState.tradeBusy = true if not SpawnState.tradeSessionStartedAt then SpawnState.tradeSessionStartedAt = tick() SpawnState.tradeBeforeInventory = TradeFlow.inventorySnapshot() SpawnState.tradeSessionSuccessSerial = SpawnState.tradeSuccessSerial SpawnState.tradeSessionCancelSerial = SpawnState.tradeCancelSerial end local hasAny, complete = TradeFlow.offerAllMaterials() if not hasAny then TradeFlow.deliveryDone(receiverName, player.Name) clearPendingTradeTarget() incomingTradeTarget = nil SpawnState.tradeMissionActive = false SpawnState.tradeTargetReceiverName = nil SpawnState.tradeMissionStartedAt = 0 SpawnState.tradeBusy = false SpawnState.tradeSessionStartedAt = nil SpawnState.tradeBeforeInventory = nil SpawnState.hopBusy = false if SpawnState.pendingTeleportSource == "trade" then SpawnState.pendingTeleportJobId = nil SpawnState.pendingTeleportSource = nil end setStatus("TRADE | Không còn material | Quay lại hop boss", 100, 2) task.spawn(function() task.wait(math.max(0.2, tonumber(Config.TradeAfterSuccessHopDelay) or 1)) if running() and Config.AutoHop and Config.TradeReceiverMode ~= true then SpawnState.lastHopAttempt = 0 hopServer(true) end end) return true end if complete and not TradeFlow.readyVisible() then TradeFlow.invoke("Ready") end setStatus(complete and "TRADE | Đã offer ALL | Chờ xác nhận" or "TRADE | Đang offer ALL material...", 100, 2) local timeout = math.max(15, tonumber(Config.TradeSessionTimeout) or 45) if tick() - (tonumber(SpawnState.tradeSessionStartedAt) or tick()) >= timeout then TradeFlow.invoke("Decline") TradeFlow.deliveryDone(receiverName, player.Name) clearPendingTradeTarget() incomingTradeTarget = nil SpawnState.tradeSessionStartedAt = nil SpawnState.tradeBeforeInventory = nil SpawnState.tradeBusy = false SpawnState.tradeRetryAfter = tick() + 1 TradeFlow.webhook("Trade Retry", receiverName, nil, nil, "Trade session timed out and rejoined queue", "session_timeout|" .. receiverName, 60) end return true end function TradeFlow.receiverReady() if Config.TradeReceiverMode ~= true then return end local tradeWith = player:FindFirstChild("TradeWith") local senderName = tradeWith and tostring(tradeWith.Value or "") or "" if senderName == "" or not TradeFlow.allowedTrader(senderName) then SpawnState.tradeOfferStableSignature = "" SpawnState.tradeOfferStableSince = 0 SpawnState.tradeActivePartner = nil SpawnState.tradeReceiverSessionPartner = "" SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false return end if not TradeFlow.nameEquals(SpawnState.tradeReceiverSessionPartner, senderName) then SpawnState.tradeReceiverSessionPartner = senderName SpawnState.tradeOfferStableSignature = "" SpawnState.tradeOfferStableSince = 0 SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false end SpawnState.tradeActivePartner = senderName local signature = TradeFlow.offerSignature(senderName) if signature == "" then SpawnState.tradeOfferStableSignature = "" SpawnState.tradeOfferStableSince = 0 SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false setStatus("ACC NHẬN | Chờ " .. senderName .. " offer đồ", 100, 2) return end if signature ~= SpawnState.tradeOfferStableSignature then SpawnState.tradeOfferStableSignature = signature SpawnState.tradeOfferStableSince = tick() SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false return end if SpawnState.tradeOfferStableSince <= 0 then SpawnState.tradeOfferStableSince = tick() return end if tick() - SpawnState.tradeOfferStableSince < math.max(0.3, tonumber(Config.TradeReadyStableSeconds) or 0.6) then return end local now = tick() if not SpawnState.tradeReceiverReadyConfirmed or now - (tonumber(SpawnState.tradeReceiverReadyAttemptAt) or 0) >= 1 then SpawnState.tradeReceiverReadyAttemptAt = now local ok, result = TradeFlow.invoke("Ready") if ok and result ~= "Fail" and result ~= false then SpawnState.tradeReceiverReadyConfirmed = true else SpawnState.tradeReceiverReadyConfirmed = false end end setStatus("ACC NHẬN | Trade với " .. senderName, 100, 2) end if TradeEvent and TradeEvent:IsA("RemoteEvent") then TradeEvent.OnClientEvent:Connect(function(action) local tradeWith = player:FindFirstChild("TradeWith") local partner = tradeWith and tostring(tradeWith.Value or "") or "" if partner == "" then partner = tostring(SpawnState.tradeActivePartner or "") end if action == "TradeSuccess" then SpawnState.tradeSuccessSerial = (tonumber(SpawnState.tradeSuccessSerial) or 0) + 1 SpawnState.tradeBusy = false SpawnState.tradeOfferStableSignature = "" SpawnState.tradeOfferStableSince = 0 SpawnState.tradeReceiverSessionPartner = "" SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false SpawnState.tradeLastAcceptName = "" SpawnState.tradeLastAcceptAttemptAt = 0 if Config.TradeReceiverMode == true and partner ~= "" then SpawnState.tradeIncomingSenders[partner:lower()] = nil TradeFlow.deliveryDone(player.Name, partner) SpawnState.tradeIncomingUpdatedAt = 0 end SpawnState.tradeActivePartner = nil elseif action == "CancelTrade" then SpawnState.tradeCancelSerial = (tonumber(SpawnState.tradeCancelSerial) or 0) + 1 SpawnState.tradeBusy = false SpawnState.tradeOfferStableSignature = "" SpawnState.tradeOfferStableSince = 0 SpawnState.tradeReceiverSessionPartner = "" SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false SpawnState.tradeLastAcceptName = "" SpawnState.tradeLastAcceptAttemptAt = 0 if Config.TradeReceiverMode == true and partner ~= "" then SpawnState.tradeIncomingSenders[partner:lower()] = nil TradeFlow.deliveryDone(player.Name, partner) SpawnState.tradeIncomingUpdatedAt = 0 end SpawnState.tradeActivePartner = nil end end) end SpawnState.webhookStateFile = historyFolder .. "/webhook_state_" .. tostring(player.UserId) .. ".json" SpawnState.webhookState = {} local function loadWebhookState() if not canUseFiles() or not isfile(SpawnState.webhookStateFile) then return end local ok, raw = pcall(readfile, SpawnState.webhookStateFile) if ok then local ok2, data = pcall(function() return HttpService:JSONDecode(raw) end) if ok2 and type(data) == "table" then SpawnState.webhookState = data end end end local function saveWebhookState() if canUseFiles() then ensureHistoryFolder() pcall(function() writefile(SpawnState.webhookStateFile, HttpService:JSONEncode(SpawnState.webhookState)) end) end end local function sendDiscordThresholdWebhook(hits, inventory) if not Config.WebhookEnabled or type(Config.WebhookURL) ~= "string" or Config.WebhookURL == "" then return false end local fields = {} for _, hit in ipairs(hits) do table.insert(fields, { name = hit.name, value = "x" .. tostring(hit.amount) .. " / threshold " .. tostring(hit.threshold), inline = true }) end local all = {} for item, threshold in pairs(Config.WebhookThresholds or {}) do table.insert(all, item .. ": " .. tostring(tonumber(inventory[item]) or 0) .. " (>= " .. tostring(threshold) .. ")") end table.sort(all) table.insert(fields, { name = "Monitored materials", value = table.concat(all, "\n"), inline = false }) local payload = { username = "King Legacy SeaFarm", embeds = {{ title = "Material threshold reached", description = player.Name .. " | " .. game.JobId, color = 5763719, fields = fields, timestamp = os.date("!%Y-%m-%dT%H:%M:%S.000Z", os.time()) }} } local result = requestJSON("POST", Config.WebhookURL, payload) return result ~= nil end local function checkMaterialWebhook() local inventory = materialInventory() if not inventory or type(Config.WebhookThresholds) ~= "table" then return end local hits = {} for item, thresholdValue in pairs(Config.WebhookThresholds) do local threshold = tonumber(thresholdValue) or 0 local amount = tonumber(inventory[item]) or 0 local key = tostring(item) .. "|" .. tostring(threshold) if threshold > 0 and amount >= threshold then if not SpawnState.webhookState[key] then table.insert(hits, {name = item, amount = amount, threshold = threshold, key = key}) end else SpawnState.webhookState[key] = nil end end if #hits > 0 and sendDiscordThresholdWebhook(hits, inventory) then for _, hit in ipairs(hits) do SpawnState.webhookState[hit.key] = os.time() end end saveWebhookState() end loadWebhookState() function TradeFlow.acceptAllowed() if (not Config.AutoTrade and Config.TradeReceiverMode ~= true) or not Config.AutoAcceptTrade or not TradeRequester then return false end local tradeWith = player:FindFirstChild("TradeWith") if tradeWith and tostring(tradeWith.Value or "") ~= "" then return true end local requests = nil local ok = pcall(function() requests = TradeRequester:InvokeServer("Get") end) if not ok or type(requests) ~= "table" then return false end local function findCandidate() for name in pairs(requests) do name = tostring(name or "") if name ~= "" and TradeFlow.allowedTrader(name) then return name end end return nil end local name = findCandidate() if not name then if Config.TradeReceiverMode == true then SpawnState.tradeIncomingUpdatedAt = 0 end return false end local now = tick() local key = name:lower() if SpawnState.tradeLastAcceptName == key and now - (tonumber(SpawnState.tradeLastAcceptAttemptAt) or 0) < 0.25 then return false end SpawnState.tradeLastAcceptName = key SpawnState.tradeLastAcceptAttemptAt = now local accepted = pcall(function() TradeRequester:InvokeServer("Accept", {TargetName = name}) end) if accepted then SpawnState.tradeActivePartner = name SpawnState.tradeOfferStableSignature = "" SpawnState.tradeOfferStableSince = 0 SpawnState.tradeReceiverSessionPartner = "" SpawnState.tradeReceiverReadyAttemptAt = 0 SpawnState.tradeReceiverReadyConfirmed = false setStatus("ACC NHẬN | Đang nhận trade từ " .. name, 100, 1) end return accepted end createStatusHUD() setStatus("Đang vào game...", 100, 2) if not ensureEnteredGame() then getgenv().SeaFarmStandaloneRunning = false return end setStatus("Đang chờ game load hoàn tất...", 100, 2) if not waitForGameplayReady() then getgenv().SeaFarmStandaloneRunning = false return end SpawnState.scriptStartedAt = tick() SpawnState.worldReadyAt = SpawnState.scriptStartedAt SpawnState.incomingChestMissingSince = nil updateSeaSpawnState() updateGhostSpawnState() createSpawnHUD() updateSpawnHUD() createStatusHUD() SpawnState.statusPriority = 0 SpawnState.statusPriorityUntil = 0 setStatus("Đang quét Boss...", 20, 1) task.spawn(function() task.wait(1) apiReportCurrent(true) end) player.CharacterAdded:Connect(function() task.wait(1) ensureFreeze(false) createSpawnHUD() createStatusHUD() SpawnState.statusPriority = 0 SpawnState.statusPriorityUntil = 0 setStatus("Đang quét Boss...", 20, 1) ensureHaki() end) task.spawn(function() while running() do if isGameplayReady() then pcall(ensureHaki) end task.wait(math.max(0.5, tonumber(Config.HakiRetryInterval) or 1)) end end) task.spawn(function() local lastNormalAttack = 0 local toolCursor = 0 while running() do if isGameplayReady() and (Config.AutoAimSkill or Config.AutoNormalAttack) and not SpawnState.fruitBusy and not SpawnState.lootBusy and not SpawnState.lootPriority and not SpawnState.tradeBusy and not SpawnState.hopBusy then local target = getMobTarget() if target and aliveModel(target) then local targetRoot = getModelRoot(target) if targetRoot then local targetKind = getBossKind(target) if targetKind == "GhostShip" then setStatus("Đang đánh Ghost Ship | Skill tầm xa", 50, 0.7) elseif targetKind == "SeaHydra" then setStatus("Đang đánh " .. tostring(target.Name) .. " | M1 + Skill", 50, 0.7) else setStatus("Đang đánh " .. tostring(target.Name), 50, 0.7) end SpawnState.combatBusy = true positionForCombat(target) local entries = getUsableTools() if #entries > 0 then local maxTools = math.max(1, tonumber(Config.MaxCombatToolsPerCycle) or 1) if getBossKind(target) == "GhostShip" then maxTools = math.max(maxTools, tonumber(Config.GhostMaxCombatToolsPerCycle) or 2) end for _ = 1, math.min(maxTools, #entries) do if not running() or not aliveModel(target) then break end toolCursor = (toolCursor % #entries) + 1 local entry = entries[toolCursor] local tool = entry.Tool local info = entry.Info if tool and tool.Parent and selectedCombatTool(tool, info) and equipTool(tool, info) then positionForCombat(target) targetRoot = getModelRoot(target) if targetRoot then aimAtTarget(target) if Config.AutoNormalAttack and shouldUseM1(target) and tick() - lastNormalAttack >= math.max(0.08, tonumber(Config.NormalAttackInterval) or 0.18) then local _, _, root = getCharacter() local rootDistance = root and (root.Position - targetRoot.Position).Magnitude or math.huge local rootRadius = math.max(targetRoot.Size.X, targetRoot.Size.Z) * 0.5 local surfaceDistance = math.max(0, rootDistance - rootRadius) if surfaceDistance <= math.max(8, tonumber(Config.AttackMaxM1SurfaceDistance) or 35) then pressM1(tool, info, target) lastNormalAttack = tick() end end if Config.AutoAimSkill and aliveModel(target) then local keys = getUnlockedSkillKeys(info) for _, key in ipairs(keys) do if not running() or not aliveModel(target) then break end if tool.Parent ~= player.Character then if not equipTool(tool, info) then break end end positionForCombat(target) aimAtTarget(target) if not gameCooldownActive(info.Prefix, key) then pressSkillKey(tool, info, key, target) end end end if Config.AutoNormalAttack and shouldUseM1(target) and aliveModel(target) and tick() - lastNormalAttack >= math.max(0.08, tonumber(Config.NormalAttackInterval) or 0.18) then positionForCombat(target) pressM1(tool, info, target) lastNormalAttack = tick() end end end task.wait(math.max(0.05, tonumber(Config.PerToolDelay) or 0.12)) end end SpawnState.combatBusy = false end else SpawnState.combatBusy = false end else SpawnState.combatBusy = false end task.wait(math.max(0.08, tonumber(Config.SkillInterval) or 0.35)) end end) task.spawn(function() while running() do if isGameplayReady() and Config.AutoDodge and not SpawnState.lootBusy and not SpawnState.lootPriority then dodgeIfNeeded() end task.wait(Config.DodgeInterval) end end) SpawnState.activeBossName = nil SpawnState.activeBossKind = nil SpawnState.bossLostAt = nil SpawnState.activeApiEventId = nil SpawnState.lastHopAttempt = 0 SpawnState.serverLooted = false SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil SpawnState.lastBossSeenAt = 0 task.spawn(function() markServerScan(game.JobId) task.wait(1) while running() do if not isGameplayReady() then ensureFreeze(false) setSpawnHUDEnabled(false) setStatus("Đang chờ game sẵn sàng...", 60, 0.7) SpawnState.bossLostAt = nil task.wait(0.25) else updateSpawnHUD() if lockedBoss then if aliveModel(lockedBoss) then SpawnState.bossLostAt = nil if getBossKind(lockedBoss) == "GhostShip" then setStatus("Đang đánh Ghost Ship | Skill tầm xa", 50, 0.7) else setStatus("Đang đánh " .. tostring(lockedBoss.Name) .. " | M1 + Skill", 50, 0.7) end if Config.AutoTeleportBoss and not SpawnState.lootBusy and not SpawnState.lootPriority and not SpawnState.fruitBusy and not SpawnState.tradeBusy then local _, _, root = getCharacter() local bossRoot = getModelRoot(lockedBoss) if root and bossRoot and tick() - SpawnState.lastDodgeAt > 0.55 then local desired = getAttackCFrame(lockedBoss) local profile = getAttackProfile(lockedBoss) if desired and (root.Position - desired.Position).Magnitude > profile.RepositionThreshold then ensureFreeze(true) root.CFrame = desired end end end else ensureFreeze(false) setStatus("Boss đã chết, đang xác nhận...", 70, 0.7) if not SpawnState.bossLostAt then SpawnState.bossLostAt = tick() end local confirmSeconds = math.max(0.5, tonumber(Config.BossDeathConfirmSeconds) or 1.8) if tick() - SpawnState.bossLostAt >= confirmSeconds then local finishedBossName = SpawnState.activeBossName local finishedBossKind = SpawnState.activeBossKind local finishedApiEventId = SpawnState.activeApiEventId lockedBoss = nil SpawnState.activeBossName = nil SpawnState.activeBossKind = nil SpawnState.activeApiEventId = nil SpawnState.bossLostAt = nil if finishedBossName and finishedBossKind then notify("Boss defeated: " .. finishedBossName .. " | collecting every chest.", 4) local chestEventId = finishedApiEventId chestEventId = select(1, apiChestReady(finishedBossName, finishedApiEventId)) or chestEventId local completed, count, timedOut, interruptedByBoss = collectLoot(finishedBossKind, finishedBossName) SpawnState.serverLooted = SpawnState.serverLooted or count > 0 if interruptedByBoss then local nextBoss = Config.AutoFindBoss and getBoss() or nil if nextBoss and aliveModel(nextBoss) then lockedBoss = nextBoss SpawnState.activeBossName = nextBoss.Name SpawnState.activeBossKind = getBossKind(nextBoss) SpawnState.activeApiEventId = apiVerifyBoss(nextBoss) SpawnState.bossLostAt = nil SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil SpawnState.lastBossSeenAt = tick() setStatus("Có boss mới | Ưu tiên đánh " .. tostring(nextBoss.Name), 100, 1) end else if incomingApiTarget and apiBossName(incomingApiTarget.boss) == apiBossName(finishedBossName) then incomingApiTarget = nil end if timedOut then notify("Chest timeout 30s: " .. tostring(count) .. " collected. Hopping now.", 3) task.spawn(function() apiLootDone(finishedBossName, chestEventId, count, count > 0) end) markServerSuccess(game.JobId) SpawnState.serverCache = nil SpawnState.serverCacheAt = 0 if Config.AutoHop and not SpawnState.hopBusy and tick() >= SpawnState.teleportRetryAfter then SpawnState.lastHopAttempt = tick() hopServer(true) end else apiLootDone(finishedBossName, chestEventId, count, completed or count > 0) if completed then notify("Collected all detected chests: " .. tostring(count) .. ". Rescanning server.", 3) else notify("No usable chest detected: " .. tostring(count) .. " collected. Rescanning server.", 3) end task.wait(math.max(0.05, tonumber(Config.PostLootBossScanDelay) or 0.15)) local nextBoss = Config.AutoFindBoss and getBoss() or nil local reportResponse = apiReportCurrent(true) markServerSuccess(game.JobId) SpawnState.serverCache = nil SpawnState.serverCacheAt = 0 if nextBoss and aliveModel(nextBoss) then lockedBoss = nextBoss SpawnState.activeBossName = nextBoss.Name SpawnState.activeBossKind = getBossKind(nextBoss) SpawnState.activeApiEventId = apiEventIdFromReport(reportResponse, nextBoss, "spawn") or apiVerifyBoss(nextBoss) SpawnState.bossLostAt = nil SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil SpawnState.lastBossSeenAt = tick() setStatus("Còn boss trong server | Đánh tiếp", 90, 2) elseif Config.AutoHop and not SpawnState.hopBusy and tick() >= SpawnState.teleportRetryAfter then SpawnState.lastHopAttempt = tick() hopServer(true) end end end end end end else ensureFreeze(false) local boss = Config.AutoFindBoss and getBoss() or nil if boss then setStatus("Đã tìm thấy " .. tostring(boss.Name), 40, 1) lockedBoss = boss SpawnState.activeBossName = boss.Name SpawnState.activeBossKind = getBossKind(boss) SpawnState.activeApiEventId = apiVerifyBoss(boss) SpawnState.bossLostAt = nil SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil SpawnState.lastBossSeenAt = tick() else local candidate, candidateState = getBossCandidate() local holdingForBoss = false if candidate then SpawnState.lastBossSeenAt = tick() if SpawnState.pendingBoss ~= candidate then SpawnState.pendingBoss = candidate SpawnState.pendingBossSince = tick() end if candidateState == "alive" and aliveModel(candidate) then lockedBoss = candidate SpawnState.activeBossName = candidate.Name SpawnState.activeBossKind = getBossKind(candidate) SpawnState.activeApiEventId = apiVerifyBoss(candidate) SpawnState.bossLostAt = nil SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil holdingForBoss = true elseif candidateState == "loading" then setStatus("Boss đang load...", 35, 0.7) local acquireTimeout = math.max(3, tonumber(Config.BossAcquireTimeout) or 5) if not SpawnState.pendingBossSince or tick() - SpawnState.pendingBossSince <= acquireTimeout then holdingForBoss = true else SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil end else SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil end else SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil end if not lockedBoss and not holdingForBoss then local seconds, spawnKind = getTargetSpawnSeconds() local maxWait = math.max(0, tonumber(Config.SpawnWindowMax) or 180) if seconds ~= nil and seconds >= 0 and seconds <= maxWait then local spawnName = spawnKind == "GhostShip" and "Ghost Ship" or (SpawnState.Hydra and "HydraSeaKing" or "SeaKing") setStatus("Chờ " .. spawnName .. ": " .. formatTimer(seconds), 20, 0.7) else setStatus("Đang tìm Boss...", 20, 0.7) end local waitingForSpawn = seconds ~= nil and seconds >= 0 and seconds <= maxWait local startupGrace = tick() - SpawnState.scriptStartedAt < math.max(0, tonumber(Config.StartupGrace) or 8) local scanGrace = tick() - SpawnState.scriptStartedAt < math.max(1.5, tonumber(Config.ServerBossScanGrace) or 3) local recentBoss = SpawnState.lastBossSeenAt > 0 and tick() - SpawnState.lastBossSeenAt < math.max(1, tonumber(Config.BossRecentSeenGrace) or 6) local tradeWith = player:FindFirstChild("TradeWith") local trading = tradeWith and tradeWith.Value ~= "" or false local incomingSeaHold = seaHydraJoinHoldActive() if Config.AutoHop and not waitingForSpawn and not startupGrace and not scanGrace and not recentBoss and not incomingSeaHold and not SpawnState.combatBusy and not SpawnState.lootBusy and not SpawnState.lootPriority and not SpawnState.fruitBusy and not SpawnState.tradeBusy and not trading and tick() >= SpawnState.teleportRetryAfter and tick() - SpawnState.lastHopAttempt >= math.max(0.1, tonumber(Config.HopRetryDelay) or 0.2) then local recheckBoss = getBoss() local recheckCandidate, recheckState = getBossCandidate() if not recheckBoss and not (recheckCandidate and recheckState ~= "dead") then if SpawnState.serverLooted then markServerSuccess(game.JobId) end SpawnState.lastHopAttempt = tick() hopServer() end end end end end task.wait(0.15) end end end) SpawnState.handledChestEvents = {} SpawnState.incomingChestMissingSince = nil task.spawn(function() while running() do local liveBoss = lockedBoss and aliveModel(lockedBoss) and lockedBoss or getBoss() local candidate, candidateState = getBossCandidate() local bossBlocksLoot = liveBoss ~= nil or (candidate ~= nil and candidateState ~= "dead" and candidateState ~= "gone") if isWorldReady() and Config.AutoCollectLoot and not bossBlocksLoot and not SpawnState.combatBusy and not SpawnState.lootBusy and not SpawnState.fruitBusy and not SpawnState.tradeBusy and not SpawnState.hopBusy then local lootKind, lootBossName, apiBoss = getAvailableLootGroup() if lootKind and lootBossName and apiBoss then SpawnState.incomingChestMissingSince = nil SpawnState.lootPriority = true local waitStarted = tick() while running() and SpawnState.combatBusy and tick() - waitStarted < 1.5 do task.wait(0.05) end end if lootKind and lootBossName and apiBoss then local reportResponse = apiReportCurrent(true) local chestEventId = apiEventIdFromReport(reportResponse, apiBoss, "chest") if incomingApiTarget and tostring(incomingApiTarget.status or "") == "chest" and apiBossName(incomingApiTarget.boss) == apiBoss then chestEventId = tostring(incomingApiTarget.event_id or chestEventId or "") end local handleKey = chestEventId and chestEventId ~= "" and chestEventId or (game.JobId .. "|" .. apiBoss) setStatus("CHEST API | Đang nhặt " .. tostring(apiBoss), 100, 3) local completed, count, timedOut, interruptedByBoss = collectLoot(lootKind, lootBossName) SpawnState.lootPriority = false SpawnState.serverLooted = SpawnState.serverLooted or count > 0 if interruptedByBoss then local nextBoss = Config.AutoFindBoss and getBoss() or nil if nextBoss and aliveModel(nextBoss) then lockedBoss = nextBoss SpawnState.activeBossName = nextBoss.Name SpawnState.activeBossKind = getBossKind(nextBoss) SpawnState.activeApiEventId = apiVerifyBoss(nextBoss) SpawnState.bossLostAt = nil SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil SpawnState.lastBossSeenAt = tick() setStatus("Có boss mới | Dừng Chest và đánh " .. tostring(nextBoss.Name), 100, 1) end else if completed or count > 0 then SpawnState.handledChestEvents[handleKey] = true end incomingApiTarget = nil if timedOut then setStatus("CHEST timeout 30s | Hop ngay | " .. tostring(count) .. " chest", 100, 2) task.spawn(function() apiLootDone(apiBoss, chestEventId, count, count > 0) end) markServerSuccess(game.JobId) if Config.AutoHop and not SpawnState.hopBusy and tick() >= SpawnState.teleportRetryAfter then SpawnState.lastHopAttempt = tick() hopServer(true) end else apiLootDone(apiBoss, chestEventId, count, completed or count > 0) apiReportCurrent(true) markServerSuccess(game.JobId) if completed then setStatus("CHEST xong | " .. tostring(count) .. " chest", 100, 2) else setStatus("CHEST không còn dùng được", 100, 2) end task.wait(math.max(0.03, tonumber(Config.ApiChestHopDelay) or 0.05)) local nextBoss = Config.AutoFindBoss and getBoss() or nil if nextBoss and aliveModel(nextBoss) then lockedBoss = nextBoss SpawnState.activeBossName = nextBoss.Name SpawnState.activeBossKind = getBossKind(nextBoss) SpawnState.activeApiEventId = apiVerifyBoss(nextBoss) SpawnState.bossLostAt = nil SpawnState.pendingBoss = nil SpawnState.pendingBossSince = nil SpawnState.lastBossSeenAt = tick() elseif Config.AutoHop and not SpawnState.hopBusy and tick() >= SpawnState.teleportRetryAfter then SpawnState.lastHopAttempt = tick() hopServer(true) end end end elseif incomingApiTarget and tostring(incomingApiTarget.status or "") == "chest" then local incomingBoss = apiBossName(incomingApiTarget.boss) local incomingEventId = tostring(incomingApiTarget.event_id or "") local handleKey = incomingEventId ~= "" and incomingEventId or (game.JobId .. "|" .. tostring(incomingBoss or "Chest")) if incomingBoss and not SpawnState.handledChestEvents[handleKey] then SpawnState.incomingChestMissingSince = SpawnState.incomingChestMissingSince or tick() local missingFor = tick() - SpawnState.incomingChestMissingSince local confirmGrace = math.max(5, tonumber(Config.ApiIncomingChestGrace) or 5) if missingFor < confirmGrace then setStatus("CHEST API | Chờ chest load " .. tostring(math.max(0, math.ceil(confirmGrace - missingFor))) .. "s", 100, 1) else SpawnState.handledChestEvents[handleKey] = true SpawnState.incomingChestMissingSince = nil apiLootDone(incomingBoss, incomingEventId, 0, false) incomingApiTarget = nil apiReportCurrent(true) markServerSuccess(game.JobId) setStatus("CHEST xác nhận đã hết | Chuyển server", 100, 2) if Config.AutoHop and not SpawnState.hopBusy and tick() >= SpawnState.teleportRetryAfter then SpawnState.lastHopAttempt = tick() hopServer(true) end end end else SpawnState.incomingChestMissingSince = nil end end task.wait(math.max(0.1, tonumber(Config.ApiChestPollInterval) or 0.25)) end end) task.spawn(function() while running() do if isGameplayReady() and Config.ShowSpawnHUD then updateSpawnHUD() else setSpawnHUDEnabled(false) end task.wait(math.max(0.1, tonumber(Config.SpawnHUDUpdateInterval) or 0.2)) end end) task.spawn(function() local lastText = "" while running() do if isGameplayReady() and Config.AutoTrackSpawn then local skTime, gsTime = getSpawnTimers() local seaName = SpawnState.Hydra and "HydraSeaKing" or "SeaKing" local text = seaName .. ": " .. formatTimer(skTime) .. " | GhostShip: " .. formatTimer(gsTime) if text ~= lastText then print("[SeaFarm Spawn] " .. text) lastText = text end end task.wait(Config.SpawnStatusInterval) end end) task.spawn(function() while running() do if isGameplayReady() and not getBoss() and not SpawnState.lootBusy and not SpawnState.tradeBusy then if Config.AutoStoreFruit then storeFruits() end if Config.AutoConvertFruit then convertFruits() end if Config.AutoDropFruit then dropFruits() end end task.wait(Config.FruitInterval) end end) task.spawn(function() local platinumWarned = false while running() do if isGameplayReady() and Config.AutoBuyKey and BuyKey then local amount = math.max(1, tonumber(Config.BuyKeyAmount) or 1) local ok, result = pcall(function() return BuyKey:InvokeServer(Config.SelectedKey, amount) end) if Config.SelectedKey == "Platinum Key" and (not ok or result == false) then if not platinumWarned then notify("Server không cho mua trực tiếp Platinum Key; script sẽ không spam BuyKey.", 4) platinumWarned = true end task.wait(8) else platinumWarned = false end end task.wait(Config.KeyBuyInterval) end end) task.spawn(function() while running() do if isGameplayReady() and Config.AutoOpenKey and UseKey then remoteInvoke(UseKey, Config.SelectedKey, "Open10") end task.wait(Config.KeyOpenInterval) end end) task.spawn(function() while running() do if isGameplayReady() and Config.TradeReceiverMode == true then local now = tick() if not SpawnState.tradeReceiverPingBusy and now - (tonumber(SpawnState.tradeLastReceiverPingAt) or 0) >= math.max(2, tonumber(Config.TradeReceiverHeartbeatInterval) or 5) then SpawnState.tradeLastReceiverPingAt = now SpawnState.tradeReceiverPingBusy = true task.spawn(function() pcall(TradeFlow.receiverPing) SpawnState.tradeReceiverPingBusy = false end) end if not SpawnState.tradeIncomingBusy and now - (tonumber(SpawnState.tradeIncomingLastAttemptAt) or 0) >= math.max(0.5, tonumber(Config.TradeIncomingPollInterval) or 0.5) then SpawnState.tradeIncomingLastAttemptAt = now SpawnState.tradeIncomingBusy = true task.spawn(function() pcall(TradeFlow.refreshIncoming) SpawnState.tradeIncomingBusy = false end) end end task.wait(0.2) end end) task.spawn(function() while running() do if isGameplayReady() then if Config.TradeReceiverMode == true then TradeFlow.acceptAllowed() TradeFlow.receiverReady() local tradeWith = player:FindFirstChild("TradeWith") if not tradeWith or tostring(tradeWith.Value or "") == "" then setStatus("ACC NHẬN | Đứng im server | " .. game.JobId, 30, 1.5) end elseif Config.AutoTrade then if incomingTradeTarget then TradeFlow.processSenderSession() elseif SpawnState.tradeMissionActive then TradeFlow.startMission() elseif not lockedBoss and not getBoss() and not SpawnState.lootBusy and not SpawnState.lootPriority and not SpawnState.fruitBusy and not SpawnState.hopBusy then TradeFlow.acceptAllowed() if TradeFlow.receiverName() ~= "" and TradeFlow.hasTrigger() then TradeFlow.startMission() end end end end task.wait(math.max(0.2, tonumber(Config.TradeInterval) or 0.25)) end end) task.spawn(function() while running() do if isGameplayReady() then checkMaterialWebhook() end task.wait(math.max(5, tonumber(Config.WebhookCheckInterval) or 20)) end end) notify("SeaFarm standalone loaded.", 4)