-- PuckAFK | Dungeon Lootr Autofarm v1.6.0 -- Cross-place continuation + reliable blessings + sticky potion stations + continuous fly navigation + noclip + hard enemy-clear target lock. -- v1.5.1 adds full Catacombs support: layout-index navigation, zone-less room geometry, RoomIndex enemy ownership, Dark Revenant miniboss handling, and Verath boss-room synchronization. -- Supports every subplace in the live/test Dungeon Lootr universes, including lobby, dungeon and AFK places. -- Auto run mode uses the game's solo start path when alone and matchmaking for parties. -- Failed runs only cool down that exact map+difficulty briefly; they no longer globally push Auto several tiers/maps downward. -- Room transitions stay locked forward until actual ZoneEntered; all fly targets are guarded by dungeon bounds and a global void/escape watchdog. local source = [====[ -- PuckAFK Dungeon Lootr v1.6.0 -- Built against the supplied lobby plus Knights and Catacombs dungeon captures from September 9, 2026. -- Uses the live PuckUI, including its native profile and layout system. local env = (getgenv and getgenv()) or _G local KEY = "__PUCK_DUNGEON_LOOTR" if env[KEY] and env[KEY].Unload then pcall(env[KEY].Unload) end local Players = game:GetService("Players") local RS = game:GetService("ReplicatedStorage") local RunService = game:GetService("RunService") local Collection = game:GetService("CollectionService") local Pathfinding = game:GetService("PathfindingService") local TeleportService = game:GetService("TeleportService") local LP = Players.LocalPlayer if not game:IsLoaded() then game.Loaded:Wait() end local SUPPORTED_UNIVERSES = { [9656201728] = true, -- live [10004532523] = true, -- test } local KNOWN_PLACES = { [106484206883664] = "Lobby", [132285059959516] = "Dungeon", [129976314326660] = "AFK", [95503046258891] = "Lobby", [89954929513873] = "Dungeon", [103006040833273] = "AFK", } local function supportedPlace() return SUPPORTED_UNIVERSES[game.GameId] == true or KNOWN_PLACES[game.PlaceId] ~= nil end if not supportedPlace() then warn("PuckAFK: Run this script in Dungeon Lootr.") return end local placeRole = KNOWN_PLACES[game.PlaceId] or "Subplace" local placeConfig pcall(function() local info=RS:FindFirstChild("GameInfo") local m=info and info:FindFirstChild("PlaceConfig") if m then placeConfig=require(m) end end) if placeConfig then if placeConfig.IsLobbyPlace then placeRole="Lobby" elseif placeConfig.IsDungeonPlace then placeRole="Dungeon" elseif placeConfig.IsAFKPlace then placeRole="AFK" end end local resumed = env.__PUCK_DUNGEON_RESUME env.__PUCK_DUNGEON_RESUME = nil local O = { Enabled = false, Dungeon = "Auto", Difficulty = "Auto", QueueMode = "Auto", RunAction = "Prepare and repeat", QueueTimeout = 55, AvoidCatacombsNormal = true, Movement = "Fast", Speed = 85, FlyNavigation = true, FlyHeight = 8, Noclip = true, Position = "Behind", Distance = 5, Skills = true, Ultimate = true, AutoDodge = true, DodgeTelegraphs = true, DodgeBasics = true, DodgeProjectiles = true, Heal = true, HealAt = 55, RetreatAt = 22, Loot = true, Buffs = true, Blessing = "Balanced", Equip = true, Stats = true, LobbyOptimize = true, RespecStats = true, SafeForge = true, Identify = true, Expand = true, Sell = true, SellRarity = "Uncommon", Buy = true, Reserve = 2500, CraftGear = true, CraftClassUnlocks = true, BestPotion = true, OpenGearPacks = true, FreeChests = true, PatchRewards = true, BuyWeapons = true, RestockPotions = true, ExtraRewards = true, SmartBank = true, SynthesizeMaterials = true, RaidPower = false, Forge = false, ForgeCap = 20, Rewards = true, ContinueTeleport = true, AntiIdle = true, } if type(resumed) == "table" then for k,v in pairs(resumed) do if O[k] ~= nil and type(O[k]) == type(v) then O[k] = v end end end -- Internal safety workaround; intentionally not exposed in the simplified UI. O.AvoidCatacombsNormal=true local S = {Alive=true, Connections={}, Jobs={}, Cooldown={}, Busy={}, Errors={}, Phase="", Zone=0, Cleared={}, Rooms={}, Enemies={}, Prompts={}, PromptUntil={}, UsedPrompts={}, LockedPrompt=nil, LockedPromptKind=nil, LockedPromptAt=0, LockedPromptRetries=0, LockedPromptFireAt=0, PotionStationPrompt=nil, PotionStationAwait=0, PotionStationBefore=nil, PotionStationVerifyUntil=0, ActiveBlessingPrompt=nil, BlessingSelection=nil, LastPromptRefresh=0, LootCollectedUIDs={}, Started=os.clock(), LastProgress=os.clock(), LastEnemy=os.clock(), ZoneAt=os.clock(), Runs=0, Wins=0, Deaths=0, LootCount=0, Status="Loading", Target=nil, Data=nil, Modules={}, Controllers={}, Failures={}, Goal=nil, Path=nil, LastPath=0, PathGoal=nil, PathIndex=nil, PathBusy=false, AvoidWallUntil=0, WallHitAt=0, FlyCruiseY=nil, FlyGoal=nil, CollisionOriginal={}, NoclipApplied=false, LastSafePosition=nil, LastSafeAt=0, LastEnemySweep=0, LastDeepEnemySweep=0, EnemyRoomCache={}, EnemyOutOfBoundsSince={}, InvalidEnemyUntil={}, LastConfirmedRoom=0, LastConfirmedRoomAt=0, FlightRecoveryAt=0, FlightOutsideSince=0, CharacterSpawnAt=0, DodgeUntil=0, LastDodgeAt=0, DodgeReason=nil, DodgeTrackState={}, TelegraphState={}, ProjectileState={}, DodgeThreatUntil={}, BossAbilityCache={}, BossEngaged={}, ActiveBossLock=nil, BossEntryAt=0, BossEntryStage=0, BossEntryPoint=nil, BossEntryCycleAt=0, BossTouchAt=0, ServerRoomIndex=0, NoEnemySince=0, RoomTransitionIndex=nil, RoomTransitionAt=0, RoomTransitionPoint=nil, LastZoneEnteredIndex=0, CombatLock=nil, CombatLockRoom=nil, CombatLockLostAt=0, BlockingEnemyCount=0, RoomClearQuietSince=0, Layout=nil, LayoutByIndex={}, LayoutOrder={}, BossLayoutIndex=nil, TreasureLayout={}, LayoutCurrentIndex=0, DungeonId=nil, QueueAt=0, QueueCanStart=false, QueueSanitized=false, QueueFailures=0, ChestSelectionState=nil, ChestSelectionGraceUntil=0, EndChestResolved=false, LobbyPrepared=false, LobbyPrepAt=0, LobbyPrepSummary=nil, LastOptimizedClass=nil, PublicCodesTried={}, SocialChecksTried={}, ExtraRewardPass=0, WeaponBuyFailures={}, HoldUntil=0, Session=0, SkillCursor=1, Boot=os.clock(), } if type(resumed)=="table" then S.Runs=math.max(0,tonumber(resumed.__Runs) or S.Runs) S.Wins=math.max(0,tonumber(resumed.__Wins) or S.Wins) S.Deaths=math.max(0,tonumber(resumed.__Deaths) or S.Deaths) S.LootCount=math.max(0,tonumber(resumed.__LootCount) or S.LootCount) if type(resumed.__LastOptimizedClass)=="string" then S.LastOptimizedClass=resumed.__LastOptimizedClass end end env[KEY] = S local function track(connection) table.insert(S.Connections, connection); return connection end local function job(fn) local thread thread = task.defer(function() local ok,err = pcall(fn) if not ok and S.Alive then S.Errors.Job=tostring(err) end S.Jobs[coroutine.running()] = nil end) S.Jobs[thread]=true return thread end local function ready(key, delay) local now=os.clock() if now < (S.Cooldown[key] or 0) then return false end S.Cooldown[key]=now+delay return true end local function part(x) if not x then return nil end if x:IsA("BasePart") then return x end if x:IsA("Model") then return x:FindFirstChild("HumanoidRootPart") or x.PrimaryPart or x:FindFirstChildWhichIsA("BasePart") end if x:IsA("Attachment") then return x.Parent:IsA("BasePart") and x.Parent or nil end return nil end local function character() local c=LP.Character local h=c and c:FindFirstChildOfClass("Humanoid") local r=c and c:FindFirstChild("HumanoidRootPart") if not c or not h or not r or h.Health<=0 or LP:GetAttribute("Dead") then return nil end return c,h,r end local function restoreCharacterCollisions() local originals=S.CollisionOriginal or {} for p,canCollide in pairs(originals) do if p and p.Parent then pcall(function() p.CanCollide=canCollide end) end end S.CollisionOriginal={} S.NoclipApplied=false end local function applyCharacterNoclip() local c=LP.Character if not c then return end S.CollisionOriginal=S.CollisionOriginal or {} for _,v in ipairs(c:GetDescendants()) do if v:IsA("BasePart") then if S.CollisionOriginal[v]==nil then S.CollisionOriginal[v]=v.CanCollide end if v.CanCollide then pcall(function() v.CanCollide=false end) end end end S.NoclipApplied=true end local function active() return S.Alive and O.Enabled end local function inRun() return LP:GetAttribute("InDungeon")==true end local function setStatus(s) S.Status=s end local function stopMotion() S.Goal=nil; S.Path=nil; S.Target=nil; S.FlyCruiseY=nil; S.FlyGoal=nil local _,h,r=character() if h and r then h:MoveTo(r.Position); h:Move(Vector3.zero) end if S.RotationHumanoid then pcall(function() S.RotationHumanoid.AutoRotate=S.OldAutoRotate end) S.RotationHumanoid=nil end end local services local function findServices() if services and services.Parent then return services end local packages=RS:FindFirstChild("Packages") local index=packages and packages:FindFirstChild("_Index") if index then for _,p in ipairs(index:GetChildren()) do if p.Name:match("^sleitnick_knit@") then local k=p:FindFirstChild("knit") services=k and k:FindFirstChild("Services") if services then return services end end end end end local function remote(service, method, kind) local root=findServices(); local s=root and root:FindFirstChild(service) local f=s and s:FindFirstChild(kind or "RF") return f and f:FindFirstChild(method) end local function call(service, method, ...) if not active() then return false, "Stopped" end local r=remote(service,method) if not r or not r:IsA("RemoteFunction") then return false, "Unavailable" end local key=service.."."..method if S.Busy[key] then return false,"Pending" end S.Busy[key]=true local args=table.pack(...); local result; local done=false job(function() result=table.pack(pcall(function() return r:InvokeServer(table.unpack(args,1,args.n)) end)) done=true; S.Busy[key]=nil end) local deadline=os.clock()+7 repeat task.wait(0.04) until done or not active() or os.clock()>deadline if not done or not active() then return false, done and "Stopped" or "Pending" end if not result[1] then S.Errors[key]=tostring(result[2]); return false,result[2] end return true,table.unpack(result,2,result.n) end local function accepted(ok,a) return ok and a~=false and not (type(a)=="table" and a.Success==false) end local function resultMessage(...) local values=table.pack(...) for i=1,values.n do local v=values[i] if type(v)=="string" and v~="" then local s=v:gsub("_"," "):gsub("%s+"," "):gsub("^%s+",""):gsub("%s+$","") if #s>92 then s=s:sub(1,89).."..." end return s elseif type(v)=="table" then local s=v.Reason or v.Message or v.Error or v.error if type(s)=="string" and s~="" then s=s:gsub("_"," "):gsub("%s+"," ") if #s>92 then s=s:sub(1,89).."..." end return s end end end return "server did not accept the request" end local function leaveQueueDetached() local r=remote("DungeonQueueService","RequestLeaveQueue") if not r or not r:IsA("RemoteFunction") then return false end task.spawn(function() pcall(function() r:InvokeServer() end) end) return true end local function clearQueueState() S.Queued=false S.QueueCanStart=false S.QueueAt=0 end local function input(name,...) if not active() then return end local p=RS:FindFirstChild("Player"); local rem=p and p:FindFirstChild("Remotes") local f=rem and rem:FindFirstChild("Inputs"); local r=f and f:FindFirstChild(name) if r and r:IsA("RemoteEvent") then r:FireServer(...) end end local Knit local function loadModules() if not Knit then local p=RS:FindFirstChild("Packages"); local k=p and p:FindFirstChild("Knit") if k then local ok,v=pcall(require,k); if ok then Knit=v end end end if Knit and Knit.Registry then local ok,v=pcall(function() return Knit.Registry:Get("PlayerData") end) if ok and v then S.Data=v end end local info=RS:FindFirstChild("GameInfo") for _,name in ipairs({"DungeonData","RarityData","QuestData","BuffData","PotionData","BattlepassData","Boss_ActionData","NPC_ActionData", "EquipmentData","EquipmentTemplates","EquipmentShopData","WeaponShopData","ItemShopData","CraftingData","ClassItemData","ForgeData","ChestData","PatchNoteData","PackageData", "CodesData","CheckData","ChallengeRewardData","BossRushData","BankData","RaidShopData"}) do if not S.Modules[name] and info then local m=info:FindFirstChild(name) if m then local ok,v=pcall(require,m); if ok then S.Modules[name]=v end end end end if not S.Modules.WeaponData then local weapons=RS:FindFirstChild("Weapons") local wm=weapons and weapons:FindFirstChild("Weapon_Data") if wm then local ok,v=pcall(require,wm) if ok and type(v)=="table" then S.Modules.WeaponData=v end end end if Knit then for _,name in ipairs({"ChestSelectionController","BoostSelectionController"}) do if not S.Controllers[name] then local ok,v=pcall(Knit.GetController,name) if ok then S.Controllers[name]=v end end end end end local function getClassData() if S.Modules.ClassData then return S.Modules.ClassData end local classes=RS:FindFirstChild("Classes") local module=classes and classes:FindFirstChild("Class_Data") if module then local ok,v=pcall(require,module) if ok and type(v)=="table" then S.Modules.ClassData=v end end return S.Modules.ClassData end local function data() return S.Data and S.Data.Data end local function count(t) local n=0; for _ in pairs(type(t)=="table" and t or {}) do n=n+1 end; return n end local rankDefault={Common=1,Uncommon=2,Rare=3,Epic=4,Legendary=5,Mythic=6,Divine=7,Celestial=8,Exotic=9,Admin=10} local function rank(r) local d=S.Modules.RarityData return (d and d.RarityIndex and d.RarityIndex[r]) or rankDefault[r] or 99 end -- Use the same gear-score implementation that the live Inventory/Forge UI uses. ForgeBonuses -- are included so a properly forged item cannot be replaced by a superficially higher-rarity -- un-forged sidegrade. The small class-aware tie score only breaks near/equal gear-score ties. local Lobby={} function Lobby.officialGearScore(item) if type(item)~="table" or item.Identified==false then return 0 end local ed=S.Modules.EquipmentData if ed then local bonuses=type(item.ForgeBonuses)=="table" and item.ForgeBonuses or {} if type(ed.ComputeItemGearScoreWithBonuses)=="function" then local ok,v=pcall(ed.ComputeItemGearScoreWithBonuses,item,bonuses) if ok and type(v)=="number" then return v end end if type(ed.ComputeItemGearScore)=="function" then local ok,v=pcall(ed.ComputeItemGearScore,item) if ok and type(v)=="number" then return v end end end -- Fallback for places where the newer EquipmentData module has not replicated yet. local n=rank(item.Rarity or "Common")*100 n=n+(tonumber(item.LevelReq) or 1)*8 if item.Slot=="Ring" then n=n+(tonumber(item.BaseDamage) or 0) end local stats=item.Stats or item.StatLines or {} for _,v in pairs(stats) do local value=type(v)=="table" and tonumber(v.Value) or tonumber(v) if value then n=n+math.abs(value)*0.25 end end return math.floor(n+0.5) end function Lobby.activeDamageType(d) d=d or data() local active=d and tostring(d.ActiveClass or "") or "" local classes=getClassData() local def if active~="" and classes then if type(classes.Get)=="function" then local ok,v=pcall(classes.Get,active); if ok then def=v end end def=def or classes[active] end return (def and def.DamageType) or "Physical" end function Lobby.statValue(item,key) if type(item)~="table" then return 0 end local total=0 if type(item.GuaranteedStat)=="table" and item.GuaranteedStat.StatKey==key then total=total+(tonumber(item.GuaranteedStat.Value) or 0) end for k,v in pairs(item.Stats or {}) do if k==key then total=total+(tonumber(v) or 0) end end for k,v in pairs(item.StatLines or {}) do if k==key then total=total+(type(v)=="table" and tonumber(v.Value) or tonumber(v) or 0) elseif type(v)=="table" and v.StatKey==key then total=total+(tonumber(v.Value) or 0) end end local bonuses=item.ForgeBonuses or {} total=total+(tonumber(bonuses[key]) or 0) return total end function Lobby.classTieScore(item,d) if type(item)~="table" then return 0 end local damageKey=({Physical="PhysicalDamage",Ranged="RangedDamage",Magic="MagicDamage"})[Lobby.activeDamageType(d)] or "PhysicalDamage" local n=Lobby.statValue(item,damageKey)*8 for _,k in ipairs({"BonusDamage","AttackDamageBonus","SkillDamageBonus","ArmorShred","CritRate","CritDamage","AttackSpeed","SkillCritChance","SkillCritDamage","LifeSteal"}) do n=n+Lobby.statValue(item,k)*2 end for _,k in ipairs({"DamageReduction","Defense","MaxHP","DodgeRate","CooldownReduction"}) do n=n+Lobby.statValue(item,k)*0.25 end n=n-Lobby.statValue(item,"DodgeCooldown")*0.25 return math.clamp(n,-900,900) end function Lobby.score(item,d) if type(item)~="table" then return -1 end return Lobby.officialGearScore(item)*1000+Lobby.classTieScore(item,d) end function Lobby.bestOwnedForSlot(d,slot) if not d then return nil,0 end local level=tonumber(d.PlayerLevel) or 1 local best=(d.Equipment or {})[slot] local bestValue=Lobby.score(best,d) for _,it in pairs(d.EquipmentInventory or {}) do if type(it)=="table" and it.GUID and it.Slot==slot and it.Identified~=false and not it.Locked and (tonumber(it.LevelReq) or 0)<=level then local v=Lobby.score(it,d) if v>bestValue then best=it; bestValue=v end end end return best,Lobby.officialGearScore(best) end function Lobby.equippedSet(d) local ids={} for _,v in pairs(d.Equipment or {}) do if type(v)=="table" and v.GUID then ids[v.GUID]=true end end return ids end function Lobby.equipBest() if not O.Equip then return 0 end local d=data(); if not d then return 0 end local best={}; local level=tonumber(d.PlayerLevel) or 1 for _,slot in ipairs({"Head","Body","Ring"}) do best[slot]=(d.Equipment or {})[slot] end for _,v in pairs(d.EquipmentInventory or {}) do if type(v)=="table" and v.GUID and v.Identified~=false and not v.Locked and (tonumber(v.LevelReq) or 0)<=level and (v.Slot=="Head" or v.Slot=="Body" or v.Slot=="Ring") then if Lobby.score(v)>Lobby.score(best[v.Slot]) then best[v.Slot]=v end end end local ids=Lobby.equippedSet(d); local changed=0 for _,slot in ipairs({"Head","Body","Ring"}) do local v=best[slot] if v and v.GUID and not ids[v.GUID] then setStatus("Equipping best "..slot:lower()) local ok,a=call("EquipmentService","Equip",v.GUID) if accepted(ok,a) then changed=changed+1; task.wait(0.22) end end end return changed end local function capacity(d) return tonumber(d.MaxInventorySlots) or 60 end function Lobby.sellJunk(force) local d=data(); if not d or not O.Sell then return 0 end local cap=capacity(d); local used=count(d.EquipmentInventory) if not force and (not cap or usedLobby.score(best[v.Slot]) then best[v.Slot]=v end -- Keep all future-level equipment and unidentified items. if (tonumber(v.LevelReq) or 0)>(tonumber(d.PlayerLevel) or 1) or v.Identified==false then protected[v.GUID]=true end end end for _,v in pairs(best) do protected[v.GUID]=true end -- Saved loadouts may refer to GUIDs in nested tables. local function protect(t,depth) if depth>6 then return end if type(t)=="string" then protected[t]=true elseif type(t)=="table" then for _,v in pairs(t) do protect(v,depth+1) end end end protect(d.EquipmentLoadouts or {},0); protect(d.Loadouts or {},0) local sell={}; local maxRank=rank(O.SellRarity) for _,v in pairs(d.EquipmentInventory or {}) do local current=type(v)=="table" and (d.Equipment or {})[v.Slot] if type(v)=="table" and v.GUID and v.Slot and not ids[v.GUID] and not protected[v.GUID] and not v.Locked and not v.Favorite and not v.Favourited and v.Identified~=false and rank(v.Rarity or "Common")<=maxRank and Lobby.score(v)=20 then break end end end if #sell>0 then setStatus("Selling spare gear"); local ok,a=call("ShopService","SellEquipment",sell); if accepted(ok,a) then task.wait(0.35); return #sell end end return 0 end function Lobby.expandInventory() local d=data() if not O.Expand or not d or count(d.EquipmentInventory)Lobby.score(b) end) local item=items[1]; attempted[item.GUID]=true local cap=capacity(d) if cap and count(d.EquipmentInventory)>=cap then Lobby.equipBest(); Lobby.sellJunk(true); d=data() if d and count(d.EquipmentInventory)>=capacity(d) then Lobby.expandInventory(); d=data() end if not d or count(d.EquipmentInventory)>=capacity(d) then setStatus("Inventory full — keeping remaining stored loot") break end end setStatus("Collecting stored dungeon loot") local ok,a=call("EquipmentService","CollectSingle",item.GUID) if accepted(ok,a) then collected=collected+1; task.wait(0.16) end end return collected end function Lobby.buyUpgrade() if not O.Buy then return false end local d=data(); if not d then return false end local cap=capacity(d) if cap and count(d.EquipmentInventory)>=cap-2 then Lobby.sellJunk(true); d=data(); cap=d and capacity(d) if not d or (cap and count(d.EquipmentInventory)>=cap-2) then return false end end local ok,list=call("ShopService","GetEquipmentShopInfo") if not ok or type(list)~="table" then return false end local level=tonumber(d.PlayerLevel) or 1 local spendable=math.max(0,(tonumber(d.Currency) or 0)-O.Reserve) local best,bestGain,bestRelative,bestTie=nil,0,0,-math.huge for _,v in pairs(list) do if type(v)=="table" and v.GUID and (v.Slot=="Head" or v.Slot=="Body" or v.Slot=="Ring") and v.Identified~=false and type(v.Cost)=="number" and v.Cost<=spendable and (tonumber(v.LevelReq) or 0)<=level then local owned,ownedGS=Lobby.bestOwnedForSlot(d,v.Slot) local candidateGS=Lobby.officialGearScore(v) local gain=candidateGS-ownedGS local minGain=math.max(3,math.floor(ownedGS*0.005+0.5)) local relative=ownedGS>0 and gain/ownedGS or gain local tie=Lobby.classTieScore(v,d)-(owned and Lobby.classTieScore(owned,d) or 0) -- Largest actual gear-score jump first. Cost efficiency is only a final tie-breaker; -- this prevents cheap +1 sidegrades from consuming coins before a major armor upgrade. if gain>=minGain then local better=gain>bestGain or (gain==bestGain and relative>bestRelative) or (gain==bestGain and math.abs(relative-bestRelative)<1e-6 and tie>bestTie) or (gain==bestGain and math.abs(relative-bestRelative)<1e-6 and tie==bestTie and best and v.Costmath.max(0,(tonumber(d.Currency) or 0)-O.Reserve) then return false end local materials=d.CraftingMaterials or {} for _,req in ipairs(recipe.Materials or {}) do if (tonumber(materials[req.Id]) or 0)<(tonumber(req.Amount) or 0) then return false end end return true end function Lobby.craftRecipe(id) local cd=S.Modules.CraftingData local recipe=cd and ((type(cd.Get)=="function" and cd.Get(id)) or (cd.Recipes and cd.Recipes[id])) local d=data() if not recipe or not Lobby.recipeCanCraft(recipe,d) then return false end if recipe.OutputType=="Equipment" then local cap=capacity(d) if cap and count(d.EquipmentInventory)>=cap-1 then Lobby.sellJunk(true); d=data() if d and count(d.EquipmentInventory)>=capacity(d) then Lobby.expandInventory(); d=data() end if not d or count(d.EquipmentInventory)>=capacity(d) then return false end end end setStatus("Crafting "..tostring(id)) local ok,a,b=call("CraftingService","Craft",id,1) if accepted(ok,a) then task.wait(0.4); return true end S.Errors["Crafting."..tostring(id)]=resultMessage(a,b) return false end function Lobby.expectedCraftGearScore(recipe,d) if type(recipe)~="table" or not d then return 0 end local sample={ Identified=true, Rarity=recipe.Rarity or "Common", LevelReq=recipe.CraftAtPlayerLevel and (tonumber(d.PlayerLevel) or 1) or (tonumber(recipe.LevelReq) or tonumber(d.PlayerLevel) or 1), Slot=recipe.Slot, } return Lobby.officialGearScore(sample) end function Lobby.craftGearUpgrades() if not O.CraftGear then return 0 end local cd=S.Modules.CraftingData; if not cd then return 0 end local crafted=0 local recipeIds={"KingsCrown","LivingArmor","SupernovaRing"} -- One Exotic Shattered Armor is shared by all three recipes. Re-evaluate after every craft and -- spend scarce materials on the slot with the biggest official GS improvement first. for pass=1,3 do local d=data(); if not d then break end local bestId,bestGain=nil,0 for _,id in ipairs(recipeIds) do local recipe=(type(cd.Get)=="function" and cd.Get(id)) or (cd.Recipes and cd.Recipes[id]) if type(recipe)=="table" and recipe.OutputType=="Equipment" and recipe.Slot and Lobby.recipeCanCraft(recipe,d) then local _,currentGS=Lobby.bestOwnedForSlot(d,recipe.Slot) local expected=Lobby.expectedCraftGearScore(recipe,d) local gain=expected-currentGS local minGain=math.max(5,math.floor(currentGS*0.01+0.5)) if gain>=minGain and gain>bestGain then bestId,bestGain=id,gain end end end if not bestId then break end setStatus("Crafting +"..math.floor(bestGain).." GS permanent upgrade") if not Lobby.craftRecipe(bestId) then break end crafted=crafted+1 Lobby.identifyLoot(); Lobby.equipBest() end return crafted end function Lobby.ownsClassItem(d,id) local items=d and d.ClassItems if type(items)~="table" then return false end if items[id]==true or type(items[id])=="table" then return true end for k,v in pairs(items) do if k==id and v then return true end if v==id then return true end if type(v)=="table" and (v.Id==id or v.ItemId==id or v.ClassItemId==id or v.Name==id) then return true end end return false end function Lobby.withdrawBankUpgrades() if not O.SmartBank then return 0 end local moved=0 for pass=1,6 do local d=data() if not d or type(d.BankEquipment)~="table" or #d.BankEquipment==0 then break end local level=tonumber(d.PlayerLevel) or 1 local best,bestGain=nil,0 for _,it in pairs(d.BankEquipment) do if type(it)=="table" and it.GUID and it.Identified~=false and not it.Locked and (it.Slot=="Head" or it.Slot=="Body" or it.Slot=="Ring") and (tonumber(it.LevelReq) or 0)<=level then local _,ownedGS=Lobby.bestOwnedForSlot(d,it.Slot) local bankGS=Lobby.officialGearScore(it) local gain=bankGS-ownedGS if gain>bestGain then best,bestGain=it,gain end end end if not best or bestGain<2 then break end local cap=capacity(d) if count(d.EquipmentInventory)>=cap then Lobby.sellJunk(true) d=data() if not d or count(d.EquipmentInventory)>=capacity(d) then Lobby.expandInventory() d=data() end if not d or count(d.EquipmentInventory)>=capacity(d) then break end end setStatus("Withdrawing +"..math.floor(bestGain).." GS gear from storage") local ok,a,b=call("BankService","WithdrawEquipment",best.GUID) if not accepted(ok,a) then S.Errors.BankWithdraw=resultMessage(a,b) break end moved=moved+1 task.wait(0.3) Lobby.equipBest() end return moved end function Lobby.weaponScore(id) local wd=S.Modules.WeaponData local def=wd and wd[id] if type(def)~="table" then return 0 end local damage=tonumber(def.Damage) or 0 local speed=tonumber(def.AttackSpeed) or 1 local rangeValue=tonumber(def.Range) or 0 local rarity=rank(def.Rarity or "Common") local skillBonus=def.Skill and 2 or 0 local dodge=tonumber(def.DodgeCooldown) or 2 -- PvE sustained basic damage is the dominant signal. Range/rarity/skill and faster dodge -- recovery only break reasonably close weapon choices. return damage*speed + rangeValue*0.12 + rarity*1.3 + skillBonus + math.max(0,2-dodge)*1.5 end function Lobby.ownedWeaponSet(d) local set={} local owned=d and d.Owned_Weapons if type(owned)=="table" then for k,v in pairs(owned) do if type(k)=="string" and v==true then set[k]=true end if type(v)=="string" then set[v]=true end if type(v)=="table" then local id=v.Id or v.ItemId or v.WeaponId or v.Name if type(id)=="string" then set[id]=true end end end end return set end function Lobby.buyBetterWeapon() if not O.BuyWeapons then return false end local d=data() local wsd=S.Modules.WeaponShopData if not d or not wsd or type(d.Owned_Weapons)~="table" then return false end local owned=Lobby.ownedWeaponSet(d) local bestOwned=0 for id in pairs(owned) do bestOwned=math.max(bestOwned,Lobby.weaponScore(id)) end local rebirths=tonumber(d.Rebirths) or 0 local spendable=math.max(0,(tonumber(d.Currency) or 0)-O.Reserve) local items=type(wsd.GetAvailableItems)=="function" and wsd.GetAvailableItems(rebirths) or wsd.Items or {} local candidate,candidateScore=nil,bestOwned for _,it in ipairs(items) do if type(it)=="table" and it.Type=="Weapon" and type(it.Id)=="string" and not owned[it.Id] and (tonumber(it.RequiredRebirth) or 0)<=rebirths and (tonumber(it.Cost) or math.huge)<=spendable then local score=Lobby.weaponScore(it.Id) if score>candidateScore then candidate,candidateScore=it,score end end end -- Avoid spending millions for an unlock that is not actually stronger than what is already owned. local minimum=bestOwned>0 and bestOwned*1.025 or 0 if not candidate or candidateScore<=minimum then return false end setStatus("Buying stronger weapon: "..candidate.Id) local ok,a,b=call("ShopService","BuyWeapon",candidate.Id) if accepted(ok,a) then task.wait(0.4) return true end S.WeaponBuyFailures[candidate.Id]=resultMessage(a,b) return false end function Lobby.restockHealingPotions() if not O.RestockPotions then return 0 end local d=data() local isd=S.Modules.ItemShopData local pd=S.Modules.PotionData if not d or not isd or not pd then return 0 end local rebirths=tonumber(d.Rebirths) or 0 local candidates={} for _,it in ipairs(isd.Items or {}) do if type(it)=="table" and it.Type=="Potion" and it.PotionId and (tonumber(it.RequiredRebirth) or 0)<=rebirths then local p=type(pd.GetPotion)=="function" and pd.GetPotion(it.PotionId) or pd[it.PotionId] if p then local flat=tonumber(p.HealAmount or p.HealFlat or p.Amount) or 0 local pct=tonumber(p.HealPercent or p.PercentHeal) or 0 local score=flat+pct*150 table.insert(candidates,{Shop=it,Score=score}) end end end table.sort(candidates,function(a,b) if a.Score~=b.Score then return a.Score>b.Score end return (tonumber(a.Shop.Cost) or 0)>(tonumber(b.Shop.Cost) or 0) end) local bought=0 for _,entry in ipairs(candidates) do local item=entry.Shop d=data() if not d then break end local have=tonumber((d.Potions or {})[item.PotionId]) or 0 if have<4 then for _=have+1,4 do d=data(); if not d then break end local spendable=math.max(0,(tonumber(d.Currency) or 0)-O.Reserve) if (tonumber(item.Cost) or math.huge)>spendable then break end setStatus("Restocking "..tostring(item.Name or item.Id)) local ok,a=call("ShopService","BuyItem",item.Id) if not accepted(ok,a) then break end bought=bought+1 task.wait(0.18) end if bought>0 then break end end end if bought>0 then Lobby.equipBestPotion() end return bought end function Lobby.claimPublicCodes() if not (O.Rewards and O.ExtraRewards) then return 0 end local cd=S.Modules.CodesData if not cd or type(cd.Codes)~="table" then return 0 end -- The live Codes UI requires group membership. Never prompt/join the group on the user's behalf; -- only redeem automatically when the server already says the player is a member. local ok,inGroup=call("CodesService","IsInGroup") if not ok or inGroup~=true then return 0 end local codes={} for code,def in pairs(cd.Codes) do if type(code)=="string" and type(def)=="table" and def.Active==true and not def.RequiresRole then table.insert(codes,code) end end table.sort(codes) local claimed=0 for _,code in ipairs(codes) do if not S.PublicCodesTried[code] then S.PublicCodesTried[code]=true setStatus("Checking reward code "..code) local good,a=call("CodesService","RedeemCode",code) if accepted(good,a) then claimed=claimed+1; task.wait(0.12) end end end return claimed end function Lobby.claimSocialChecks() if not (O.Rewards and O.ExtraRewards) then return 0 end local checkData=S.Modules.CheckData if not checkData or type(checkData.Checks)~="table" then return 0 end local claimedSet={} local ok,claimed=call("CheckService","GetClaimedChecks") if ok and type(claimed)=="table" then for _,idx in pairs(claimed) do claimedSet[tonumber(idx) or idx]=true end end local total=0 for i=1,#checkData.Checks do if not claimedSet[i] and not S.SocialChecksTried[i] then S.SocialChecksTried[i]=true local good,result=call("CheckService","ClaimCheck",i) if good and (result=="CLAIMED" or result=="ALREADY" or result==true) then total=total+1 task.wait(0.08) end end end return total end function Lobby.claimChallengeRewards() if not (O.Rewards and O.ExtraRewards) then return 0 end local crd=S.Modules.ChallengeRewardData local d=data() local progress=d and d.ChallengeDungeon if not crd or type(crd.MILESTONES)~="table" or type(progress)~="table" then return 0 end local highest=tonumber(progress.HighestFloor) or 0 local sameSeason=progress.Season==nil or tonumber(progress.Season)==tonumber(crd.CURRENT_SEASON) if not sameSeason then return 0 end local claimed={} for _,idx in pairs(progress.ClaimedRewards or {}) do claimed[tonumber(idx) or idx]=true end local total=0 for i,milestone in ipairs(crd.MILESTONES) do if not claimed[i] and (tonumber(milestone.Floor) or math.huge)<=highest then setStatus("Claiming challenge floor "..tostring(milestone.Floor).." reward") local ok,a=call("ChallengeRewardService","ClaimReward",i) if accepted(ok,a) then claimed[i]=true total=total+1 task.wait(0.1) end end end return total end function Lobby.claimBossRushRewards() if not (O.Rewards and O.ExtraRewards) then return 0 end local brd=S.Modules.BossRushData local d=data() local progress=d and d.BossRush if not brd or type(brd.MILESTONES)~="table" or type(progress)~="table" then return 0 end local highest=tonumber(progress.HighestFloor) or 0 local sameSeason=progress.Season==nil or tonumber(progress.Season)==tonumber(brd.CURRENT_SEASON) if not sameSeason then return 0 end local claimed={} for _,idx in pairs(progress.ClaimedRewards or {}) do claimed[tonumber(idx) or idx]=true end local total=0 for i,milestone in ipairs(brd.MILESTONES) do if not claimed[i] and (tonumber(milestone.Floor) or math.huge)<=highest then setStatus("Claiming boss-rush floor "..tostring(milestone.Floor).." reward") local ok,a=call("BossRushService","ClaimMilestoneReward",i) if accepted(ok,a) then claimed[i]=true total=total+1 task.wait(0.1) end end end return total end function Lobby.claimExtraRewards() if not (O.Rewards and O.ExtraRewards) then return 0 end local total=0 total=total+Lobby.claimPublicCodes() total=total+Lobby.claimSocialChecks() total=total+Lobby.claimChallengeRewards() total=total+Lobby.claimBossRushRewards() return total end function Lobby.ensureCraftMaterial(id,amount,depth) if not O.SynthesizeMaterials then return false end depth=tonumber(depth) or 0 if depth>8 then return false end local d=data() local cd=S.Modules.CraftingData local recipe=cd and ((type(cd.Get)=="function" and cd.Get(id)) or (cd.Recipes and cd.Recipes[id])) local current=d and tonumber((d.CraftingMaterials or {})[id]) or 0 amount=math.max(0,math.ceil(tonumber(amount) or 0)) if current>=amount then return true end if not d or type(recipe)~="table" or recipe.OutputType~="CraftingMaterial" then return false end if #(recipe.Materials or {})~=1 then return false end local req=recipe.Materials[1] local perCraft=math.max(1,tonumber(recipe.Amount) or 1) local needOut=amount-current local crafts=math.ceil(needOut/perCraft) local reqAmount=crafts*(tonumber(req.Amount) or 0) if not Lobby.ensureCraftMaterial(req.Id,reqAmount,depth+1) then return false end d=data() if not d then return false end local spendable=math.max(0,(tonumber(d.Currency) or 0)-O.Reserve) local coinCost=(tonumber(recipe.CoinCost) or 0)*crafts if coinCost>spendable then return false end setStatus("Synthesizing "..tostring(id).." x"..crafts) local ok,a=call("CraftingService","Craft",id,crafts) if not accepted(ok,a) then return false end task.wait(0.3) d=data() return d and (tonumber((d.CraftingMaterials or {})[id]) or 0)>=amount end function Lobby.synthesizeForBestExotic() if not (O.CraftGear and O.SynthesizeMaterials) then return false end local cd=S.Modules.CraftingData local d=data() if not cd or not d then return false end local bestRecipe,bestGain=nil,0 for _,id in ipairs({"KingsCrown","LivingArmor","SupernovaRing"}) do local recipe=(type(cd.Get)=="function" and cd.Get(id)) or (cd.Recipes and cd.Recipes[id]) if type(recipe)=="table" and recipe.OutputType=="Equipment" and recipe.Slot then local mats=d.CraftingMaterials or {} local otherReady=true local celestialNeed=0 for _,req in ipairs(recipe.Materials or {}) do if req.Id=="Celestial Ore" then celestialNeed=tonumber(req.Amount) or 0 elseif (tonumber(mats[req.Id]) or 0)<(tonumber(req.Amount) or 0) then otherReady=false break end end if otherReady and celestialNeed>0 then local _,currentGS=Lobby.bestOwnedForSlot(d,recipe.Slot) local gain=Lobby.expectedCraftGearScore(recipe,d)-currentGS local minGain=math.max(5,math.floor(currentGS*0.01+0.5)) if gain>=minGain and gain>bestGain then bestRecipe,bestGain=recipe,gain end end end end if not bestRecipe then return false end for _,req in ipairs(bestRecipe.Materials or {}) do if req.Id=="Celestial Ore" then return Lobby.ensureCraftMaterial("Celestial Ore",tonumber(req.Amount) or 0,0) end end return false end function Lobby.buyRaidPower() if not O.RaidPower then return 0 end local rsd=S.Modules.RaidShopData if not rsd then return 0 end local raidId="The First Test" local ok,info=call("RaidShopService","GetRaidShopInfo",raidId) if not ok or type(info)~="table" then return 0 end local total=0 for _,entryId in ipairs({"MoriaBlade","FrostWisteriaWand"}) do local entry=type(rsd.GetEntry)=="function" and rsd.GetEntry(raidId,entryId) or nil local d=data() if entry and d and not Lobby.ownsClassItem(d,entry.GrantId) and not (type(info.Owned)=="table" and info.Owned[entry.Id]==true) and (tonumber(info.Balance) or 0)>=(tonumber(entry.Cost) or math.huge) and (not entry.DailyLimit or (tonumber((info.Counts or {})[entry.Id]) or 0)0 and not p.InstantUse and (tonumber(p.RequiredRebirth) or 0)<=rebirths and p.Type~="ClassXP" then local immediate=tonumber(p.HealAmount) or ((tonumber(p.HealPercent) or 0)*maxHP) local total=immediate if p.Type=="HoT" then local ticks=(tonumber(p.Duration) or 0)/math.max(tonumber(p.TickRate) or 1,0.1) total=(tonumber(p.HealPerTick) or 0)*ticks -- HoT is useful, but an emergency autofarm potion should not outrank a strong -- instant heal purely because its full-duration total is large. immediate=total*0.35 end local cooldown=math.max(tonumber(p.Cooldown) or 1,1) local value=immediate+total*0.08+20/cooldown if value>bestScore then best,bestScore=p,value end end end if best and d.EquippedPotion~=best.Id then setStatus("Equipping best healing potion") local ok,a=call("PotionService","EquipPotion",best.Id) if accepted(ok,a) then task.wait(0.25); return true end end return false end function Lobby.packageMaxGear(id,depth) depth=tonumber(depth) or 0 if depth>6 then return 0 end local pd=S.Modules.PackageData local packages=pd and pd.Packages local def=packages and packages[id] if type(def)~="table" then return 0 end if type(def.RandomPool)=="table" then local maxGear=0 for _,choice in ipairs(def.RandomPool) do if choice.PackageId then maxGear=math.max(maxGear,Lobby.packageMaxGear(choice.PackageId,depth+1)) end end return maxGear end local total=0 for _,entry in ipairs(def.Contents or {}) do if entry.Type=="Equipment" then total=total+math.max(1,tonumber(entry.Amount) or 1) elseif entry.Type=="Package" and entry.Id then total=total+Lobby.packageMaxGear(entry.Id,depth+1) end end return total end function Lobby.isUsefulProgressionPackage(id) local pd=S.Modules.PackageData if not pd or type(id)~="string" then return false end if type(pd.IsGearPackage)=="function" then local ok,v=pcall(pd.IsGearPackage,id) if ok and v==true then return true end end local useful={ ForgeStonePackage=true, ReforgeStonePackage=true, UpgradeMaterialBox=true, MaterialBundle_Tier1=true, MaterialBundle_Tier2=true, MaterialBundle_Tier3=true, RandomGMBlessing=true, GMBlessing1=true, GMBlessing2=true, GMBlessing3=true, GMBlessing4=true, BossPackage=true, MysteryBox=true, RarityChest_Mythic=true, RarityChest_Celestial=true, EventChest_Normal=true, EventChest_Extreme=true, EventChest_Impossible=true, } return useful[id]==true end function Lobby.openProgressionPackages() if not O.OpenGearPacks then return 0 end local pd=S.Modules.PackageData; if not pd then return 0 end local opened=0 for pass=1,40 do local d=data(); if not d then break end local chosen,isGear,maxGear=nil,false,0 for id,amount in pairs(d.Packs or {}) do if (tonumber(amount) or 0)>0 and Lobby.isUsefulProgressionPackage(id) then local gear=false if type(pd.IsGearPackage)=="function" then local ok,v=pcall(pd.IsGearPackage,id) gear=ok and v==true end chosen=id isGear=gear maxGear=Lobby.packageMaxGear(id,0) break end end if not chosen then break end local needed=isGear and 5 or maxGear if needed>0 then local cap=capacity(d); local free=cap-count(d.EquipmentInventory) if free0 then Lobby.identifyLoot() Lobby.equipBest() Lobby.sellJunk(false) end end return opened end function Lobby.claimFreeChests() if not (O.Rewards and O.FreeChests) then return 0 end local d=data(); local cd=S.Modules.ChestData if not d or not cd or type(cd.FreeChests)~="table" then return 0 end local free=d.FreeChests or {} local claimed=free.ClaimedIds or {} local reset=tonumber(free.ResetTimestamp) or 0 local interval=tonumber(cd.FREE_CHEST_RESET_INTERVAL) or 86400 local expired=reset>0 and os.time()-reset>=interval local total=0 for i,def in ipairs(cd.FreeChests) do local already=(not expired) and table.find(claimed,i)~=nil if not already and (tonumber(d.Rebirths) or 0)>=(tonumber(def.RequiredRebirth) or 0) then setStatus("Claiming free lobby chest") local ok,a=call("ChestService","ClaimFreeChest",i) if accepted(ok,a) then total=total+1; task.wait(0.25); d=data() or d end end end return total end function Lobby.claimPatchRewards() if not (O.Rewards and O.PatchRewards) then return 0 end local d=data(); local pd=S.Modules.PatchNoteData if not d or not pd or type(pd.GetOrdered)~="function" then return 0 end local claimed=d.ClaimedPatchNotes or {} local total=0 local ok,notes=pcall(pd.GetOrdered) if not ok or type(notes)~="table" then return 0 end for _,note in ipairs(notes) do if note and note.Id and type(note.Rewards)=="table" and #note.Rewards>0 and claimed[note.Id]~=true then setStatus("Claiming update reward") local good,a=call("PatchNotesService","ClaimReward",note.Id) if accepted(good,a) then total=total+1; task.wait(0.25) end end end return total end function Lobby.forgeGear() if not (O.SafeForge or O.Forge) then return 0 end local total=0 local ed=S.Modules.EquipmentData for pass=1,math.max(1,O.ForgeCap*3) do local d=data(); if not d then break end local best for _,slot in ipairs({"Head","Body","Ring"}) do local it=(d.Equipment or {})[slot] if type(it)=="table" and it.GUID and (tonumber(it.ForgeLevel) or 0)=0.9999 and affordable then local current=Lobby.officialGearScore(it) local projected=current if ed and type(ed.ComputeItemGearScoreWithBonuses)=="function" then local okCurrent,vCurrent=pcall(ed.ComputeItemGearScoreWithBonuses,it,info.ForgeBonuses or it.ForgeBonuses or {}) local okNext,vNext=pcall(ed.ComputeItemGearScoreWithBonuses,it,info.ProjectedBonuses or info.ForgeBonuses or it.ForgeBonuses or {}) if okCurrent and type(vCurrent)=="number" then current=vCurrent end if okNext and type(vNext)=="number" then projected=vNext end end local gain=math.max(0,projected-current) if not best or gain>best.Gain or (gain==best.Gain and current0 and " (+"..math.floor(best.Gain).." GS)" or "")) local good,a=call("ForgeService","ForgeItem",best.Item.GUID,{Protect=false,Enhance=false}) if not accepted(good,a) then break end total=total+1 task.wait(0.32) end return total end function Lobby.claimRewards() if not O.Rewards then return 0 end local d=data(); if not d then return 0 end local claimedCount=0 claimedCount=claimedCount+Lobby.claimFreeChests() claimedCount=claimedCount+Lobby.claimPatchRewards() claimedCount=claimedCount+Lobby.claimExtraRewards() d=data() or d local qd=S.Modules.QuestData if qd then for category,group in pairs(d.Quests or {}) do if type(group)=="table" then for i,q in pairs(group.Active or {}) do local def=q.QuestId and qd.GetQuestById(q.QuestId) if not q.Claimed and (q.Completed or (def and (tonumber(q.Progress) or 0)>=(tonumber(def.Target) or math.huge))) then local ok,a=call("QuestService","ClaimQuest",category,i) if accepted(ok,a) then claimedCount=claimedCount+1 end end end end end end local bp=d.Battlepass if type(bp)=="table" then local bpd=S.Modules.BattlepassData for i,q in pairs(bp.Quests or {}) do if type(q)=="table" and not q.Claimed then local def=bpd and q.QuestId and bpd.GetQuestById(q.QuestId) if q.Completed or (def and (tonumber(q.Progress) or 0)>=(tonumber(def.Target) or math.huge)) then local ok,a=call("BattlepassService","ClaimQuest",i) if accepted(ok,a) then claimedCount=claimedCount+1 end end end end for tier=1,math.min(tonumber(bp.Tier) or 0,100) do for _,trackName in ipairs({"Free","Premium"}) do if trackName=="Free" or bp.HasPremium==true then local claimed=bp["Claimed"..trackName] or {} if not table.find(claimed,tier) then local ok,a=call("BattlepassService","ClaimReward",tier,trackName) if accepted(ok,a) then claimedCount=claimedCount+1 end end end end end end if ready("achievements",60) then local ok,a=call("AchievementService","ClaimAll") if accepted(ok,a) then claimedCount=claimedCount+1 end end return claimedCount end local primaryByDamage={Physical="STR",Ranged="DEX",Magic="INT"} function Lobby.optimizeStatsForClass() if not O.Stats then return {Respec=false,Allocated=false} end local d=data(); if not d then return {Respec=false,Allocated=false} end local active=tostring(d.ActiveClass or "") local classDef local classes=getClassData() if active~="" and classes then if type(classes.Get)=="function" then local ok,v=pcall(classes.Get,active); if ok then classDef=v end end classDef=classDef or classes[active] end local damageType=classDef and classDef.DamageType or "Physical" local primary=primaryByDamage[damageType] or "STR" local points=d.SkillPoints or {} local wrong=0 for _,stat in ipairs({"STR","DEX","INT"}) do if stat~=primary then wrong=wrong+(tonumber(points[stat]) or 0) end end local didRespec=false local classChanged=active~="" and type(S.LastOptimizedClass)=="string" and S.LastOptimizedClass~="" and S.LastOptimizedClass~=active local blocked=false if O.RespecStats and active~="" and (wrong>0 or classChanged) then setStatus("Refunding stats for "..active) local ok,a,b=call("StatService","Respec") if accepted(ok,a) then didRespec=true local before=tonumber(d.UnspentSkillPoints) or 0 local deadline=os.clock()+2 repeat task.wait(0.08); d=data() until not d or (tonumber(d.UnspentSkillPoints) or 0)>before or os.clock()>=deadline else blocked=tostring(b or a or ""):find("NotEnoughGold",1,true)~=nil S.Errors.StatRespec=tostring(b or a or "Respec failed") end end d=data() local unspent=d and (tonumber(d.UnspentSkillPoints) or 0) or 0 local allocated=false if d and (unspent>0 or didRespec) then setStatus("Auto-allocating best stats for "..(active~="" and active or damageType)) local ok,a=call("StatService","AutoAllocate") if accepted(ok,a) then allocated=true; task.wait(0.3) end end S.LastOptimizedClass=active return {Respec=didRespec,Allocated=allocated,Primary=primary,Class=active,Blocked=blocked,ClassChanged=classChanged} end function Lobby.optimizeLobby() if S.LobbyPrepared then return true end if inRun() or placeRole=="AFK" then return false end if not O.LobbyOptimize then Lobby.equipBest(); Lobby.equipBestPotion() if O.Stats and data() and (tonumber(data().UnspentSkillPoints) or 0)>0 then call("StatService","AutoAllocate") end S.LobbyPrepared=true; S.LobbyPrepAt=os.clock(); return true end local d=data(); if not d then return false end local summary={Rewards=0,Loot=0,Equipped=0,Sold=0,Bought=0,Weapons=0,Forged=0,Expanded=0,Crafted=0,ClassUnlocks=0,Packs=0,Potions=0,Bank=0,Raid=0,Potion=false,Respec=false,Allocated=false,StatBlocked=false} -- Free/daily/update rewards come first because they can grant coins/materials that unlock a -- stronger crafting or shop decision during the same lobby visit. setStatus("Lobby prep: claiming all available rewards") summary.Rewards=summary.Rewards+Lobby.claimRewards(); task.wait(0.2) summary.Packs=summary.Packs+Lobby.openProgressionPackages() summary.Bank=summary.Bank+Lobby.withdrawBankUpgrades() setStatus("Lobby prep: collecting dungeon loot") summary.Loot=summary.Loot+Lobby.identifyLoot() summary.Equipped=summary.Equipped+Lobby.equipBest() summary.Potions=summary.Potions+Lobby.restockHealingPotions() summary.Potion=Lobby.equipBestPotion() or summary.Potion -- Fix the active class's stats before spending coins. If Respec needs coins, the reserve logic -- below deliberately protects that money instead of buying equipment first. local st=Lobby.optimizeStatsForClass() summary.Respec=st.Respec; summary.Allocated=st.Allocated; summary.StatBlocked=st.Blocked==true -- Protect equipped, loadout, future-level, unidentified, forged and best-per-slot gear before -- freeing inventory room for crafting/shop purchases. for i=1,5 do local sold=Lobby.sellJunk(true); summary.Sold=summary.Sold+sold; if sold<=0 then break end end -- Craft permanent equipment upgrades before shopping. If an actual Exotic upgrade is ready -- except for Celestial Ore, synthesize only the required ore chain first. Lobby.synthesizeForBestExotic() summary.Crafted=summary.Crafted+Lobby.craftGearUpgrades() summary.ClassUnlocks=summary.ClassUnlocks+Lobby.craftClassUnlocks() summary.Equipped=summary.Equipped+Lobby.equipBest() if summary.StatBlocked then setStatus("Saving coins for the active-class stat refund") else -- Re-fetch stock after each purchase and always buy the largest real GS jump first. for i=1,12 do if not Lobby.buyUpgrade() then break end summary.Bought=summary.Bought+1 summary.Equipped=summary.Equipped+Lobby.equipBest() end end summary.Equipped=summary.Equipped+Lobby.equipBest() if not summary.StatBlocked and Lobby.buyBetterWeapon() then summary.Weapons=summary.Weapons+1 end summary.Raid=summary.Raid+Lobby.buyRaidPower() -- Forge only the final equipped best set and only when the server reports 100% success. if not summary.StatBlocked then summary.Forged=Lobby.forgeGear() end summary.Equipped=summary.Equipped+Lobby.equipBest() -- Expansion stays after power upgrades so slots cannot consume money needed for actual strength. for i=1,4 do if Lobby.expandInventory() then summary.Expanded=summary.Expanded+1 else break end end for i=1,5 do local sold=Lobby.sellJunk(true); summary.Sold=summary.Sold+sold; if sold<=0 then break end end -- Final consolidation: late reward replication can create more loot/materials, so re-run the -- safe no-risk progression steps once before entering the next dungeon. summary.Rewards=summary.Rewards+Lobby.claimRewards(); task.wait(0.2) summary.Packs=summary.Packs+Lobby.openProgressionPackages() summary.Bank=summary.Bank+Lobby.withdrawBankUpgrades() summary.Loot=summary.Loot+Lobby.identifyLoot() Lobby.synthesizeForBestExotic() summary.Crafted=summary.Crafted+Lobby.craftGearUpgrades() summary.ClassUnlocks=summary.ClassUnlocks+Lobby.craftClassUnlocks() summary.Equipped=summary.Equipped+Lobby.equipBest() summary.Potions=summary.Potions+Lobby.restockHealingPotions() summary.Potion=Lobby.equipBestPotion() or summary.Potion for i=1,3 do local sold=Lobby.sellJunk(true); summary.Sold=summary.Sold+sold; if sold<=0 then break end end for i=1,2 do if Lobby.expandInventory() then summary.Expanded=summary.Expanded+1 else break end end summary.Equipped=summary.Equipped+Lobby.equipBest() d=data() S.LobbyPrepared=true; S.LobbyPrepAt=os.clock(); S.LobbyPrepSummary=summary local level=d and (tonumber(d.PlayerLevel) or 1) or 1 local cls=d and tostring(d.ActiveClass or "") or "" local gs=0 if d then for _,slot in ipairs({"Head","Body","Ring"}) do gs=gs+Lobby.officialGearScore((d.Equipment or {})[slot]) end end setStatus("Lobby ready — Lv."..level.." / GS "..math.floor(gs)..(cls~="" and " / "..cls or "")) return true end function Lobby.prepare() if S.Preparing then return false end S.Preparing=true local ok,result=pcall(function() if not inRun() and placeRole~="AFK" then return Lobby.optimizeLobby() end Lobby.equipBest(); Lobby.identifyLoot(); Lobby.equipBest(); Lobby.sellJunk(false) if O.Stats and data() and (tonumber(data().UnspentSkillPoints) or 0)>0 then call("StatService","AutoAllocate") end Lobby.claimRewards() return true end) if not ok then S.Errors.Prepare=tostring(result) end S.Preparing=false return ok and result~=false end local function aliveEnemy(m) if not m or not m.Parent or not m:IsA("Model") or m==LP.Character or Players:GetPlayerFromCharacter(m) then return false end if m:GetAttribute("Dead") or m:FindFirstAncestor("Dialogue_NPCS") then return false end local h=m:FindFirstChildOfClass("Humanoid") return part(m)~=nil and (not h or h.Health>0) end local function hittable(m) if not aliveEnemy(m) then return false end return not m:HasTag("Ignore_Damage") and not m:FindFirstChildOfClass("ForceField") end local function findRoomMarker(roomModel) if not roomModel or not roomModel.Parent then return nil,nil,0 end local zone=roomModel:FindFirstChild("Zone") if not (zone and zone:IsA("BasePart")) then local deep=roomModel:FindFirstChild("Zone",true) zone=(deep and deep:IsA("BasePart")) and deep or nil end local anchor=zone local quality=zone and 5 or 0 if not anchor then local direct=roomModel:FindFirstChild("TileAnchor") if direct and direct:IsA("BasePart") then anchor=direct; quality=4 end end if not anchor then local deep=roomModel:FindFirstChild("TileAnchor",true) if deep and deep:IsA("BasePart") then anchor=deep; quality=4 end end -- Different dungeon tiles do not all expose the same marker hierarchy. Prefer explicit -- room-center/entry markers before falling back to PrimaryPart/any BasePart so a valid -- generated room can still participate while its Zone is streaming in. if not anchor then for _,name in ipairs({"RoomCenter","Center","RoomSpawn","Spawn","Entrance","Entry","Trigger"}) do local p=roomModel:FindFirstChild(name,true) if p and p:IsA("BasePart") then anchor=p; quality=3; break end end end if not anchor and roomModel:IsA("Model") and roomModel.PrimaryPart then anchor=roomModel.PrimaryPart; quality=2 end if not anchor then local p=roomModel:FindFirstChildWhichIsA("BasePart",true) if p then anchor=p; quality=1 end end return zone,anchor,quality end local function collectGeneratedRooms(world) if not world or not world.Parent then return {} end local byIndex={} local function consider(v) local index=tonumber(v.Name:match("^Room_(%d+)$")) if not index then return end local zone,anchor,quality=findRoomMarker(v) if not anchor then return end local old=byIndex[index] if not old or quality>(old.Quality or 0) then byIndex[index]={Index=index,Model=v,Zone=zone,Anchor=anchor,Quality=quality} end end for _,v in ipairs(world:GetChildren()) do consider(v) end -- Some maps place Room_n models under a generated Rooms/Tiles container. The old direct-child -- scan silently produced S.Rooms={} on those maps, leaving the farm at "Waiting for dungeon rooms". for _,v in ipairs(world:GetDescendants()) do if v:IsA("Model") or v:IsA("Folder") then consider(v) end end local rooms={} for _,room in pairs(byIndex) do table.insert(rooms,room) end table.sort(rooms,function(a,b) return a.Index0 then S.Rooms=rooms S.LastRoomDiscoveryAt=os.clock() elseif #(S.Rooms or {})>0 then -- Prune only when every cached marker has truly disappeared. A one-frame nested streaming -- gap should not turn a working dungeon into an empty room list. local anyLive=false for _,room in ipairs(S.Rooms) do if room.Model and room.Model.Parent and room.Anchor and room.Anchor.Parent then anyLive=true; break end end if not anyLive then S.Rooms={} end end local prompts={} for _,v in ipairs(selected:GetDescendants()) do if v:IsA("ProximityPrompt") then prompts[v]=true end end local localChests=workspace:FindFirstChild("_LocalBossLootChests") if localChests then for _,v in ipairs(localChests:GetDescendants()) do if v:IsA("ProximityPrompt") then prompts[v]=true end end end S.Prompts=prompts local folder=selected:FindFirstChild("NPCs") if folder then for _,v in ipairs(folder:GetChildren()) do if v:IsA("Model") then S.Enemies[v]=true end end end -- Only tags physically owned by the selected generated dungeon are candidates. This keeps the -- v1.4.1 developer/test-NPC protection while allowing robust room bootstrap on every map. for _,tag in ipairs({"Enemy","NPC"}) do for _,v in ipairs(Collection:GetTagged(tag)) do if v:IsA("Model") and v:IsDescendantOf(selected) and not v:FindFirstAncestor("Dialogue_NPCS") then S.Enemies[v]=true end end end return #S.Rooms>0 end local function activeDungeonId() if S.DungeonId and S.DungeonId~="" then return S.DungeonId end local world=S.World if world and world.Parent then local name=tostring(world.Name or "") local id=name:match("^Generated_(.-)_[%da-fA-F]+$") or name:match("^Generated_(.+)$") if id and id~="" then S.DungeonId=id return id end end for m in pairs(S.Enemies or {}) do if m and m.Parent then local id=m:GetAttribute("DungeonRun") or m:GetAttribute("ChallengeDungeon") or m:GetAttribute("BossDungeon") if type(id)=="string" and id~="" then S.DungeonId=id return id end end end return nil end local function roomIndexLookup(index) index=tonumber(index) if not index then return nil end for _,room in ipairs(S.Rooms) do if tonumber(room.Index)==index then return room end end return nil end local function roomGeometry(room) if not room then return nil,nil,false end local zone=room.Zone if zone and zone.Parent and zone:IsA("BasePart") then return zone.CFrame,zone.Size,true end local model=room.Model if model and model.Parent and model:IsA("Model") then local ok,cf,size=pcall(function() local c,s=model:GetBoundingBox() return c,s end) if ok and typeof(cf)=="CFrame" and typeof(size)=="Vector3" and size.Magnitude>0.1 then return cf,size,false end end local anchor=room.Anchor if anchor and anchor.Parent and anchor:IsA("BasePart") then -- Last-resort envelope. Keep it deliberately modest so a random decorative fallback -- cannot claim half the dungeon as one room. return CFrame.new(anchor.Position),Vector3.new(22,18,22),false end return nil,nil,false end local function roomCenter(room) local cf=roomGeometry(room) if cf then return cf.Position end local a=room and room.Anchor return a and a.Parent and a.Position or nil end local function roomContainment(room,position,padXZ,padY) if not room or typeof(position)~="Vector3" then return false,nil,nil end local cf,size,hasZone=roomGeometry(room) if not cf or not size then return false,nil,nil end local lp=cf:PointToObjectSpace(position) local half=size*0.5 local px=tonumber(padXZ) or 0 local py=tonumber(padY) or 0 local inside=math.abs(lp.X)<=half.X+px and math.abs(lp.Z)<=half.Z+px and math.abs(lp.Y)<=half.Y+py if not inside then return false,lp,half end -- Bounding-box fallbacks include the room walls themselves and sometimes a connector stub. -- Prefer Zone geometry when available, but still make zone-less Catacombs rooms usable. local nx=math.abs(lp.X)/math.max(half.X,1) local nz=math.abs(lp.Z)/math.max(half.Z,1) local score=math.max(nx,nz)+(hasZone and 0 or 0.035) return true,lp,half,score end local function roomOutsideDistance(room,position) local cf,size=roomGeometry(room) if not cf or not size or typeof(position)~="Vector3" then return math.huge,math.huge,nil,nil end local lp=cf:PointToObjectSpace(position) local half=size*0.5 local dx=math.max(0,math.abs(lp.X)-half.X) local dz=math.max(0,math.abs(lp.Z)-half.Z) local dy=math.max(0,math.abs(lp.Y)-half.Y) return math.sqrt(dx*dx+dz*dz),dy,lp,half end local function layoutRebuild(layout,currentIndex) S.Layout=type(layout)=="table" and layout or {} S.LayoutByIndex={} S.LayoutOrder={} S.TreasureLayout={} S.BossLayoutIndex=nil S.LayoutCurrentIndex=tonumber(currentIndex) or tonumber(S.LayoutCurrentIndex) or 0 for _,entry in ipairs(S.Layout) do if type(entry)=="table" then local idx=tonumber(entry.Index) if idx then S.LayoutByIndex[idx]=entry table.insert(S.LayoutOrder,idx) if entry.IsBoss==true then S.BossLayoutIndex=idx end if entry.HasTreasure==true then S.TreasureLayout[idx]=true end end end end table.sort(S.LayoutOrder) end local function nextLayoutIndex(baseIndex) baseIndex=tonumber(baseIndex) or 0 for _,idx in ipairs(S.LayoutOrder or {}) do if tonumber(idx)>baseIndex then return tonumber(idx) end end return nil end local function roomAtPosition(position) if typeof(position)~="Vector3" then return nil end local containing={} local nearest,nearestScore=nil,math.huge for _,room in ipairs(S.Rooms) do local inside,_,_,depth=roomContainment(room,position,6,16) if inside then table.insert(containing,{Room=room,Score=depth or 0}) end local horizontal,vertical=roomOutsideDistance(room,position) local score=horizontal+vertical*1.5 if score0 then table.sort(containing,function(a,b) if math.abs(a.Score-b.Score)>0.001 then return a.Scoretonumber(b.Room.Index) end) return containing[1].Room end return nearest end local function currentRoom(r) if not r then return nil end local position=r.Position local containing={} local nearest,nearestScore=nil,math.huge for _,room in ipairs(S.Rooms) do local inside,_,_,depth=roomContainment(room,position,0.75,10) if inside then table.insert(containing,{Room=room,Score=depth or 0}) end local horizontal,vertical=roomOutsideDistance(room,position) local score=horizontal+vertical*1.5 if score0 then table.sort(containing,function(a,b) if math.abs(a.Score-b.Score)>0.001 then return a.Scoretonumber(b.Room.Index) end) return containing[1].Room end -- ZoneEntered remains authoritative when the character is in a connector gap. This also -- makes Catacombs robust because several combat Room_n models in the supplied capture have -- no Zone/TileAnchor even though NPCs correctly report those RoomIndex values. local authoritative=math.max(tonumber(S.Zone) or 0,tonumber(S.LastZoneEnteredIndex) or 0) if authoritative>0 then local room=roomIndexLookup(authoritative) if room then local center=roomCenter(room) if center and (center-position).Magnitude<=190 then return room end end end return nearest end local function enemyBelongsToActiveRun(m) if not m or not m.Parent or not m:IsA("Model") then return false end if not S.World or not S.World.Parent then return false end if not m:IsDescendantOf(S.World) then return false end -- Normal dungeon mobs/bosses are owned by Generated_*/NPCs. Keep this as the strongest -- provenance signal because it cannot be satisfied by the static developer/test NPCs that -- exist elsewhere in Workspace. local folder=S.World:FindFirstChild("NPCs") if folder and m:IsDescendantOf(folder) then return true end -- Some room variants may parent a wave model directly under Room_n. Accept those only while -- they are actually descendants of one of the generated room models and look combat-capable. for _,room in ipairs(S.Rooms) do if room.Model and room.Model.Parent and m:IsDescendantOf(room.Model) then if m:HasTag("Enemy") or m:HasTag("NPC") or m:FindFirstChildOfClass("Humanoid") then return true end end end -- Final generated-world fallback for tagged enemies that use a nonstandard generated folder. return m:HasTag("Enemy") or m:HasTag("NPC") end local function refreshEnemies() local now=os.clock() local function register(m) if not m or not m:IsA("Model") or m==LP.Character or Players:GetPlayerFromCharacter(m) then return end if m:FindFirstAncestor("Dialogue_NPCS") or m:GetAttribute("Dead") then return end -- Do not classify anything until discoverWorld has selected the active Generated_* model. -- Otherwise a legitimate run enemy that replicates before world discovery could be -- permanently mistaken for a global developer NPC. if not S.World or not S.World.Parent then return end if not enemyBelongsToActiveRun(m) then -- External/static NPCs are not temporarily invalid; they are permanently irrelevant -- to this generated run. This also prevents a tag refresh from reacquiring them later. S.InvalidEnemyUntil[m]=math.huge S.Enemies[m]=nil; S.EnemyRoomCache[m]=nil return end if os.clock()<(S.InvalidEnemyUntil[m] or 0) then return end local h=m:FindFirstChildOfClass("Humanoid") if h and h.Health<=0 then return end if not part(m) then return end S.Enemies[m]=true end -- Authoritative tags first. These are cheap enough to refresh every farming tick. for _,tag in ipairs({"Enemy","NPC"}) do for _,v in ipairs(Collection:GetTagged(tag)) do if v:IsA("Model") and v:IsDescendantOf(workspace) and not v:FindFirstAncestor("Dialogue_NPCS") then register(v) end end end -- The generated NPC container can receive children before CollectionService tags are -- replicated. Descendant scanning catches nested wave folders too, not just direct children. local folder=S.World and S.World:FindFirstChild("NPCs") if folder then for _,v in ipairs(folder:GetDescendants()) do if v:IsA("Model") and (v:FindFirstChildOfClass("Humanoid") or v:HasTag("Enemy") or v:HasTag("NPC")) then register(v) end end end -- Deep sweep every 0.45s, but only inside the selected Generated_* world. This keeps the -- missed-enemy safety net without scanning arbitrary Workspace humanoids such as developer -- test rigs, boss-rush display/spawn rigs, or other non-run NPCs. if now-(S.LastDeepEnemySweep or 0)>=0.45 then S.LastDeepEnemySweep=now if S.World and S.World.Parent then for _,v in ipairs(S.World:GetDescendants()) do if v:IsA("Humanoid") and v.Health>0 then local m=v.Parent if m and m:IsA("Model") then register(m) end end end end end -- Prune only genuinely dead/despawned models. Never drop a live model merely because it -- moved far away; knockback and late spawns behind us are exactly what the sweep must keep. for m in pairs(S.Enemies) do if not aliveEnemy(m) or not enemyBelongsToActiveRun(m) then local external=aliveEnemy(m) and not enemyBelongsToActiveRun(m) S.Enemies[m]=nil; S.EnemyRoomCache[m]=nil S.EnemyOutOfBoundsSince[m]=nil S.BossEngaged[m]=nil if S.ActiveBossLock==m then S.ActiveBossLock=nil end if external then S.InvalidEnemyUntil[m]=math.huge else S.InvalidEnemyUntil[m]=nil end if S.CombatLock==m then S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 end end end S.LastEnemySweep=now end local function enemyRoom(m) local p=part(m); if not p then return nil end -- Open-world enemies already carry the generator's physical RoomIndex. This is more reliable -- than spatial inference and is especially important in Catacombs, where several active combat -- rooms do not expose a Zone part in the client capture. local attrIndex=tonumber(m:GetAttribute("RoomIndex")) if attrIndex then local byAttribute=roomIndexLookup(attrIndex) if byAttribute then S.EnemyRoomCache[m]=byAttribute return byAttribute end end local cached=S.EnemyRoomCache[m] if cached and cached.Model and cached.Model.Parent then local room=roomAtPosition(p.Position) if room and room.Index==cached.Index then return cached end end local room=roomAtPosition(p.Position) if room then S.EnemyRoomCache[m]=room end return room end local function enemyInsideDungeonBounds(m) if not enemyBelongsToActiveRun(m) then return false,nil end local p=part(m); if not p then return false,nil end if #S.Rooms==0 then return true,nil end local attrIndex=tonumber(m:GetAttribute("RoomIndex")) if attrIndex then local room=roomIndexLookup(attrIndex) if room then local horizontal,vertical=roomOutsideDistance(room,p.Position) if horizontal<=38 and vertical<=26 then return true,room end -- A real RoomIndex remains strong ownership evidence even after knockback, but never -- allow a wildly displaced model to pull noclip flight into the void. if horizontal<=75 and vertical<=45 then return true,room end end end local nearest,nearestScore=nil,math.huge local valid,validScore=nil,math.huge for _,room in ipairs(S.Rooms) do local horizontal,vertical=roomOutsideDistance(room,p.Position) local score=horizontal+vertical*1.5 if score=0.65 then S.InvalidEnemyUntil[m]=os.clock()+0.75 S.Enemies[m]=nil; S.EnemyRoomCache[m]=nil end end if S.CombatLock==m then S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 end return nil end S.EnemyOutOfBoundsSince[m]=nil local eroom=boundsRoom or enemyRoom(m) -- Unknown-room enemies that are already cached are conservatively treated as current. -- It is safer to finish one extra live enemy than to start a room transition that can -- fight the combat goal on alternating ticks. local eindex=eroom and tonumber(eroom.Index) or tonumber(m:GetAttribute("RoomIndex")) or currentIndex local isBoss=m:GetAttribute("IsBoss")==true local isMiniBoss=m:GetAttribute("IsMiniBoss")==true local mainBoss=isBoss and not isMiniBoss local canHit=hittable(m) if mainBoss then local bossIndex=tonumber(S.BossLayoutIndex) local reachedBossRoom=(bossIndex and bossProgress>=bossIndex) or (last and bossProgress>=tonumber(last.Index)) local engaged=S.BossEngaged[m]==true or S.ActiveBossLock==m or S.CombatLock==m local dormant=m:GetAttribute("IsDormant")==true local bossReady=engaged or phaseBossReady or ((not dormant) and canHit) or ((not dormant) and reachedBossRoom) if not bossReady then return nil end -- Only the real dungeon boss receives the cross-room lifetime lock. Catacombs' Dark -- Revenant sets IsBoss=true too, but also IsMiniBoss=true; treating it as Verath made -- the farm abandon progression and chase a dormant miniboss across the map. S.BossEngaged[m]=true S.ActiveBossLock=m elseif isMiniBoss then -- Minibosses are ordinary room blockers. Never globally lock them before their room is -- actually reached. A missing RoomIndex is resolved by current physical bounds above. if eindex>currentIndex+1 then return nil end end local distance=(p.Position-r.Position).Magnitude return {Model=m, Part=p, Room=eroom, Index=eindex, Distance=distance, Hittable=canHit, Boss=mainBoss, MiniBoss=isMiniBoss} end for m in pairs(S.Enemies) do if not aliveEnemy(m) then S.Enemies[m]=nil; S.EnemyRoomCache[m]=nil if S.CombatLock==m then S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 end else local info=candidateInfo(m) if info then -- An engaged boss owns navigation regardless of which physical Room_n contains it. if info.Boss then blockerCount=blockerCount+1 table.insert(blockers,info) elseif info.Index<=currentIndex then blockerCount=blockerCount+1 table.insert(blockers,info) elseif info.Index==currentIndex+1 and (S.RoomTransitionIndex==info.Index or (O.FlyNavigation and O.Noclip and info.Distance<120)) then table.insert(forward,info) end end end end S.BlockingEnemyCount=blockerCount -- A live enemy in the current/previous room owns navigation exclusively. Never allow a -- room-travel goal to coexist with it. This is the key invariant that stops the left/right -- ping-pong when only one enemy remains. if blockerCount>0 then S.RoomClearQuietSince=0 S.NoEnemySince=0 -- Keep the existing combat lock if it is one of the blockers. Target stability matters -- more than shaving a few studs off travel distance. if S.CombatLock and aliveEnemy(S.CombatLock) then for _,info in ipairs(blockers) do if info.Model==S.CombatLock then S.CombatLockLostAt=0 S.CombatLockRoom=info.Index return info.Model end end end table.sort(blockers,function(a,b) -- Main boss first once engaged, then miniboss, then ordinary enemies. if a.Boss~=b.Boss then return a.Boss end if a.MiniBoss~=b.MiniBoss then return a.MiniBoss end if a.Hittable~=b.Hittable then return a.Hittable end if a.Index~=b.Index then return a.Index0 then table.sort(forward,function(a,b) if a.Hittable~=b.Hittable then return a.Hittable end return a.Distance0.001 then return a.Scoretonumber(b.Room.Index) end) return containing[1].Room end local function lootPromptRoom(p) local pos=promptPosition(p) if not pos then return nil,nil end -- Locked side rooms store their owning route room as ParentRoomIndex. Use it directly so -- Catacombs' Locked_1/Locked_2 rewards are eligible as soon as their parent combat room is -- reached, instead of depending on which neighbouring room happens to be spatially closest. local cur=p.Parent local depth=0 while cur and cur~=S.World and depth<12 do local parentIndex=tonumber(cur:GetAttribute("ParentRoomIndex")) if parentIndex then local room=roomIndexLookup(parentIndex) if room then return room,pos end end cur=cur.Parent depth=depth+1 end return lootStrictRoomAtPosition(pos) or roomAtPosition(pos),pos end local function pendingLootPrompt(r,includeAll) if not O.Loot or not r then return nil end refreshPromptCache() local physical=lootStrictRoomAtPosition(r.Position) local reached=math.max( physical and tonumber(physical.Index) or 0, tonumber(S.LastPhysicalRoom) or 0, 1 ) local best,bestKind,bestScore=nil,nil,math.huge for p in pairs(S.Prompts) do local kind=promptKind(p) if (kind=="chest" or kind=="book" or kind=="key") and promptAllowed(p,kind,false) then local room,pos=lootPromptRoom(p) if pos then local idx=room and tonumber(room.Index) or nil -- During normal progression only backtrack for loot that belongs to the physical -- route we have already reached (plus the immediate edge of that route for a -- Locked_n side room). After victory, collect every still-enabled physical reward. local eligible=includeAll==true or idx==nil or idx<=reached+1 if eligible then local d=(pos-r.Position).Magnitude local priority=(kind=="book" and -35) or (kind=="chest" and -28) or -16 local behindBonus=(idx and idx<=reached) and -45 or 0 local score=d+priority+behindBonus if scoreS.PotionStationBefore local promptConsumed=(not p.Parent) or p.Enabled==false if countConfirmed or promptConsumed then confirmPotionStation() return true end -- Verification is deliberately background-only. The player is free to move to the next -- objective immediately after the prompt fires; this just leaves time for a delayed event/data -- replication to mark the one-shot station as consumed. if (S.PotionStationVerifyUntil or 0)>0 and now>=S.PotionStationVerifyUntil then S.PotionStationPrompt=nil; S.PotionStationAwait=0; S.PotionStationBefore=nil; S.PotionStationVerifyUntil=0 end return false end local function interact() local _,_,r=character(); if not r then return false end local now=os.clock() pollPotionStation(now) refreshPromptCache() local best,kind,pos,dist if S.LockedPrompt then best=S.LockedPrompt; kind=S.LockedPromptKind or promptKind(best) if not promptAllowed(best,kind,true) then releasePrompt(best,best and (not best.Parent or not best.Enabled)) S.PotionStationPrompt=nil; S.PotionStationAwait=0; S.PotionStationBefore=nil; S.PotionStationVerifyUntil=0 return false end pos=promptPosition(best) if not pos then releasePrompt(best,false); return false end dist=(pos-r.Position).Magnitude if kind=="chest" or kind=="book" or kind=="key" then -- Once fired, give the server time to disable/collect the prompt before retrying. -- This prevents a sticky chest objective from spamming while still guaranteeing it -- cannot be stolen by the next-room enemy/transition. if not best.Parent or not best.Enabled then releasePrompt(best,true) S.InteractingUntil=0 return false end if (S.LockedPromptRetries or 0)>0 then local sinceFire=now-(S.LockedPromptFireAt or 0) if sinceFire<0.72 then return true end local maxRetries=(kind=="key" and 2 or 3) if (S.LockedPromptRetries or 0)>=maxRetries then S.PromptUntil[best]=now+(kind=="key" and 18 or 3.5) releasePrompt(best,false) S.InteractingUntil=0 return false end end end -- Potion confirmation is intentionally non-blocking. Once activation has been launched, -- release this objective so combat/loot/room travel can take over on the very next tick. -- PotionStationUsed, a count increase, or the prompt disabling will still confirm it in -- the background and permanently mark the station consumed for this run. if kind=="potion" and S.PotionStationAwait>0 then releasePrompt(best,false) S.InteractingUntil=0 return false end if now-(S.LockedPromptAt or now)>18 then S.PromptUntil[best]=now+20 releasePrompt(best,false) if kind=="potion" then S.PotionStationPrompt=nil; S.PotionStationAwait=0; S.PotionStationBefore=nil; S.PotionStationVerifyUntil=0 end return false end else best,pos,dist=nil,nil,math.huge local bestWeight=math.huge for p in pairs(S.Prompts) do local k=promptKind(p) if promptAllowed(p,k,false) then local v=promptPosition(p) if v then local d=(v-r.Position).Magnitude if d<130 then local priority=(k=="blessing" and -18) or (k=="potion" and -10) or (k=="book" and -14) or (k=="chest" and -12) or (k=="key" and -9) or 0 local weight=d+priority if weightactivation then goal(pos,false); return true end stopMotion() if kind=="potion" then if S.PotionStationAwait>0 then return true end S.PotionStationPrompt=best local _,before=potionCount(); S.PotionStationBefore=before S.PotionStationAwait=now S.PotionStationVerifyUntil=now+6 S.LockedPromptRetries=(S.LockedPromptRetries or 0)+1 -- Do not immediately select this same station again while its server confirmation catches up. S.PromptUntil[best]=now+8 elseif kind=="blessing" then S.ActiveBlessingPrompt=best S.PromptUntil[best]=now+8 elseif kind=="chest" or kind=="book" or kind=="key" then S.LockedPromptRetries=(S.LockedPromptRetries or 0)+1 S.LockedPromptFireAt=now -- The lock ignores this while active; it only prevents immediate reselection after a -- bounded failure/release. S.PromptUntil[best]=now+0.65 else S.PromptUntil[best]=now+math.max(4,(tonumber(best.HoldDuration) or 0)+2) end -- Native prompt holds still use their real duration. Executors with fireproximityprompt can -- activate a potion station instantly, so never inherit the prompt's HoldDuration as an -- artificial navigation delay in that case. if fireproximityprompt and (kind=="potion" or kind=="chest" or kind=="book" or kind=="key") then S.InteractingUntil=now+0.05 else S.InteractingUntil=now+(tonumber(best.HoldDuration) or 0)+0.7 end S.HeldPrompt=best job(function() if not active() or not best.Parent or not best.Enabled then return end if fireproximityprompt then -- No explicit hold time: the executor fires the prompt immediately instead of waiting -- out HoldDuration. Keep the old signature as a compatibility fallback only if needed. local fired=pcall(fireproximityprompt,best) if not fired then fired=pcall(fireproximityprompt,best,best.HoldDuration) end if kind=="chest" or kind=="book" or kind=="key" then -- Do not release the sticky objective yet; ChestCollected/BookCollected or the -- prompt disabling is the confirmation. Only free the artificial movement delay. S.InteractingUntil=0 if not fired then S.PromptUntil[best]=os.clock()+0.7 releasePrompt(best,false) end end if kind=="potion" then -- The activation was dispatched. Let the next objective take ownership right away; -- confirmation continues through PotionStationUsed/count/prompt-state in the background. if S.LockedPrompt==best then releasePrompt(best,false) end S.InteractingUntil=0 if not fired then S.PotionStationAwait=0; S.PotionStationVerifyUntil=0 S.PromptUntil[best]=os.clock()+0.6 end end else best:InputHoldBegin() local untilTime=os.clock()+(tonumber(best.HoldDuration) or 0)+0.15 repeat task.wait(0.05) until os.clock()>=untilTime or not active() or not best.Parent pcall(function() best:InputHoldEnd() end) if kind=="chest" or kind=="book" or kind=="key" then S.InteractingUntil=0 end if kind=="potion" then if S.LockedPrompt==best then releasePrompt(best,false) end S.InteractingUntil=0 end end if S.HeldPrompt==best then S.HeldPrompt=nil end end) return true end local function movementRayParams(c,extraExclude) local params=RaycastParams.new() params.FilterType=Enum.RaycastFilterType.Exclude local exclude={c} if extraExclude and extraExclude.Parent then table.insert(exclude,extraExclude) end params.FilterDescendantsInstances=exclude pcall(function() params.RespectCanCollide=true end) return params end local function blockingRay(origin,delta,c,extraExclude) if O.Noclip and active() and inRun() then return nil end if typeof(origin)~="Vector3" or typeof(delta)~="Vector3" or delta.Magnitude<0.05 then return nil end local hit=workspace:Raycast(origin,delta,movementRayParams(c,extraExclude)) if hit and hit.Instance and hit.Instance.CanCollide then return hit end return nil end local function positionClear(position,c,extraExclude) if typeof(position)~="Vector3" then return false end -- Noclip is authoritative. Do not let our own anti-wall probe recreate a wall. if O.Noclip and active() and inRun() then return true end local params=OverlapParams.new() params.FilterType=Enum.RaycastFilterType.Exclude local exclude={c} if extraExclude and extraExclude.Parent then table.insert(exclude,extraExclude) end params.FilterDescendantsInstances=exclude params.MaxParts=24 -- Probe the torso/head volume only. Keeping the bottom above the feet prevents the -- normal floor from being mistaken for a wall while still rejecting embedded walls. local cf=CFrame.new(position+Vector3.new(0,1.45,0)) local ok,parts=pcall(function() return workspace:GetPartBoundsInBox(cf,Vector3.new(3.2,4.1,3.2),params) end) if not ok then return true end for _,v in ipairs(parts) do if v.CanCollide and v.Transparency<1 then return false end end return true end local function requestAvoidancePath(r,target) if S.PathBusy or not r or not target then return end if S.PathGoal and (S.PathGoal-target).Magnitude<8 and S.Path and os.clock()-S.LastPath<1.25 then return end S.PathBusy=true; S.LastPath=os.clock(); S.PathGoal=target local session=S.Session job(function() local path=Pathfinding:CreatePath({AgentRadius=2.1,AgentHeight=5,AgentCanJump=true,WaypointSpacing=4}) local ok=pcall(function() path:ComputeAsync(r.Position,target) end) if active() and session==S.Session and ok and path.Status==Enum.PathStatus.Success then S.Path=path:GetWaypoints(); S.PathIndex=2 elseif active() and session==S.Session then S.Path=nil; S.PathIndex=nil end S.PathBusy=false end) end local function pathWaypoint(r) local waypoint=S.Path and S.Path[S.PathIndex or 2] while waypoint and (waypoint.Position-r.Position).Magnitude<3.5 do S.PathIndex=(S.PathIndex or 2)+1 waypoint=S.Path[S.PathIndex] end return waypoint end local function snapFloor(position,c) local hit=workspace:Raycast(position+Vector3.new(0,8,0),Vector3.new(0,-28,0),movementRayParams(c,nil)) if hit and hit.Instance and hit.Instance.CanCollide and hit.Normal.Y>0.45 then return Vector3.new(position.X,hit.Position.Y+3,position.Z) end return position end local function recoveryPosition(r,c) if S.LastSafePosition and (S.LastSafePosition-r.Position).Magnitude<45 and positionClear(S.LastSafePosition,c,S.Target) then return S.LastSafePosition end local base=r.Position for _,radius in ipairs({3,5,8,12}) do for i=0,7 do local angle=i*math.pi/4 local p=base+Vector3.new(math.cos(angle)*radius,0,math.sin(angle)*radius) if not (S.CombatMove and O.Position=="Above") then p=snapFloor(p,c) end if positionClear(p,c,S.Target) then return p end end end return nil end local function safeCombatPosition(m,desired) local c=LP.Character; local p=part(m) if not c or not p or not desired then return desired end if positionClear(desired,c,m) then return desired end local range=math.max(O.Distance,4) if O.Position=="Above" then -- Keep Above mode airborne but move sideways around ceilings/walls instead of -- forcing the root into geometry directly over the enemy. local y=desired.Y for _,radius in ipairs({2.5,4.5,7}) do for i=0,7 do local angle=i*math.pi/4 local q=Vector3.new(p.Position.X+math.cos(angle)*radius,y,p.Position.Z+math.sin(angle)*radius) if positionClear(q,c,m) then return q end end end else -- Behind/Orbit can put the desired point inside a wall when an enemy hugs one. -- Search the same combat radius around the enemy for the nearest clear angle. local candidates={} for i=0,11 do local angle=i*math.pi/6 local q=p.Position+Vector3.new(math.cos(angle)*range,0,math.sin(angle)*range) q=Vector3.new(q.X,desired.Y,q.Z) table.insert(candidates,q) end table.sort(candidates,function(a,b) return (a-desired).Magnitude<(b-desired).Magnitude end) for _,q in ipairs(candidates) do if positionClear(q,c,m) then return q end end end return desired end local function roomNavigationPoint(room,fromPosition) if not room or typeof(fromPosition)~="Vector3" then return nil end local c=LP.Character local zone=room.Zone if zone and zone:IsA("BasePart") then local localFrom=zone.CFrame:PointToObjectSpace(fromPosition) local half=zone.Size*0.5 local insetX=math.min(4,math.max(1,half.X*0.18)) local insetZ=math.min(4,math.max(1,half.Z*0.18)) local minX,maxX=-math.max(0.5,half.X-insetX),math.max(0.5,half.X-insetX) local minZ,maxZ=-math.max(0.5,half.Z-insetZ),math.max(0.5,half.Z-insetZ) local lx=math.clamp(localFrom.X,minX,maxX) local lz=math.clamp(localFrom.Z,minZ,maxZ) -- Aim for the closest point safely *inside* the next room instead of its -- centre/TileAnchor. This naturally approaches the connecting doorway and -- avoids pathfinding to decorative or non-walkable room centres. local edge=zone.CFrame:PointToWorldSpace(Vector3.new(lx,0,lz)) edge=Vector3.new(edge.X,fromPosition.Y,edge.Z) edge=snapFloor(edge,c) if positionClear(edge,c,nil) then return edge end local center=Vector3.new(zone.Position.X,fromPosition.Y,zone.Position.Z) for _,alpha in ipairs({0.18,0.35,0.55,0.75,1}) do local q=edge:Lerp(center,alpha) q=snapFloor(q,c) if positionClear(q,c,nil) then return q end end end local p=room.Anchor and room.Anchor.Position if p then local base=snapFloor(Vector3.new(p.X,fromPosition.Y,p.Z),c) if positionClear(base,c,nil) then return base end for _,radius in ipairs({3,5,8,12}) do for i=0,7 do local a=i*math.pi/4 local q=snapFloor(base+Vector3.new(math.cos(a)*radius,0,math.sin(a)*radius),c) if positionClear(q,c,nil) then return q end end end end return nil end local function roomFlightPoint(room,fromPosition) if not room or typeof(fromPosition)~="Vector3" then return nil end local cf,size=roomGeometry(room) if cf and size then local localFrom=cf:PointToObjectSpace(fromPosition) local half=size*0.5 local lx,lz=0,0 local nx=math.abs(localFrom.X)/math.max(half.X,1) local nz=math.abs(localFrom.Z)/math.max(half.Z,1) if nx>=nz then lx=-(localFrom.X>=0 and 1 or -1)*math.min(12,math.max(3,half.X*0.30)) else lz=-(localFrom.Z>=0 and 1 or -1)*math.min(12,math.max(3,half.Z*0.30)) end return cf:PointToWorldSpace(Vector3.new(lx,0,lz)) end local a=room.Anchor and room.Anchor.Position if a then return Vector3.new(a.X,a.Y+3,a.Z) end return nil end local function bossRoom() for _,room in ipairs(S.Rooms) do local model=room.Model if model and model.Parent then local spawns=model:FindFirstChild("Spawns") local bossSpawn=spawns and spawns:FindFirstChild("Boss_Spawn",true) if bossSpawn and bossSpawn:IsA("BasePart") then return room end end end for _,room in ipairs(S.Rooms) do local model=room.Model if model and model.Parent then local spawns=model:FindFirstChild("Spawns") local extract=spawns and spawns:FindFirstChild("Extract",true) if extract and extract:IsA("BasePart") then return room end end end -- RoomLayoutUpdate is the server's own ordered checkpoint map. Catacombs does not expose a -- Boss_Spawn in the supplied mid-run capture, so the physical last Room_n is not reliable. if S.BossLayoutIndex then local byLayout=roomIndexLookup(S.BossLayoutIndex) if byLayout then return byLayout end end -- Knights was observed using an already-generated physical boss arena even before layout -- synchronization. Preserve that fallback only for that map; do not send Catacombs into a -- wall-cap model merely because it has the largest Room_n number. if activeDungeonId()=="Knights" then return S.Rooms[#S.Rooms] end if not S.LayoutOrder or #S.LayoutOrder==0 then return S.Rooms[#S.Rooms] end return nil end local function roomSpawnPart(room,name) local model=room and room.Model local spawns=model and model:FindFirstChild("Spawns") local p=spawns and spawns:FindFirstChild(name,true) return p and p:IsA("BasePart") and p or nil end local function roomEntryPart(room) local model=room and room.Model local connectors=model and model:FindFirstChild("Connectors") local p=connectors and connectors:FindFirstChild("Entry") return p and p:IsA("BasePart") and p or nil end local function bossReentryPoints(room) if not room then return nil,nil,nil end local zone=room.Zone local entry=roomEntryPart(room) local playerSpawn=roomSpawnPart(room,"Player_Spawn") local bossSpawn=roomSpawnPart(room,"Boss_Spawn") if not zone or not zone:IsA("BasePart") then local inside=(playerSpawn and playerSpawn.Position) or (bossSpawn and bossSpawn.Position) or (room.Anchor and room.Anchor.Position) return nil,inside,inside end local half=zone.Size*0.5 local entryWorld=(entry and entry.Position) or zone.Position local lp=zone.CFrame:PointToObjectSpace(entryWorld) -- Find the actual side of the Zone used by Connectors.Entry. The outside point is only a few -- studs beyond that boundary, so the normal transition-aware void corridor still considers it -- legitimate. The inside point sits well across the boundary rather than at the room centre. local nx=math.abs(lp.X)/math.max(half.X,1) local nz=math.abs(lp.Z)/math.max(half.Z,1) local x,z=lp.X,lp.Z local ox,oz,ix,iz if nx>=nz then local sign=lp.X>=0 and 1 or -1 ox=sign*(half.X+5.5); oz=math.clamp(z,-math.max(1,half.Z-3),math.max(1,half.Z-3)) ix=sign*math.max(0.5,half.X-math.min(10,math.max(5,half.X*0.28))); iz=oz else local sign=lp.Z>=0 and 1 or -1 oz=sign*(half.Z+5.5); ox=math.clamp(x,-math.max(1,half.X-3),math.max(1,half.X-3)) iz=sign*math.max(0.5,half.Z-math.min(10,math.max(5,half.Z*0.28))); ix=ox end local y=math.clamp(lp.Y,-math.max(0.5,half.Y-3),math.max(0.5,half.Y-3)) local outside=zone.CFrame:PointToWorldSpace(Vector3.new(ox,y,oz)) local inside=zone.CFrame:PointToWorldSpace(Vector3.new(ix,y,iz)) local deep=(playerSpawn and playerSpawn.Position) or (bossSpawn and bossSpawn.Position) or zone.Position return outside,inside,deep end local function pulseBossRoomTouch(room,r) if not room or not r or not r.Parent then return end local now=os.clock() if now-(S.BossTouchAt or 0)<0.45 then return end S.BossTouchAt=now local zone=room.Zone local entry=roomEntryPart(room) local playerSpawn=roomSpawnPart(room,"Player_Spawn") -- Knights' generated boss Zone and Entry are CanTouch=true. Noclip/CFrame travel can cross -- them without producing the same touch transition as ordinary character movement on some -- executors. firetouchinterest, when available, replays that native touch without inventing a -- server remote. The physical outside->inside pass below remains the fallback. local fireTouch=rawget((getgenv and getgenv()) or _G,"firetouchinterest") or firetouchinterest if type(fireTouch)~="function" then return end task.spawn(function() for _,touchPart in ipairs({entry,zone,playerSpawn}) do if touchPart and touchPart.Parent and r and r.Parent then pcall(function() r.CanTouch=true fireTouch(r,touchPart,0) end) task.wait(0.03) pcall(function() fireTouch(r,touchPart,1) end) end end end) end local function bossActivationPoint(room,stage) if not room then return nil end stage=math.max(0,math.floor(tonumber(stage) or 0)) -- Generated boss rooms expose the player's intended boss-warp spawn and the boss spawn. -- Use those before arbitrary Zone quadrants, especially for Knights' large castle arena. if stage==0 then local playerSpawn=roomSpawnPart(room,"Player_Spawn") if playerSpawn then return playerSpawn.Position end elseif stage==1 then local bossSpawn=roomSpawnPart(room,"Boss_Spawn") if bossSpawn then local zone=room.Zone if zone and zone:IsA("BasePart") then return bossSpawn.Position:Lerp(zone.Position,0.18) end return bossSpawn.Position end end local zone=room.Zone if zone and zone:IsA("BasePart") then local half=zone.Size*0.5 -- First go straight through the arena centre. If the server's boss trigger is offset -- from the Zone centre, cycle a compact set of deep interior points. Noclip means these -- points can safely cross decorative walls without stopping outside the arena. local points={ Vector3.new(0,0,0), Vector3.new(math.max(3,half.X*0.42),0,0), Vector3.new(-math.max(3,half.X*0.42),0,0), Vector3.new(0,0,math.max(3,half.Z*0.42)), Vector3.new(0,0,-math.max(3,half.Z*0.42)), } local localPoint=points[(stage % #points)+1] localPoint=Vector3.new( math.clamp(localPoint.X,-math.max(1,half.X-3),math.max(1,half.X-3)), 0, math.clamp(localPoint.Z,-math.max(1,half.Z-3),math.max(1,half.Z-3)) ) return zone.CFrame:PointToWorldSpace(localPoint) end local anchor=room.Anchor and room.Anchor.Position if anchor then return Vector3.new(anchor.X,anchor.Y+3,anchor.Z) end return nil end local function clampRoomVertical(room,position,margin) if not room or typeof(position)~="Vector3" then return position end local cf,size=roomGeometry(room) if not cf or not size then return position end local lp=cf:PointToObjectSpace(position) local half=size*0.5 local m=math.max(1,tonumber(margin) or 2.5) local limit=math.max(0.5,half.Y-m) local y=math.clamp(lp.Y,-limit,limit) return cf:PointToWorldSpace(Vector3.new(lp.X,y,lp.Z)) end local function clampRoomInterior(room,position,marginXZ,marginY) if not room or typeof(position)~="Vector3" then return position end local cf,size=roomGeometry(room) if not cf or not size then return position end local lp=cf:PointToObjectSpace(position) local half=size*0.5 local mx=math.max(1,tonumber(marginXZ) or 2.5) local my=math.max(1,tonumber(marginY) or 2.5) local x=math.clamp(lp.X,-math.max(0.5,half.X-mx),math.max(0.5,half.X-mx)) local y=math.clamp(lp.Y,-math.max(0.5,half.Y-my),math.max(0.5,half.Y-my)) local z=math.clamp(lp.Z,-math.max(0.5,half.Z-mx),math.max(0.5,half.Z-mx)) return cf:PointToWorldSpace(Vector3.new(x,y,z)) end local function projectedFlightRoom(position) if typeof(position)~="Vector3" then return nil end local best,bestScore=nil,math.huge for _,room in ipairs(S.Rooms) do local cf,size=roomGeometry(room) if cf and size then local lp=cf:PointToObjectSpace(position) local half=size*0.5 if math.abs(lp.X)<=half.X+5 and math.abs(lp.Z)<=half.Z+5 then local score=math.max(math.abs(lp.X)/math.max(half.X,1),math.abs(lp.Z)/math.max(half.Z,1)) if score=tonumber(S.RoomTransitionIndex) then sourceIndex=math.max(1,tonumber(S.RoomTransitionIndex)-1) end local source=roomByIndex(sourceIndex) local scf,ssize=roomGeometry(source) local a=scf and scf.Position or nil local b=S.RoomTransitionPoint if typeof(b)~="Vector3" then b=dcf.Position end if not a then local anchor=source and source.Anchor and source.Anchor.Position a=anchor or b end local ax,az=a.X,a.Z local bx,bz=b.X,b.Z local px,pz=position.X,position.Z local dx,dz=bx-ax,bz-az local len2=dx*dx+dz*dz local t=0 if len2>0.01 then t=math.clamp(((px-ax)*dx+(pz-az)*dz)/len2,0,1) end local cx,cz=ax+dx*t,az+dz*t local horizontal=math.sqrt((px-cx)^2+(pz-cz)^2) local sourceHalf=ssize and ssize.Y*0.5 or dsize.Y*0.5 local destHalf=dsize.Y*0.5 local sourceY=scf and scf.Position.Y or b.Y local destY=dcf.Position.Y local minY=math.min(sourceY-sourceHalf,destY-destHalf)-10 local maxY=math.max(sourceY+sourceHalf,destY+destHalf)+10 local verticalOutside=position.YmaxY and (position.Y-maxY) or 0) local safe=horizontal<=52 and verticalOutside<=14 local corridor=Vector3.new(cx,math.clamp(position.Y,minY+3,maxY-3),cz) return safe,corridor,destination,math.max(horizontal,verticalOutside) end local function combatFlightEnvelope(position) if typeof(position)~="Vector3" or not S.CombatMove or not S.Target then return false,nil,nil,math.huge end local targetPart=part(S.Target) if not targetPart then return false,nil,nil,math.huge end -- Only grant a combat corridor to a target that is still a real member of the active -- generated run and is physically inside/near one of its rooms. This cannot legitimize -- developer/test NPCs or a target that has actually fallen into the void. local inBounds,targetRoom=enemyInsideDungeonBounds(S.Target) if not inBounds or not targetRoom then return false,nil,nil,math.huge end local source=roomByIndex(S.LastConfirmedRoom) if not source then local nearest=nearestFlightRoom(position) source=nearest end local a if source and source.Zone and source.Zone:IsA("BasePart") then a=source.Zone.Position elseif source and source.Anchor then a=source.Anchor.Position else a=position end -- End the corridor at the enemy's *current* physical position, not the room center. -- This is important when a boss has been lured several rooms away from its spawn room. local b=targetPart.Position local ax,az=a.X,a.Z local bx,bz=b.X,b.Z local px,pz=position.X,position.Z local dx,dz=bx-ax,bz-az local len2=dx*dx+dz*dz local t=0 if len2>0.01 then t=math.clamp(((px-ax)*dx+(pz-az)*dz)/len2,0,1) end local cx,cz=ax+dx*t,az+dz*t local horizontal=math.sqrt((px-cx)^2+(pz-cz)^2) local sourceHalf=(source and source.Zone and source.Zone:IsA("BasePart")) and source.Zone.Size.Y*0.5 or 18 local destHalf=(targetRoom.Zone and targetRoom.Zone:IsA("BasePart")) and targetRoom.Zone.Size.Y*0.5 or 18 local sourceY=(source and source.Zone and source.Zone:IsA("BasePart")) and source.Zone.Position.Y or a.Y local destY=(targetRoom.Zone and targetRoom.Zone:IsA("BasePart")) and targetRoom.Zone.Position.Y or b.Y local minY=math.min(sourceY-sourceHalf,destY-destHalf,b.Y-12)-12 local maxY=math.max(sourceY+sourceHalf,destY+destHalf,b.Y+12)+12 local verticalOutside=position.YmaxY and (position.Y-maxY) or 0) -- Noclip combat deliberately crosses the empty space between room trigger volumes. -- Keep this corridor a little wider than normal room travel because a moving boss can -- change the endpoint while we are already in flight. local safe=horizontal<=58 and verticalOutside<=14 local corridor=Vector3.new(cx,math.clamp(position.Y,minY+3,maxY-3),cz) return safe,corridor,targetRoom,math.max(horizontal,verticalOutside) end local function recoverEscapedFlight(r) if not r or #S.Rooms==0 then return false end local now=os.clock() if now-(S.CharacterSpawnAt or 0)<0.8 then return false end local strict=strictContainingRoom(r.Position) if strict then S.LastConfirmedRoom=tonumber(strict.Index) or S.LastConfirmedRoom S.LastConfirmedRoomAt=now S.FlightOutsideSince=0 return false end -- A committed room transition is allowed to pass through the gap/corridor between Zone -- parts. v1.3.7 treated that gap as a void escape and teleported the player back to the -- previous room, producing the repeated "Recovering from void flight" flash. if S.RoomTransitionIndex and not S.CombatMove then local transitionSafe,corridor,destination,severity=transitionFlightEnvelope(r.Position) if transitionSafe then S.FlightOutsideSince=0 return false end S.FlightOutsideSince=(S.FlightOutsideSince and S.FlightOutsideSince>0) and S.FlightOutsideSince or now local extreme=severity>120 if not extreme and now-S.FlightOutsideSince<0.40 then return false end if now-(S.FlightRecoveryAt or 0)<0.35 then return true end -- If transition flight really drifts away, correct it to the nearest point on the -- forward corridor and KEEP the transition lock/goal. Never teleport back to the old room. if corridor and destination then local target=S.RoomTransitionPoint if typeof(target)~="Vector3" then target=roomFlightPoint(destination,corridor) end if typeof(target)~="Vector3" then target=destination.Zone.Position end local face=target-corridor if face.Magnitude<0.05 then face=r.CFrame.LookVector end if face.Magnitude<0.05 then face=Vector3.new(0,0,-1) end r.CFrame=CFrame.lookAt(corridor,corridor+face.Unit) r.AssemblyLinearVelocity=Vector3.zero; r.AssemblyAngularVelocity=Vector3.zero S.FlightRecoveryAt=now; S.FlightOutsideSince=0 S.LastSafePosition=corridor; S.LastSafeAt=now S.Path=nil; S.PathIndex=nil; S.PathGoal=nil S.FlyCruiseY=nil; S.FlyGoal=nil; S.FlyCombat=nil S.Goal=target; S.CombatMove=false setStatus("Correcting room flight") return true end end -- Combat has its own legitimate cross-room corridor. A lured boss/enemy can be -- physically several rooms behind the server's current/final room index. The generic void -- recovery used that authoritative index and could pull the player away from the real target -- every time flight crossed the gap between room Zones. if S.CombatMove and S.Target then local combatSafe,corridor,targetRoom,severity=combatFlightEnvelope(r.Position) if combatSafe then S.FlightOutsideSince=0 return false end local targetValid=enemyInsideDungeonBounds(S.Target) if targetValid and corridor and targetRoom then S.FlightOutsideSince=(S.FlightOutsideSince and S.FlightOutsideSince>0) and S.FlightOutsideSince or now local extreme=severity>145 if not extreme and now-S.FlightOutsideSince<0.42 then return false end if now-(S.FlightRecoveryAt or 0)<0.35 then return true end -- Correct sideways/vertical drift back onto the route TO THE ENEMY. Keep combat -- ownership and the current combat goal; never send a lured boss chase back to the -- final/authoritative room. local enemyPart=part(S.Target) local face=(enemyPart and (enemyPart.Position-corridor)) or (S.Goal and (S.Goal-corridor)) or r.CFrame.LookVector if typeof(face)~="Vector3" or face.Magnitude<0.05 then face=Vector3.new(0,0,-1) end r.CFrame=CFrame.lookAt(corridor,corridor+face.Unit) r.AssemblyLinearVelocity=Vector3.zero r.AssemblyAngularVelocity=Vector3.zero S.FlightRecoveryAt=now S.FlightOutsideSince=0 S.LastSafePosition=corridor S.LastSafeAt=now S.Path=nil; S.PathIndex=nil; S.PathGoal=nil S.FlyCruiseY=nil; S.FlyGoal=nil setStatus("Correcting boss chase") return true end end local nearest,horizontal,lp=nearestFlightRoom(r.Position) if not nearest or not nearest.Zone or not nearest.Zone:IsA("BasePart") then return false end local half=nearest.Zone.Size*0.5 local verticalOutside=lp and math.max(0,math.abs(lp.Y)-half.Y) or 0 -- Small gaps and one-frame replication jumps are not void escapes. Require a sustained -- out-of-bounds state unless the character is extremely far outside the dungeon. if horizontal<=55 and verticalOutside<=8 then S.FlightOutsideSince=0; return false end S.FlightOutsideSince=(S.FlightOutsideSince and S.FlightOutsideSince>0) and S.FlightOutsideSince or now local extreme=horizontal>150 or verticalOutside>70 if not extreme and now-S.FlightOutsideSince<0.35 then return false end if now-(S.FlightRecoveryAt or 0)<0.45 then return true end local authoritative=math.max(tonumber(S.LastZoneEnteredIndex) or 0,tonumber(S.Zone) or 0,tonumber(S.LastConfirmedRoom) or 0,1) local recoveryRoom=roomByIndex(authoritative) or nearest local safe=roomRecoveryPoint(recoveryRoom,r.Position) if not safe then return false end local face=(S.Goal and (S.Goal-safe)) or r.CFrame.LookVector if typeof(face)~="Vector3" or face.Magnitude<0.05 then face=Vector3.new(0,0,-1) end r.CFrame=CFrame.lookAt(safe,safe+face.Unit) r.AssemblyLinearVelocity=Vector3.zero; r.AssemblyAngularVelocity=Vector3.zero S.FlightRecoveryAt=now; S.FlightOutsideSince=0 S.LastSafePosition=safe; S.LastSafeAt=now S.Goal=nil; S.Path=nil; S.PathIndex=nil; S.PathGoal=nil S.FlyCruiseY=nil; S.FlyGoal=nil; S.FlyCombat=nil S.RoomTransitionIndex=nil; S.RoomTransitionAt=0; S.RoomTransitionPoint=nil -- If combat pulled us into the void, do not immediately reacquire the same bad target. if S.Target then local ok=enemyInsideDungeonBounds(S.Target) if not ok then S.InvalidEnemyUntil[S.Target]=now+3 S.Enemies[S.Target]=nil; S.EnemyRoomCache[S.Target]=nil S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 end end S.Target=nil; S.CombatMove=false setStatus("Recovering from void flight") return true end -- Predictive defense --------------------------------------------------------- -- Dungeon Lootr's native Dodge/Dash grants the actual server-side iFrame and -- protection state. We predict only attacks that intersect the player and then -- ask the game to perform its own dodge rather than manufacturing immunity. local function flatUnit(v) if typeof(v)~="Vector3" then return nil end local f=Vector3.new(v.X,0,v.Z) if f.Magnitude<0.01 then return nil end return f.Unit end local function bossProfile(m) local bd=S.Modules["Boss_ActionData"] if type(bd)~="table" or not m then return nil end if type(bd.Resolve)=="function" then local ok,v=pcall(bd.Resolve,m.Name) if ok and type(v)=="table" then return v end end return type(bd.Index)=="table" and bd.Index[m.Name] or nil end local function npcProfile(m) local nd=S.Modules["NPC_ActionData"] if type(nd)~="table" or not m then return nil end if type(nd.Resolve)=="function" then local ok,v=pcall(nd.Resolve,m.Name) if ok and type(v)=="table" then return v end end return type(nd.Index)=="table" and nd.Index[m.Name] or nil end local function abilityTrackMap(m,profile) if not m or type(profile)~="table" then return {} end local key=tostring(m.Name).."|"..tostring(profile.ClassSource or "") if S.BossAbilityCache[key] then return S.BossAbilityCache[key] end local map={} local info=RS:FindFirstChild("GameInfo") local root=info and info:FindFirstChild("Boss_Abilities") local folder=root and profile.ClassSource and root:FindFirstChild(profile.ClassSource) for _,a in ipairs(type(profile.Abilities)=="table" and profile.Abilities or {}) do local anim=a.AnimationName or a.AnimName local moduleData=nil if folder and a.ModuleName then local mod=folder:FindFirstChild(a.ModuleName) if mod and mod:IsA("ModuleScript") then local ok,v=pcall(require,mod) if ok and type(v)=="table" then moduleData=v anim=anim or v.AnimationName or v.AnimName end end end if type(anim)=="string" and anim~="" then -- Boss_ActionData describes telegraph timing while the ability module often owns the -- exact runtime hitbox. Merge them so prediction uses the most specific values present. local merged={} if type(moduleData)=="table" then for k,v in pairs(moduleData) do merged[k]=v end end for k,v in pairs(a) do merged[k]=v end map[anim]=merged end end S.BossAbilityCache[key]=map return map end local function horizontalSegmentDistance(point,a,b) local px,pz=point.X,point.Z; local ax,az=a.X,a.Z; local bx,bz=b.X,b.Z local dx,dz=bx-ax,bz-az; local len2=dx*dx+dz*dz local t=len2>0.001 and math.clamp(((px-ax)*dx+(pz-az)*dz)/len2,0,1) or 0 local x,z=ax+dx*t,az+dz*t return math.sqrt((px-x)^2+(pz-z)^2),t,Vector3.new(x,point.Y,z) end local function orientedThreat(m,playerPos,size,range,margin) local p=part(m); if not p then return false end size=typeof(size)=="Vector3" and size or Vector3.new(14,10,16) range=tonumber(range) or math.max(12,size.Z) margin=tonumber(margin) or 2.5 local lp=p.CFrame:PointToObjectSpace(playerPos) local forward=-lp.Z return math.abs(lp.X)<=size.X*0.5+margin and math.abs(lp.Y)<=size.Y*0.5+5 and forward>=-3-margin and forward<=range+size.Z*0.5+margin end local function aoeThreat(origin,playerPos,radius,height,margin) local horizontal=Vector3.new(playerPos.X-origin.X,0,playerPos.Z-origin.Z).Magnitude return horizontal<=(tonumber(radius) or 20)+(tonumber(margin) or 3) and math.abs(playerPos.Y-origin.Y)<=(tonumber(height) or 16)*0.5+8 end local function dodgeRoom(r) if not r then return nil end local strict=strictContainingRoom(r.Position) if strict then return strict end -- During a committed transition, never choose the room behind us as the dodge envelope. -- A side-step should preserve forward progress toward the already-owned destination. if S.RoomTransitionIndex and not S.CombatMove then local destination=roomByIndex(S.RoomTransitionIndex) if destination then return destination end end return roomAtPosition(r.Position) or roomByIndex(math.max(tonumber(S.LastZoneEnteredIndex) or 0,tonumber(S.Zone) or 0,1)) end local function safeDodgeDirection(r,preferred,source,velocity) local pref=flatUnit(preferred) local away=source and flatUnit(r.Position-source) or nil local vel=flatUnit(velocity) local candidates={} local function add(v) v=flatUnit(v) if not v then return end for _,old in ipairs(candidates) do if old:Dot(v)>0.985 then return end end table.insert(candidates,v) end add(pref); add(away) if vel then add(Vector3.new(-vel.Z,0,vel.X)); add(Vector3.new(vel.Z,0,-vel.X)) end if away then add(Vector3.new(-away.Z,0,away.X)); add(Vector3.new(away.Z,0,-away.X)) end local look=flatUnit(r.CFrame.LookVector) or Vector3.new(0,0,-1) local right=Vector3.new(-look.Z,0,look.X) add(right); add(-right); add(look); add(-look) for i=0,7 do local a=i*math.pi/4; add(Vector3.new(math.cos(a),0,math.sin(a))) end local room=dodgeRoom(r) local best,bestScore=nil,-math.huge for _,dir in ipairs(candidates) do local endpoint=r.Position+dir*15 local clamped=room and clampRoomInterior(room,endpoint,3.5,2.5) or endpoint local actual=flatUnit(clamped-r.Position) local travel=Vector3.new(clamped.X-r.Position.X,0,clamped.Z-r.Position.Z).Magnitude if actual and travel>=5 then local score=travel*0.35 if pref then score=score+actual:Dot(pref)*7 end if away then score=score+actual:Dot(away)*5 end if source then score=score+Vector3.new(clamped.X-source.X,0,clamped.Z-source.Z).Magnitude*0.12 end if vel then score=score-math.abs(actual:Dot(vel))*2 end if score>bestScore then best,bestScore=actual,score end end end return best or pref or away or right end local function canAutoDodge() if not O.AutoDodge or not active() or not inRun() or S.Complete or S.PendingBuff or S.PendingChest then return false end local c,h,r=character(); if not c then return false end local now=os.clock() if now0.001 then state.LastChange=now end state.LastValue=value if now-(state.LastChange or 0)<0.18 then active=true end end for _,v in ipairs(root:GetDescendants()) do if v:IsA("Beam") and v.Enabled then active=true local a0,a1=v.Attachment0,v.Attachment1 if a0 and a1 then local d=horizontalSegmentDistance(r.Position,a0.WorldPosition,a1.WorldPosition) local width=math.max(tonumber(v.Width0) or 0,tonumber(v.Width1) or 0,4) if d<=width*0.5+5 then lineDanger=true lineDir=a1.WorldPosition-a0.WorldPosition end end elseif v:IsA("BasePart") and v~=root and v.Transparency<0.94 then active=true end end if active and not state.WasActive then state.ActiveSince=now end if not active then state.ActiveSince=0 end state.WasActive=active if not active then return false end local elapsed=now-(state.ActiveSince or now) local danger=lineDanger local dangerDuration=math.huge local source=part(m) and part(m).Position or nil for _,a in ipairs(type(profile.Abilities)=="table" and profile.Abilities or {}) do local duration=tonumber(a.TelegraphDuration) or 0.8 local typ=tostring(a.TelegraphType or "") if typ=="AoE" or typ=="BigAoE" then local radius=tonumber(a.TelegraphRadius) or ((typeof(a.HitboxSize)=="Vector3" and math.max(a.HitboxSize.X,a.HitboxSize.Z)*0.5) or 22) if source and aoeThreat(source,r.Position,radius,typeof(a.HitboxSize)=="Vector3" and a.HitboxSize.Y or 18,2.5) then danger=true -- When a line Beam is visibly active, prefer the line timing instead of mixing -- in an unrelated longer/shorter AoE from the same boss profile. if not lineDanger then dangerDuration=math.min(dangerDuration,duration) end end elseif typ=="Line" then local size=a.HitboxSize or profile.DefaultHitboxSize local range=a.HitboxRange or a.TelegraphRange or profile.DefaultRange if lineDanger or orientedThreat(m,r.Position,size,range,3.5) then danger=true; dangerDuration=math.min(dangerDuration,duration) end end end if dangerDuration==math.huge then dangerDuration=0.8 end -- Time the real Dash near impact instead of burning its cooldown at the beginning of a -- long warning. Native protection lasts 0.7s and the strongest iFrame window is 0.35s. if danger and elapsed>=math.max(0.08,dangerDuration*0.52) then local away=source and (r.Position-source) or -r.CFrame.LookVector if lineDir then away=Vector3.new(-lineDir.Z,0,lineDir.X) end return performDodge(away,"telegraphed ability",source,lineDir,m) end return false end local function attackAnimationThreat(m,r,now) if not O.DodgeBasics and not O.DodgeTelegraphs then return false end local mp=part(m); if not mp then return false end local h=m:FindFirstChildOfClass("Humanoid") local animator=h and h:FindFirstChildOfClass("Animator") if not animator then return false end local bp=bossProfile(m) local np=not bp and npcProfile(m) or nil local abilityMap=bp and abilityTrackMap(m,bp) or nil local ok,tracks=pcall(function() return animator:GetPlayingAnimationTracks() end) if not ok or type(tracks)~="table" then return false end for _,track in ipairs(tracks) do local name=tostring(track.Name or "") local pos=tonumber(track.TimePosition) or 0 local state=S.DodgeTrackState[track] or {LastPos=pos,Started=now} if pos+0.03<(state.LastPos or 0) then state.Started=now end state.LastPos=pos; S.DodgeTrackState[track]=state local ability=abilityMap and abilityMap[name] if ability and O.DodgeTelegraphs then local source=mp.Position local typ=tostring(ability.TelegraphType or "") local dangerous=false if typ=="AoE" or typ=="BigAoE" then local radius=tonumber(ability.TelegraphRadius) or ((typeof(ability.HitboxSize)=="Vector3" and math.max(ability.HitboxSize.X,ability.HitboxSize.Z)*0.5) or 22) dangerous=source and aoeThreat(source,r.Position,radius,typeof(ability.HitboxSize)=="Vector3" and ability.HitboxSize.Y or 18,2) else dangerous=orientedThreat(m,r.Position,ability.HitboxSize or bp.DefaultHitboxSize,ability.HitboxRange or ability.TelegraphRange or bp.DefaultRange,3) end -- Ability tracks normally begin after their telegraph. React immediately once the -- damaging animation starts, but only if its geometry can actually reach us. if dangerous and pos>=0.03 then local sourcePos=source or mp.Position local side=flatUnit((r.Position-sourcePos):Cross(Vector3.new(0,1,0))) return performDodge(side or (r.Position-sourcePos),"boss ability",sourcePos,nil,track) end elseif O.DodgeBasics and name:match("^Attack_") then local profile=bp or np or {} local basic=type(profile.BasicString)=="table" and profile.BasicString or profile local size=basic.HitboxSize or profile.DefaultHitboxSize or Vector3.new(14,10,16) local range=basic.HitboxRange or profile.DefaultRange or 16 local wind=tonumber(profile.WindUpDuration) or 0.4 if orientedThreat(m,r.Position,size,range,2.75) and pos>=math.max(0.07,wind*0.38) and pos<=wind+0.55 then local source=mp.Position local away=r.Position-source return performDodge(away,"melee attack",source,nil,track) end end end return false end local function projectileThreat(r,now) if not O.DodgeProjectiles then return false end local folder=workspace:FindFirstChild("ActiveProjectiles") if not folder then return false end local seen={}; local checked=0 for _,obj in ipairs(folder:GetChildren()) do if checked>=80 then break end checked=checked+1 local p=part(obj) if p then seen[obj]=true local owner=obj:GetAttribute("OwnerUserId") or p:GetAttribute("OwnerUserId") local ownerPlayer=tonumber(owner) and Players:GetPlayerByUserId(tonumber(owner)) or nil -- Any player-owned projectile is friendly for dungeon PvE. Enemy projectiles usually -- have no OwnerUserId, so they remain eligible for trajectory prediction. if not ownerPlayer then local state=S.ProjectileState[obj] local pos=p.Position; local velocity=p.AssemblyLinearVelocity if state and now-(state.At or now)>0.01 and now-(state.At or now)<0.3 then local derived=(pos-state.Position)/math.max(0.01,now-state.At) if derived.Magnitude>velocity.Magnitude+4 then velocity=derived end end S.ProjectileState[obj]={Position=pos,At=now} local speed=velocity.Magnitude if speed>3 then local rel=pos-r.Position local vv=velocity:Dot(velocity) local t=vv>0.01 and math.clamp(-rel:Dot(velocity)/vv,0,0.65) or 0 local closest=rel+velocity*t local miss=closest.Magnitude local approaching=rel:Dot(velocity)<0 if approaching and t<=0.65 and (miss<=8.5 or rel.Magnitude<=10) then local vflat=flatUnit(velocity) local side=vflat and Vector3.new(-vflat.Z,0,vflat.X) or (r.Position-pos) return performDodge(side,"incoming projectile",pos,velocity,obj) end elseif (pos-r.Position).Magnitude<=7 then return performDodge(r.Position-pos,"projectile",pos,nil,obj) end end end end for obj,state in pairs(S.ProjectileState) do if not seen[obj] and (not obj.Parent or now-(state.At or 0)>1) then S.ProjectileState[obj]=nil end end return false end local function dodgeStep() local ok,c,h,r=canAutoDodge(); if not ok then return end local now=os.clock() -- Projectiles get first priority because their time-to-impact can be shorter than the -- next enemy scan. Boss telegraphs then outrank animation-only melee prediction. if projectileThreat(r,now) then return end local nearby={} for m in pairs(S.Enemies) do if aliveEnemy(m) and enemyBelongsToActiveRun(m) and not (S.InvalidEnemyUntil[m] and now5 if changed then S.NavWatchGoal=target; S.NavBestDistance=distance; S.NavLastImprove=now; S.NavStuckStage=0 elseif distance<(S.NavBestDistance or math.huge)-0.8 then S.NavBestDistance=distance; S.NavLastImprove=now; S.NavStuckStage=0 end return now-(S.NavLastImprove or now),distance end local function sideStepAroundHit(r,target,hit,c) if not r or not hit then return nil end local normal=Vector3.new(hit.Normal.X,0,hit.Normal.Z) if normal.Magnitude<0.05 then return nil end normal=normal.Unit local tangent=Vector3.new(-normal.Z,0,normal.X) local toGoal=target-r.Position local goalFlat=Vector3.new(toGoal.X,0,toGoal.Z) -- Try both directions around the wall. Prefer the side that still has forward -- progress, but accept a pure sidestep when the route is head-on into geometry. local candidates={} for _,side in ipairs({1,-1}) do for _,radius in ipairs({2.5,4,6}) do local sideways=tangent*side*radius local forward=goalFlat.Magnitude>0.1 and goalFlat.Unit*math.min(2.5,radius*0.55) or Vector3.zero table.insert(candidates,r.Position+sideways+forward) table.insert(candidates,r.Position+sideways) end end table.sort(candidates,function(a,b) return (a-target).Magnitude<(b-target).Magnitude end) for _,q in ipairs(candidates) do q=snapFloor(q,c) if positionClear(q,c,S.Target) and not blockingRay(r.Position,q-r.Position,c,S.Target) then return q end end return nil end local function moveStep(dt) if not active() then return end local c,h,r=character(); if not c then return end if os.clock()0.35 and positionClear(r.Position,c,S.Target) then S.LastSafePosition=r.Position; S.LastSafeAt=os.clock() end if not O.Noclip and not positionClear(r.Position,c,S.Target) and ready("wallrecovery",0.6) then local safe=recoveryPosition(r,c) if safe then local look=(S.Target and part(S.Target) and part(S.Target).Position) or (safe+r.CFrame.LookVector) r.CFrame=CFrame.lookAt(safe,look) r.AssemblyLinearVelocity=Vector3.zero; r.AssemblyAngularVelocity=Vector3.zero S.Path=nil; S.PathIndex=nil; S.AvoidWallUntil=os.clock()+1.5 setStatus("Recovering from a wall") return end end -- Global flight envelope watchdog. The older protection only worked when the root -- remained horizontally above a room; if a stale enemy/goal pulled us diagonally away, -- projectedFlightRoom() returned nil and flight could continue forever under the map. if O.FlyNavigation and O.Noclip then if recoverEscapedFlight(r) then return end -- Mild vertical drift while still directly over a room is corrected immediately. local envelope=(not S.RoomTransitionIndex) and projectedFlightRoom(r.Position) or nil if envelope and envelope.Zone and envelope.Zone:IsA("BasePart") then local zone=envelope.Zone local lp=zone.CFrame:PointToObjectSpace(r.Position) local half=zone.Size*0.5 if math.abs(lp.Y)>half.Y+2.5 then local safe=roomRecoveryPoint(envelope,r.Position) if safe then local face=S.Goal-safe if face.Magnitude>0.05 then r.CFrame=CFrame.lookAt(safe,safe+face.Unit) else r.CFrame=CFrame.new(safe)*r.CFrame.Rotation end r.AssemblyLinearVelocity=Vector3.zero; r.AssemblyAngularVelocity=Vector3.zero S.FlyCruiseY=nil; S.FlyGoal=nil S.LastSafePosition=safe; S.LastSafeAt=os.clock() setStatus("Recovering inside the dungeon") return end end end end local target=S.Goal local aboveTarget if S.CombatMove and S.Target then local validTarget=enemyInsideDungeonBounds(S.Target) if not validTarget then S.InvalidEnemyUntil[S.Target]=math.huge S.Enemies[S.Target]=nil; S.EnemyRoomCache[S.Target]=nil S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 S.Target=nil; S.Goal=nil; S.CombatMove=false S.FlyGoal=nil; S.FlyCruiseY=nil; S.FlyCombat=nil setStatus("Ignoring an enemy outside dungeon bounds") return end end if S.CombatMove and O.Position=="Above" and S.Target and part(S.Target) then aboveTarget=part(S.Target) local hp=h.Health/math.max(h.MaxHealth,1)*100 local height=math.max(O.Distance,10) if O.Heal and hp pause -> fly cadence while approaching that enemy. -- Keep the same 3D movement path through target acquisition and only change the aim/hold -- behavior once we are actually at the combat position. if O.FlyNavigation and O.Noclip then local horizontal=Vector3.new(target.X-r.Position.X,0,target.Z-r.Position.Z) local changed=not S.FlyGoal or (S.FlyGoal-target).Magnitude>7 or S.FlyCombat~=S.CombatMove local roomTravel=(not S.CombatMove) and S.RoomTransitionIndex~=nil if changed then S.FlyGoal=target S.FlyCombat=S.CombatMove if not S.CombatMove and not roomTravel then S.FlyCruiseY=math.max(r.Position.Y,target.Y)+math.max(2,tonumber(O.FlyHeight) or 8) else -- A room transition already has a safe interior Y from roomFlightPoint. -- Adding another cruise-height offset is what pushed us through ceilings. S.FlyCruiseY=nil end elseif S.CombatMove then -- Enemy positions can update every farming tick. Track them without resetting -- movement state or inserting a pause. S.FlyGoal=target end local flyTarget=target if roomTravel then -- Direct noclip flight between rooms: continuous and vertically inside the -- destination room envelope. There is no reason to climb over geometry. local destinationRoom=roomByIndex(S.RoomTransitionIndex) flyTarget=clampRoomVertical(destinationRoom,target,2.5) S.FlyCruiseY=nil elseif not S.CombatMove and horizontal.Magnitude>4.5 then local cruiseY=math.max(S.FlyCruiseY or r.Position.Y,target.Y+2) flyTarget=Vector3.new(target.X,cruiseY,target.Z) local destinationRoom=roomAtPosition(target) if destinationRoom then flyTarget=clampRoomVertical(destinationRoom,flyTarget,2.5) end elseif not S.CombatMove then S.FlyCruiseY=nil end local travel=flyTarget-r.Position local arrival=S.CombatMove and 0.28 or 0.35 if travel.Magnitude0.01 then r.CFrame=CFrame.lookAt(target,target+face.Unit) else r.CFrame=CFrame.new(target)*r.CFrame.Rotation end end else r.CFrame=CFrame.new(target)*r.CFrame.Rotation end r.AssemblyLinearVelocity=Vector3.zero r.AssemblyAngularVelocity=Vector3.zero elseif (target-r.Position).Magnitude<1.1 then S.Goal=nil; S.Path=nil; S.PathIndex=nil; S.FlyGoal=nil; S.FlyCruiseY=nil; S.FlyCombat=nil end return end local step=math.min(travel.Magnitude,O.Speed*math.min(dt,0.1)) local pos=r.Position+travel.Unit*step if S.CombatMove and S.Target and part(S.Target) then local enemyPart=part(S.Target) local d=enemyPart.Position-pos if O.Position=="Above" and d.Magnitude>0.01 then r.CFrame=CFrame.lookAt(pos,enemyPart.Position) else local face=Vector3.new(d.X,0,d.Z) if face.Magnitude>0.01 then r.CFrame=CFrame.lookAt(pos,pos+face.Unit) else r.CFrame=CFrame.new(pos)*r.CFrame.Rotation end end else local face=Vector3.new(travel.X,0,travel.Z) if face.Magnitude>0.01 then r.CFrame=CFrame.lookAt(pos,pos+face.Unit) else r.CFrame=CFrame.new(pos)*r.CFrame.Rotation end end r.AssemblyLinearVelocity=Vector3.zero r.AssemblyAngularVelocity=Vector3.zero h:Move(Vector3.zero) return else S.FlyCruiseY=nil; S.FlyGoal=nil; S.FlyCombat=nil end local stalled,navDistance=navigationStall(r,target) if not S.CombatMove and stalled>1.1 then requestAvoidancePath(r,target) end if aboveTarget and delta.Magnitude<0.45 then r.CFrame=CFrame.lookAt(target,aboveTarget.Position) r.AssemblyLinearVelocity=Vector3.zero r.AssemblyAngularVelocity=Vector3.zero h:Move(Vector3.zero) return end if not aboveTarget and flat.Magnitude<1.1 and math.abs(delta.Y)<5 then h:Move(Vector3.zero) -- Clear the current goal so the farming loop can resolve a fresh, deeper room -- entry point instead of reporting "Moving to room" forever at the old boundary. S.Goal=nil; S.Path=nil; S.PathIndex=nil return end if O.Movement=="Walk" and not aboveTarget then if S.PathGoal and (S.PathGoal-target).Magnitude>12 then S.Path=nil; S.PathIndex=nil end if not S.Path or os.clock()-S.LastPath>2.5 then requestAvoidancePath(r,target) end local waypoint=pathWaypoint(r) if waypoint then if waypoint.Action==Enum.PathWaypointAction.Jump then h.Jump=true end h:MoveTo(waypoint.Position) else h:MoveTo(target) end else -- Fast mode still moves at the configured CFrame speed, but it no longer tunnels -- directly through walls. A blocked segment switches to path waypoints; a wall -- slide provides immediate movement while Pathfinding computes the route. local moveTarget=target local directDelta=moveTarget-r.Position local directHit=blockingRay(r.Position,directDelta,c,S.Target) if directHit and not aboveTarget then S.AvoidWallUntil=os.clock()+1.4; S.WallHitAt=os.clock() requestAvoidancePath(r,target) if not pathWaypoint(r) and stalled>0.65 then local bypass=sideStepAroundHit(r,target,directHit,c) if bypass then moveTarget=bypass end end elseif not directHit and os.clock()>(S.AvoidWallUntil or 0) and not S.CombatMove then S.Path=nil; S.PathIndex=nil end if (directHit or os.clock()<(S.AvoidWallUntil or 0)) and not aboveTarget then local waypoint=pathWaypoint(r) if waypoint then moveTarget=waypoint.Position if waypoint.Action==Enum.PathWaypointAction.Jump then h.Jump=true end end end local travel=moveTarget-r.Position if travel.Magnitude<0.05 then return end local step=math.min(travel.Magnitude,O.Speed*math.min(dt,0.1)) local stepVector=travel.Unit*step local hit=blockingRay(r.Position,stepVector,c,S.Target) local pos if hit then -- Slide parallel to the wall instead of entering it. If no useful slide is -- available, remain at the last safe point until the avoidance path is ready. local normal=hit.Normal local slide=stepVector-normal*stepVector:Dot(normal) if slide.Magnitude>0.15 then slide=slide.Unit*math.min(slide.Magnitude,step) local slideHit=blockingRay(r.Position,slide,c,S.Target) local candidate=r.Position+slide if not slideHit and positionClear(candidate,c,S.Target) then pos=candidate end end S.AvoidWallUntil=os.clock()+1.4; S.WallHitAt=os.clock() if not aboveTarget then requestAvoidancePath(r,target) end else local candidate=r.Position+stepVector if positionClear(candidate,c,S.Target) then pos=candidate end end if not pos then h:Move(Vector3.zero) if S.LastSafePosition and not positionClear(r.Position,c,S.Target) then r.CFrame=CFrame.new(S.LastSafePosition) S.NavLastImprove=os.clock() return end -- The old code returned here before the anti-stuck watchdog below could ever -- run. A head-on wall with no ready path therefore froze forever. Run bounded -- recovery right here, while the blocked condition is known. if not S.CombatMove then if hit and stalled>0.9 then local bypass=sideStepAroundHit(r,target,hit,c) if bypass then local d=bypass-r.Position local step2=math.min(d.Magnitude,math.max(3,O.Speed*math.min(dt,0.08))) local q=r.Position+d.Unit*step2 if positionClear(q,c,S.Target) and not blockingRay(r.Position,q-r.Position,c,S.Target) then r.CFrame=CFrame.lookAt(q,q+Vector3.new(d.X,0,d.Z).Unit) r.AssemblyLinearVelocity=Vector3.zero; r.AssemblyAngularVelocity=Vector3.zero S.NavLastImprove=os.clock() setStatus("Routing around a blocked doorway") return end end end if stalled>2.4 then S.Path=nil; S.PathIndex=nil; S.PathGoal=nil; S.LastPath=0 requestAvoidancePath(r,target) h.Jump=true setStatus("Rebuilding the route") end if stalled>4.8 then local safe=recoveryPosition(r,c) if safe and (safe-r.Position).Magnitude>0.5 then r.CFrame=CFrame.new(safe) r.AssemblyLinearVelocity=Vector3.zero; r.AssemblyAngularVelocity=Vector3.zero S.NavLastImprove=os.clock(); S.NavBestDistance=(target-safe).Magnitude end end end return end if not S.CombatMove then local floorHit=workspace:Raycast(pos+Vector3.new(0,7,0),Vector3.new(0,-22,0),movementRayParams(c,S.Target)) if floorHit and floorHit.Normal.Y>0.5 and floorHit.Instance.CanCollide then local grounded=Vector3.new(pos.X,floorHit.Position.Y+h.HipHeight+r.Size.Y/2,pos.Z) if positionClear(grounded,c,S.Target) then pos=grounded end end end local facing=flat.Magnitude>0.01 and flat.Unit or r.CFrame.LookVector local lookAtTarget=nil if S.CombatMove and S.Target and part(S.Target) then local targetPart=part(S.Target) local d=targetPart.Position-pos if O.Position=="Above" and d.Magnitude>0.01 then lookAtTarget=targetPart.Position else local horizontal=Vector3.new(d.X,0,d.Z) if horizontal.Magnitude>0.01 then facing=horizontal.Unit end end end if lookAtTarget then r.CFrame=CFrame.lookAt(pos,lookAtTarget) else r.CFrame=CFrame.lookAt(pos,pos+facing) end r.AssemblyLinearVelocity=Vector3.zero r.AssemblyAngularVelocity=Vector3.zero end if os.clock()-(S.StuckAt or 0)>1.5 then local moved=not S.LastPosition or (r.Position-S.LastPosition).Magnitude>=1.0 if not moved and (target-r.Position).Magnitude>6 then S.StuckCount=(S.StuckCount or 0)+1 S.AvoidWallUntil=os.clock()+2 requestAvoidancePath(r,target) if S.StuckCount>=2 then setStatus("Routing around an obstacle") h.Jump=true end if S.StuckCount>=4 then local safe=recoveryPosition(r,c) if safe then r.CFrame=CFrame.new(safe) r.AssemblyLinearVelocity=Vector3.zero; r.AssemblyAngularVelocity=Vector3.zero end S.StuckCount=0 end else S.StuckCount=0 end S.LastPosition=r.Position; S.StuckAt=os.clock() end end local function attackStep() if not active() or not inRun() or S.Complete or S.PendingBuff or S.PendingChest then return end local c,h,r=character(); local m=S.Target if not c or not hittable(m) or os.clock()math.max(12,O.Distance+3) then return end if S.RotationHumanoid~=h then if S.RotationHumanoid then pcall(function() S.RotationHumanoid.AutoRotate=S.OldAutoRotate end) end S.RotationHumanoid=h; S.OldAutoRotate=h.AutoRotate; h.AutoRotate=false end if O.Position=="Above" then -- Face the actual enemy position. The small horizontal offset used by Above mode -- avoids the straight-down lookAt singularity while still aiming almost vertically. r.CFrame=CFrame.lookAt(r.Position,p.Position) else r.CFrame=CFrame.lookAt(r.Position,Vector3.new(p.Position.X,r.Position.Y,p.Position.Z)) end if ready("attack",0.105) then input("Attack",attackDirection) end if O.Skills and ready("skillscan",0.25) then for offset=0,3 do local i=(S.SkillCursor+offset-1)%4+1; local key="Skill"..i local charges=LP:GetAttribute(key.."_Charges") local known=charges~=nil or LP:GetAttribute(key.."_OnCooldown")~=nil or LP:GetAttribute(key.."_HasHold")~=nil if known and not LP:GetAttribute(key.."_OnCooldown") and (charges==nil or charges>0) and ready(key,0.85) then input("Skill",i,"tap",attackDirection); S.SkillCursor=i%4+1; break end end end if O.Ultimate and LP:GetAttribute("HasUltimate") and LP:GetAttribute("UltimateReady") and ready("ultimate",2) then input("Skill","E","tap",attackDirection) end end local function combatGoal(m) local _,h,r=character(); if not r then return end local p=part(m); if not p then return end local inBounds,boundsRoom=enemyInsideDungeonBounds(m) if not inBounds then S.InvalidEnemyUntil[m]=math.huge S.Enemies[m]=nil; S.EnemyRoomCache[m]=nil if S.CombatLock==m then S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 end S.Target=nil; stopMotion(); setStatus("Ignoring an enemy outside dungeon bounds"); return end if not LP:GetAttribute("Weapon_Equipped") then stopMotion(); setStatus("Equip a class weapon to continue"); return end if not hittable(m) then stopMotion(); setStatus("Waiting for the enemy's protected phase"); return end local hp=h.Health/math.max(h.MaxHealth,1)*100 local range=O.Distance local retreat=O.Heal and hpO.HealAt then return end local id=d.EquippedPotion if not id or id=="" or (tonumber((d.Potions or {})[id]) or 0)<=0 or not ready("heal",1.5) then return end local ok,a,b,extra=call("PotionService","UsePotion",1) if ok and a then S.Cooldown.heal=os.clock()+3 elseif b=="POTION_ON_COOLDOWN" and type(extra)=="table" then S.Cooldown.heal=os.clock()+math.max(0.5,tonumber(extra.Remaining) or 1) end end local buffWeights={dmg_up=100,skill_dmg=85,skill_cooldown=90,skill_cdr=90,skill_cd=90,crit_rate=70,skill_crit_dmg=55,skill_crit_rate=60,dmg_reduction=65,hp_up=50,move_up=30,kill_xp=35,clear_xp=45,coins=20,parry_window=5,dodge_cd=25} local function buffId(v) if type(v)~="table" then return nil end local id=v.Id or v.ID or v.BuffId or v.BuffID if id then return tostring(id) end local title=tostring(v.Title or v.Name or "") local bd=S.Modules.BuffData for _,def in ipairs((bd and bd.Buffs) or {}) do if tostring(def.Title or "")==title then return def.Id end end return nil end local function buffScore(v) local id=buffId(v) local n=buffWeights[id] or 20 if O.Blessing=="Experience" and (id=="kill_xp" or id=="clear_xp") then n=n+120 end if O.Blessing=="Damage" and (id=="dmg_up" or id=="skill_dmg" or id=="crit_rate" or id=="skill_crit_rate" or id=="skill_crit_dmg" or id=="skill_cd") then n=n+80 end local _,h=character() if h and h.Health/h.MaxHealth<0.6 and (id=="dmg_reduction" or id=="hp_up") then n=n+100 end local clean=tostring(v.Description or v.Desc or ""):gsub("<[^>]+>","") local value=tonumber(v.Value or v.Amount or clean:match("([%d.]+)")) or 0 return n+value*0.25 end local function rewardScore(v) if type(v)~="table" then return 0 end if v.Type=="Equipment" then local d=data(); local old=d and (d.Equipment or {})[v.Slot] return 10000+Lobby.score(v)+math.max(0,Lobby.score(v)-Lobby.score(old))*2 end local weights={ClassItem=8000,Key=7000,Material=5000,Stars=4500,Coins=1000} return (weights[v.Type] or 3000)+math.min(tonumber(v.Amount) or 0,1000) end local function getChestController() local cc=S.Controllers.ChestSelectionController if cc then return cc end if Knit and Knit.GetController then local ok,v=pcall(Knit.GetController,"ChestSelectionController") if ok and v then S.Controllers.ChestSelectionController=v; return v end end return nil end local function recoverChestSelection() local cc=getChestController() if cc and cc._active and type(cc._candidates)=="table" and #cc._candidates>0 and cc._candidates~=S.HandledChest then local items=cc._candidates if not S.PendingChest or S.PendingChest.Items~=items then S.PendingChest={Items=items,Extra=cc._ownsExtraLoot==true,Source=cc._source or "Dungeon",At=os.clock()} S.ChestSelectionState=nil end S.ChestSelectionGraceUntil=math.max(S.ChestSelectionGraceUntil or 0,os.clock()+8) end return cc end local function chestSelectionOutstanding() if not O.Loot then return false end local cc=recoverChestSelection() return S.PendingChest~=nil or (cc and cc._active==true) end local function finishChestSelection(items,picks,cc) local pending=S.PendingChest S.HandledChest=items S.PendingChest=nil S.ChestSelectionState=nil S.LootCount=S.LootCount+#picks if pending and (pending.Source or "Dungeon")~="MidRun" then S.EndChestResolved=true end setStatus("Chest rewards selected") end local function handleChoices() if not active() then return false end -- Recover selections even when their RemoteEvent fired before our listener bound. local bc=S.Controllers.BoostSelectionController local cc=recoverChestSelection() if not S.PendingBuff and bc and bc._active and bc._candidates~=S.HandledBuff then S.PendingBuff={Items=bc._candidates,At=os.clock()} end if S.PendingBuff and O.Buffs then local pending=S.PendingBuff; local items=pending.Items if type(items)~="table" or #items==0 then S.PendingBuff=nil; S.BlessingSelection=nil; return false end stopMotion() local best=1 for i=2,#items do if buffScore(items[i])>buffScore(items[best]) then best=i end end local picked=items[best] local pickedName=tostring((type(picked)=="table" and (picked.Title or picked.Name or buffId(picked))) or "best") local now=os.clock() local state=S.BlessingSelection if state and state.Items==items then -- The game's own controller resets itself after SelectBuff completes. if state.Native and bc and (not bc._active or bc._candidates~=items) then S.HandledBuff=items; S.PendingBuff=nil; S.BlessingSelection=nil if S.ActiveBlessingPrompt then releasePrompt(S.ActiveBlessingPrompt,true); S.ActiveBlessingPrompt=nil end setStatus("Blessing selected") return true end if now-state.At<4.5 then setStatus("Choosing "..pickedName); return true end -- Native animation/controller stalled. Fall through to one server fallback. S.BlessingSelection=nil end -- Prefer the exact path the game uses: wait until the cards are ready, then -- call BoostSelectionController:_OnCardClicked(index). if bc and bc._active and bc._candidates==items and bc._ready and not bc._selecting and type(bc._OnCardClicked)=="function" then local ok=pcall(function() bc:_OnCardClicked(best) end) if ok then S.BlessingSelection={Items=items,Best=best,At=now,Native=true} setStatus("Choosing "..pickedName) return true end end if now-(pending.At or now)<1.35 then setStatus("Choosing a blessing"); return true end if ready("buffchoice",1.5) then local ok,a,b=call("DungeonBuffService","SelectBuff",best) if accepted(ok,a) then S.HandledBuff=items; S.PendingBuff=nil; S.BlessingSelection=nil if bc and bc._candidates==items and bc._Reset then pcall(function() bc:_Reset() end) end if S.ActiveBlessingPrompt then releasePrompt(S.ActiveBlessingPrompt,true); S.ActiveBlessingPrompt=nil end setStatus("Blessing selected") else setStatus("Blessing selection retrying"..(b and (" — "..tostring(b)) or "")) end end return true end if S.PendingChest and O.Loot then local pending=S.PendingChest; local items=pending.Items if type(items)~="table" or #items==0 then S.PendingChest=nil; S.ChestSelectionState=nil; return false end stopMotion() local indexes={}; for i=1,#items do table.insert(indexes,i) end table.sort(indexes,function(a,b) return rewardScore(items[a])>rewardScore(items[b]) end) -- Trust the live controller's max-pick count when available. This is 2 normally -- and 3 only when the player legitimately owns the extra-loot entitlement. local maxPicks=(cc and cc._candidates==items and tonumber(cc._maxPicks)) or (pending.Extra and 3 or 2) maxPicks=math.clamp(math.floor(tonumber(maxPicks) or 2),1,#indexes) local picks={}; for i=1,maxPicks do table.insert(picks,indexes[i]) end local now=os.clock() local state=S.ChestSelectionState if state and state.Items==items then -- _OnFinish immediately hides the UI, but its server request runs 0.35s later. -- Wait for the controller to actually reset its candidate table before treating -- the rewards as complete, otherwise RequestReturn can race SelectChests. if state.FinishAt then if not cc or cc._candidates~=items then finishChestSelection(items,picks,cc) return true end if now-state.FinishAt<5.5 then setStatus("Claiming selected chest rewards") return true end -- Native submission did not reset. Use the service as a bounded fallback. S.ChestSelectionState=nil elseif now-state.At<4 then setStatus("Opening the best chests") return true else S.ChestSelectionState=nil end end -- Primary path: drive the game's own ChestSelectionController exactly like clicks -- on the chosen chests followed by its Finish button. This preserves its internal -- selected-count/source state and the server submission timing. if cc and cc._active and cc._candidates==items then if not cc._ready then if now-(pending.At or now)<3.5 then setStatus("Waiting for chest selection to open") return true end elseif type(cc._OnChestClicked)=="function" and type(cc._OnFinish)=="function" then local ok=pcall(function() for _,index in ipairs(picks) do if not (cc._selected and cc._selected[index]) then cc:_OnChestClicked(index) end end end) if ok then local selected=tonumber(cc._selectedCount) or 0 if selected>=maxPicks then S.ChestSelectionState={Items=items,Picks=picks,At=now,Native=true} local finished=pcall(function() cc:_OnFinish() end) if finished then S.ChestSelectionState.FinishAt=os.clock() setStatus("Claiming selected chest rewards") return true end end end end end -- Fallback for executors where Knit controller internals are unavailable. if now-(pending.At or now)<1.25 then setStatus("Selecting the best chest rewards"); return true end if ready("chestchoice",1.5) then local method=(pending.Source=="MidRun") and "SelectMidRunChests" or "SelectChests" local ok,a,b=call("DungeonRunService",method,picks) if accepted(ok,a) then if cc and cc._candidates==items and cc._Reset then pcall(function() cc:_Reset() end) end finishChestSelection(items,picks,cc) else setStatus("Chest selection retrying"..(b and (" — "..tostring(b)) or "")) end end return true end if (S.PendingBuff and O.Buffs) or (S.PendingChest and O.Loot) then return true end return false end local difficulties={"Easy","Normal","Hard","Nightmare"} local function dungeonNames() local dd=S.Modules.DungeonData if not dd then return {} end -- ChallengeDisplayOrder is the game's non-legacy regular-dungeon list. local source=dd.ChallengeDisplayOrder or dd.DisplayOrder or {} local names={} for _,name in ipairs(source) do local def=dd.GetDungeon(name) if def and not def.HideFromSelect and not def.Legacy and not def.ChallengeMode then table.insert(names,name) end end return names end local function selectRun() local dd=S.Modules.DungeonData; local d=data() if not dd or not d then return nil end local names=dungeonNames() if O.Dungeon~="Auto" then names={O.Dungeon} end local level=tonumber(d.PlayerLevel) or 1 local candidates={} local manualCatacombsNormal=O.AvoidCatacombsNormal and O.Dungeon=="Catacombs" and O.Difficulty=="Normal" for mapOrder,name in ipairs(names) do local def=dd.GetDungeon(name) if def then local ok,access=call("DungeonQueueService","GetDungeonAccessState",name) if ok and type(access)=="table" and access.Unlocked then local ok2,unlocked=call("DungeonQueueService","GetUnlockedDifficulties",name) if ok2 and type(unlocked)=="table" then for diffRank,diff in ipairs(difficulties) do local gate=unlocked[diff] local bracket=(def.DifficultyLevelBrackets or {})[diff] or def.LevelRange or {} local minimum=tonumber(bracket.Min) or tonumber(def.MinLevel) or tonumber((def.LevelRange or {}).Min) or 1 local maximum=tonumber(bracket.Max) or tonumber((def.LevelRange or {}).Max) or math.huge local knownBroken=O.AvoidCatacombsNormal and name=="Catacombs" and diff=="Normal" local effectiveAuto=O.Difficulty=="Auto" or manualCatacombsNormal local allowedDifficulty if manualCatacombsNormal and name=="Catacombs" then -- The supplied live trace shows the server accepts Catacombs/Normal, -- creates the correct DungeonRequest, destroys it almost immediately, -- and the teleport then fails. Use the nearest safe progression option: -- Hard if the player's level/unlocks allow it, otherwise Easy. allowedDifficulty=(diff=="Hard" or diff=="Easy") else allowedDifficulty=(O.Difficulty=="Auto" or O.Difficulty==diff) end -- Automatic/workaround choices must respect the game's level bracket. -- A genuinely manual non-broken difficulty remains a deliberate override. local levelReady=(not effectiveAuto or level>=minimum) if gate and gate.Unlocked and allowedDifficulty and levelReady and not knownBroken then local key=name..":"..diff local failedUntil=tonumber(S.Failures[key]) or 0 local mapTier=tonumber(def.Tier) or tonumber(def.Difficulty) or tonumber(def.DisplayOrder) or mapOrder local displayOrder=tonumber(def.DisplayOrder) or mapOrder table.insert(candidates,{ Name=name, Difficulty=diff, DifficultyRank=diffRank, MapTier=mapTier, DisplayOrder=displayOrder, Minimum=minimum, Maximum=maximum, InRecommendedRange=level<=maximum, FailedUntil=failedUntil, Key=key, }) end end end end end end if #candidates==0 then return nil end -- Progression order is deliberate: -- 1) newest/highest-tier map we can actually enter, -- 2) hardest unlocked difficulty on that map that our level meets, -- 3) highest level requirement as a final progression tie-breaker. -- This prevents an older-map Nightmare from beating a newly available higher-tier map, -- and prevents alphabetical map names from affecting progression selection. table.sort(candidates,function(a,b) if a.MapTier~=b.MapTier then return a.MapTier>b.MapTier end if a.DisplayOrder~=b.DisplayOrder then return a.DisplayOrder>b.DisplayOrder end if a.DifficultyRank~=b.DifficultyRank then return a.DifficultyRank>b.DifficultyRank end if a.Minimum~=b.Minimum then return a.Minimum>b.Minimum end return a.Name1 and "Matchmaking" or "Solo" end local function partySelectionMatches(party,name,diff) return type(party)=="table" and party.SelectedMode=="Dungeon" and party.SelectedDungeon==name and party.SelectedDifficulty==diff end local function verifyPartySelection(name,diff,timeout) local deadline=os.clock()+(tonumber(timeout) or 2.4) repeat local ok,party=call("DungeonQueueService","RequestPartyData") if ok and partySelectionMatches(party,name,diff) then return true,party end task.wait(0.12) until not active() or os.clock()>=deadline return false,nil end local function startOnboardingIfNeeded(d) if not d or d.OnboardingCompleted~=false or d.OnboardingDidDungeon==true then return false end setStatus("Starting the tutorial dungeon") local ok,a,b=call("DungeonQueueService","RequestStartOnboardingRun") if accepted(ok,a) then S.QueueFailures=0 S.Cooldown.queue=os.clock()+10 else S.QueueFailures=S.QueueFailures+1 setStatus("Tutorial start failed — "..resultMessage(a,b)) S.Cooldown.queue=os.clock()+4 end return true end local function queueRun() local now=os.clock() -- A previous execution can leave the server-side pod queue active even though the new -- script has not received PodQueueUpdate yet. Clear it once before taking over the lobby. if not S.QueueSanitized then setStatus("Syncing the lobby queue") call("DungeonQueueService","RequestLeaveQueue") clearQueueState() S.QueueSanitized=true S.Cooldown.queue=now+0.8 return end if S.Queued then if S.QueueAt<=0 then S.QueueAt=now end local elapsed=now-S.QueueAt if O.QueueTimeout>0 and elapsed>=O.QueueTimeout then setStatus("Queue timed out — rejoining") call("DungeonQueueService","RequestLeaveQueue") clearQueueState() S.QueueFailures=S.QueueFailures+1 S.Cooldown.queue=os.clock()+1.5 return end if S.QueueCanStart and ready("queuedstart",2.5) then setStatus("Starting the queued dungeon") local ok,a,b=call("DungeonQueueService","RequestStartNow") if not accepted(ok,a) then setStatus("Queue start retry — "..resultMessage(a,b)) end else setStatus("Matchmaking — waiting for the dungeon") end return end if not ready("queue",4) then return end setStatus("Preparing the next run") if not Lobby.prepare() then setStatus("Lobby preparation is still loading — retrying") S.Cooldown.queue=os.clock()+1 return end if not active() then return end local d=data(); local cap=d and capacity(d) if cap and count(d.EquipmentInventory)>=cap-1 then setStatus("Inventory full — free space before another run") return end if startOnboardingIfNeeded(d) then return end local ok,party=call("DungeonQueueService","RequestPartyData") if not ok or type(party)~="table" then setStatus("Waiting for lobby party data") S.Cooldown.queue=os.clock()+2 return end if party.LeaderId and party.LeaderId~=LP.UserId then setStatus("Waiting for the party leader") return end local mode=effectiveQueueMode(party) if mode=="Solo" and partySize(party)>1 then setStatus("Solo needs a one-player party — use Auto or Matchmaking") return end local name,diff=selectRun() if not name then setStatus("No suitable unlocked dungeon — check dungeon settings") return end S.SelectedDungeon=name; S.SelectedDifficulty=diff if O.AvoidCatacombsNormal and O.Dungeon=="Catacombs" and O.Difficulty=="Normal" and diff~="Normal" then setStatus("Catacombs Normal teleport is broken — using "..diff) task.wait(0.35) end for _,v in ipairs({{"RequestSelectMode","Dungeon"},{"RequestSelectDungeon",name},{"RequestSelectDifficulty",diff}}) do local good,a,b=call("DungeonQueueService",v[1],v[2]) if not accepted(good,a) then S.QueueFailures=S.QueueFailures+1 setStatus("Dungeon selection retry — "..resultMessage(a,b)) S.Cooldown.queue=os.clock()+2 return end end local selectionReady=verifyPartySelection(name,diff,2.6) if not selectionReady then S.QueueFailures=S.QueueFailures+1 setStatus("Waiting for the server to confirm "..name.." / "..diff) S.Cooldown.queue=os.clock()+1.1 return end if mode=="Solo" then setStatus("Starting solo "..name.." / "..diff) local good,a,b=call("DungeonQueueService","RequestStartSoloRun") if accepted(good,a) then S.LobbyPrepared=false S.QueueFailures=0 S.QueueAt=os.clock() S.Cooldown.queue=os.clock()+10 setStatus("Warping into "..name.." / "..diff) else S.QueueFailures=S.QueueFailures+1 local reason=resultMessage(a,b) -- If another execution left a pod queue active, clear it and retry quickly. if reason:lower():find("queue",1,true) then call("DungeonQueueService","RequestLeaveQueue") clearQueueState() S.QueueSanitized=true S.Cooldown.queue=os.clock()+1 else S.Cooldown.queue=os.clock()+3 end setStatus("Solo start retry — "..reason) end return end setStatus("Joining matchmaking for "..name.." / "..diff) local good,a,b=call("DungeonQueueService","RequestStartPodQueue") if accepted(good,a) then S.LobbyPrepared=false S.Queued=true -- Preserve QueueCanStart if PodQueueUpdate arrived before this RemoteFunction returned. S.QueueAt=os.clock() S.QueueFailures=0 setStatus("Matchmaking — waiting for the dungeon") else S.QueueFailures=S.QueueFailures+1 setStatus("Matchmaking retry — "..resultMessage(a,b)) S.Cooldown.queue=os.clock()+3 end end local function resetRun() S.Session=S.Session+1; S.Complete=nil; S.Phase=""; S.Zone=0; S.Cleared={}; S.Visited={} S.PendingBuff=nil; S.PendingChest=nil; S.HandledBuff=nil; S.HandledChest=nil; S.BlessingSelection=nil S.ChestSelectionState=nil; S.ChestSelectionGraceUntil=0; S.EndChestResolved=false S.PromptUntil={}; S.UsedPrompts={}; S.World=nil; S.Rooms={}; S.Enemies={}; S.Goal=nil; S.Path=nil S.LockedPrompt=nil; S.LockedPromptKind=nil; S.LockedPromptAt=0; S.LockedPromptRetries=0; S.LockedPromptFireAt=0 S.PotionStationPrompt=nil; S.PotionStationAwait=0; S.PotionStationBefore=nil; S.PotionStationVerifyUntil=0; S.ActiveBlessingPrompt=nil; S.LastPromptRefresh=0; S.LootCollectedUIDs={} S.LastProgress=os.clock(); S.ZoneAt=os.clock(); S.LastEnemy=os.clock(); S.LastPhysicalRoom=nil; S.Drops={}; S.PendingEndless=nil; S.AfterCompletePrepared=false S.ServerRoomIndex=0; S.LastZoneEnteredIndex=0; S.NoEnemySince=0; S.RoomTransitionIndex=nil; S.RoomTransitionAt=0; S.RoomTransitionPoint=nil; S.LastDeepEnemySweep=0 S.Layout=nil; S.LayoutByIndex={}; S.LayoutOrder={}; S.BossLayoutIndex=nil; S.TreasureLayout={}; S.LayoutCurrentIndex=0; S.DungeonId=nil S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0; S.BlockingEnemyCount=0; S.RoomClearQuietSince=0 S.EnemyOutOfBoundsSince={}; S.InvalidEnemyUntil={}; S.LastConfirmedRoom=0; S.LastConfirmedRoomAt=0; S.FlightRecoveryAt=0; S.FlightOutsideSince=0 S.BossEngaged={}; S.ActiveBossLock=nil; S.BossEntryAt=0; S.BossEntryStage=0; S.BossEntryPoint=nil; S.BossEntryCycleAt=0; S.BossTouchAt=0 S.DodgeUntil=0; S.LastDodgeAt=0; S.DodgeReason=nil; S.DodgeTrackState={}; S.TelegraphState={}; S.ProjectileState={}; S.DodgeThreatUntil={}; S.BossAbilityCache={} end local function finishRun() if not S.Complete then return end -- DungeonComplete can arrive while a physical chest/book is still sitting in the generated -- world. The old main loop jumped straight into finishRun and never called navigate/interact -- again, so those rewards were abandoned before RequestReturn. if O.Loot and inRun() then local _,_,r=character() if r then if S.LockedPrompt then if interact() then setStatus("Collecting final physical loot") return end end local lootPrompt,lootKind=pendingLootPrompt(r,true) if lootPrompt then lockPromptObjective(lootPrompt,lootKind) if interact() then setStatus(lootKind=="book" and "Collecting final EXP book" or (lootKind=="chest" and "Collecting final dungeon chest" or "Unlocking final loot room")) return end end end end stopMotion() if os.clock()-(S.CompleteAt or 0)<2 then return end if O.Loot and S.Complete.IsVictory~=false then if chestSelectionOutstanding() then setStatus("Finishing dungeon chest rewards"); return end -- ChestSelection can arrive slightly after DungeonComplete. Never start lobby prep or -- RequestReturn during this grace period unless the end chest was already resolved. if not S.EndChestResolved and os.clock()<(S.ChestSelectionGraceUntil or 0) then setStatus("Waiting for end-of-dungeon chest rewards") return end end if O.RunAction=="Stop after run" then O.Enabled=false; if S.EnableControl then S.EnableControl:Set(false) end; setStatus("Run complete"); return end if not S.AfterCompletePrepared then Lobby.prepare(); S.AfterCompletePrepared=true end if not ready("finish",10) then return end if O.RunAction=="Replay dungeon" and S.Complete.IsVictory~=false then setStatus("Replaying dungeon") local ok,a=call("DungeonRunService","RequestReplay") if not accepted(ok,a) then setStatus("Replay unavailable — returning to lobby"); call("DungeonRunService","RequestReturn") end else setStatus("Returning to prepare the next run"); call("DungeonRunService","RequestReturn") end end local function collectDropPoint(healthOnly) local _,h,r=character(); if not r or (not healthOnly and not O.Loot) then return false end local now=os.clock(); local chosen,nearest=nil,math.huge for i=#(S.Drops or {}),1,-1 do local d=S.Drops[i]; local distance=(d.Position-r.Position).Magnitude local dropInBounds=positionNearDungeon(d.Position,55,28) if not dropInBounds or now-d.At>32 or (d.ReachedAt and now-d.ReachedAt>0.9) then table.remove(S.Drops,i) elseif (not healthOnly or d.Kind=="Health") and distance<80 and distancemath.min(prompt and prompt.MaxActivationDistance-1 or 5,6) then goal(warp.Position,false) elseif ready("extract",3) then stopMotion(); call("PortalService","UsePortal",best.Name) end return true end local last=S.Rooms[#S.Rooms] local spawns=last and last.Model:FindFirstChild("Spawns") local marker=spawns and spawns:FindFirstChild("Extract") if marker and marker:IsA("BasePart") then goal(marker.Position,false) end setStatus("Waiting for the extraction portal") return true end local function navigate() local _,_,r=character(); if not r then return end if os.clock()<(S.InteractingUntil or 0) then return end -- Once a one-shot altar/station is selected, finish that interaction before newly -- spawned enemies or drops can pull us away and create an oscillation. if S.LockedPrompt and interact() then return end local m=chooseEnemy(); S.Target=m local _,h=character() if m and O.Heal and h and h.Health/h.MaxHealth*100here.Index then setStatus("Clearing room "..there.Index.." before entering") elseif (tonumber(S.BlockingEnemyCount) or 0)>0 then setStatus("Finishing the last enemies in room "..progressIndex) end combatGoal(m) return end if collectDropPoint(false) then return end if interact() then return end if extractRun() then return end local physicalRoom=strictContainingRoom(r.Position) local room=physicalRoom or currentRoom(r) if not room then -- Room replication can lag behind StateUpdate/ZoneEntered, especially on newly unlocked maps. -- Re-scan immediately instead of waiting for the slow game-data loop. discoverWorld() physicalRoom=strictContainingRoom(r.Position) room=physicalRoom or currentRoom(r) if not room then if active() and ready("layout_missing_nav",0.9) then call("DungeonRunService","RequestZoneLayout") end setStatus("Syncing dungeon rooms") return end end -- Do not clear a transition just because currentRoom() picked the next room as the -- nearest one. Require actual Zone containment (or ZoneEntered below). Near a doorway, -- nearest-room guesses were clearing the lock and rebuilding the destination backwards. if S.RoomTransitionIndex and physicalRoom and tonumber(physicalRoom.Index)>=tonumber(S.RoomTransitionIndex) then S.RoomTransitionIndex=nil; S.RoomTransitionAt=0; S.RoomTransitionPoint=nil end local now=os.clock() local physicalIndex=physicalRoom and tonumber(physicalRoom.Index) or 0 if physicalIndex>0 and (not S.LastPhysicalRoom or physicalIndex>tonumber(S.LastPhysicalRoom)) then S.LastPhysicalRoom=physicalIndex; S.ZoneAt=now; S.LastProgress=now end local waveGrace=now-S.LastEnemy<1.3 or now-S.ZoneAt<1.2 if waveGrace then if not (O.FlyNavigation and O.Noclip) then stopMotion(); setStatus("Waiting for the next wave"); return end setStatus("Scanning while flying") end if (S.Phase=="SurviveRoom" or S.Phase=="MightRoom" or S.Phase=="RushRoom") and not S.Cleared[room.Index] then stopMotion(); setStatus("Completing the room challenge"); return end local nextRoom local transitionAlready=S.RoomTransitionIndex~=nil if transitionAlready then nextRoom=roomByIndex(S.RoomTransitionIndex) if not nextRoom then S.RoomTransitionIndex=nil; S.RoomTransitionAt=0; S.RoomTransitionPoint=nil transitionAlready=false end end if not nextRoom then -- Room progression is monotonic once ZoneEntered confirms an index. This prevents -- overlapping Zone geometry from sending us back toward an already-entered room. -- Never let currentRoom()'s nearest-room fallback advance progression. When the -- player is just outside the boss arena, nearest-room can already be the final room even -- though ZoneEntered never fired. That made nextRoom nil and produced "Waiting for the boss". local baseIndex=math.max( physicalIndex, tonumber(S.Zone) or 0, tonumber(S.LastZoneEnteredIndex) or 0 ) -- The server layout contains the real progression checkpoints (combat/checkpoint/treasure/ -- boss). Catacombs has many connector/wall-cap Room_n models between those checkpoints; -- marching every numeric Room_n was both slower and prone to choosing a decoration as a -- destination. Prefer the next layout index whenever available. local layoutNext=nextLayoutIndex(baseIndex) if layoutNext then nextRoom=roomByIndex(layoutNext) if not nextRoom then discoverWorld() nextRoom=roomByIndex(layoutNext) if not nextRoom then if ready("layout_room_sync_"..tostring(layoutNext),0.85) then call("DungeonRunService","RequestZoneLayout") end setStatus("Waiting for room "..layoutNext.." to generate") return end end else for _,v in ipairs(S.Rooms) do if tonumber(v.Index)>baseIndex then nextRoom=v; break end end end end if nextRoom then -- Do the enemy-free confirmation exactly once, *before* travel starts. After a -- transition has been committed, it owns navigation until ZoneEntered/strict physical -- entry confirms success. Re-running this gate during travel could clear/rebuild goals -- on alternating ticks and create the forward/backward bounce. if not transitionAlready then local serverIndex=math.max(tonumber(S.Zone) or 0,tonumber(S.ServerRoomIndex) or 0) local gateRoom=roomByIndex(math.max(tonumber(S.Zone) or 0,tonumber(S.LastZoneEnteredIndex) or 0)) or physicalRoom or room local gateIndex=gateRoom and tonumber(gateRoom.Index) or tonumber(room.Index) or 0 local required=tonumber(S.RequiredKills) or 0 local kills=tonumber(S.Kills) or 0 local serverCleared=S.Cleared[gateIndex]==true or serverIndex>gateIndex or (required>0 and kills>=required) local quietSince=tonumber(S.RoomClearQuietSince) or 0 local quietFor=quietSince>0 and (now-quietSince) or 0 local blockers=tonumber(S.BlockingEnemyCount) or 0 local requiredQuiet=serverCleared and 0.42 or 0.72 if blockers>0 or quietFor0 then setStatus("Finishing "..blockers.." remaining enem"..(blockers==1 and "y" or "ies").." before room "..nextRoom.Index) else setStatus("Confirming room "..gateIndex.." is fully clear") end return end end setStatus("Moving to room "..nextRoom.Index) if O.FlyNavigation and O.Noclip then if not transitionAlready or S.RoomTransitionIndex~=nextRoom.Index then S.RoomTransitionIndex=nextRoom.Index S.RoomTransitionAt=now S.RoomTransitionPoint=nil transitionAlready=true end -- Noclip flight should never stop at the doorway/Zone edge. Target a point deep -- inside the room so movement crosses the game's real trigger in one continuous run. local p=S.RoomTransitionPoint if not p then p=roomFlightPoint(nextRoom,r.Position) S.RoomTransitionPoint=p end if p then -- If we somehow reach the deep point without either geometry or the server -- acknowledging the room, push even farther toward the far half of the Zone. -- This removes the final "stopped right next to the next room" failure mode. if now-(S.RoomTransitionAt or now)>1.35 and (p-r.Position).Magnitude<2.75 then local cf,size=roomGeometry(nextRoom) if cf and size then local lp=cf:PointToObjectSpace(p) local half=size*0.5 local lx,lz=lp.X,lp.Z if math.abs(lp.X)/math.max(half.X,1)>=math.abs(lp.Z)/math.max(half.Z,1) then lx=(lp.X>=0 and 1 or -1)*math.max(3,half.X*0.68); lz=0 else lz=(lp.Z>=0 and 1 or -1)*math.max(3,half.Z*0.68); lx=0 end local q=cf:PointToWorldSpace(Vector3.new(lx,0,lz)) p=q; S.RoomTransitionPoint=p; S.RoomTransitionAt=now setStatus("Pushing through to room "..nextRoom.Index) end end goal(p,false) else local anchor=nextRoom.Anchor.Position goal(Vector3.new(anchor.X,anchor.Y+3,anchor.Z),false) end else S.RoomTransitionIndex=nil; S.RoomTransitionAt=0; S.RoomTransitionPoint=nil local p=roomNavigationPoint(nextRoom,r.Position) if p then goal(p,false) else local anchor=nextRoom.Anchor.Position goal(Vector3.new(anchor.X,r.Position.Y,anchor.Z),false) end end else S.RoomTransitionIndex=nil; S.RoomTransitionAt=0; S.RoomTransitionPoint=nil if S.Phase=="BossDefeated" or S.Phase=="Escape" then S.BossEntryAt=0; S.BossEntryStage=0; S.BossEntryPoint=nil; S.BossEntryCycleAt=0; S.BossTouchAt=0 stopMotion() setStatus("Waiting for extraction") return end -- Once progression reaches the server's boss checkpoint, choose the activation strategy -- from the actual dungeon. Knights needs the explicit outside->inside touch re-arm observed -- in its castle arena. Catacombs does not: its supplied capture has no Boss_Spawn at all and -- several zone-less rooms, so forcing the Knights loop would send us into a wall-cap. local dungeonId=activeDungeonId() local finalRoom=bossRoom() local bossPhase=S.Phase=="Boss" or S.Phase=="BossPhase" or S.Phase=="BossWarp" local liveBoss=S.ActiveBossLock and aliveEnemy(S.ActiveBossLock) if liveBoss or bossPhase then S.BossEntryAt=0; S.BossEntryStage=0; S.BossEntryPoint=nil; S.BossEntryCycleAt=0; S.BossTouchAt=0 stopMotion() if dungeonId=="Catacombs" then setStatus("Waiting for Verath") else setStatus("Waiting for the boss") end return end if dungeonId=="Knights" and finalRoom then local finalIndex=tonumber(finalRoom.Index) or 0 local strictFinal=physicalRoom and tonumber(physicalRoom.Index)==finalIndex if strictFinal then pulseBossRoomTouch(finalRoom,r) end local outside,inside,deep=bossReentryPoints(finalRoom) local cycleAt=tonumber(S.BossEntryCycleAt) or 0 if cycleAt<=0 then S.BossEntryCycleAt=now; cycleAt=now end local cycleElapsed=now-cycleAt local stage=tonumber(S.BossEntryStage) or 0 local point if not strictFinal then stage=2; point=inside or deep or bossActivationPoint(finalRoom,0) setStatus("Entering Knights boss room") elseif cycleElapsed<1.35 then stage=0; point=deep or bossActivationPoint(finalRoom,0) setStatus("Arming Knights boss encounter") elseif cycleElapsed<2.30 and outside then stage=1; point=outside setStatus("Rearming Knights boss trigger") elseif cycleElapsed<3.25 then stage=2; point=inside or deep setStatus("Crossing Knights boss trigger") else stage=3; point=deep or bossActivationPoint(finalRoom,1) pulseBossRoomTouch(finalRoom,r) if point and (point-r.Position).Magnitude<3 then S.BossEntryCycleAt=now end setStatus("Waiting for Knight Lord") end S.BossEntryStage=stage; S.BossEntryPoint=point; S.BossEntryAt=now if point then S.RoomTransitionIndex=finalIndex; S.RoomTransitionAt=now; S.RoomTransitionPoint=point goal(point,false) return end elseif finalRoom then -- Generic/Catacombs activation: go to the server-marked boss layout room once and hold -- a deep interior point. Do not repeatedly leave/re-enter unless the map is Knights. local finalIndex=tonumber(finalRoom.Index) or tonumber(S.BossLayoutIndex) or 0 local point=roomFlightPoint(finalRoom,r.Position) or roomCenter(finalRoom) if point then S.BossEntryPoint=point; S.BossEntryAt=now S.RoomTransitionIndex=finalIndex>0 and finalIndex or nil S.RoomTransitionAt=now; S.RoomTransitionPoint=point setStatus(dungeonId=="Catacombs" and "Entering Verath boss checkpoint" or "Entering boss checkpoint") goal(point,false) return end else -- The boss tile can stream/generate only after the last combat checkpoint. Ask for the -- layout and re-scan instead of treating the current numerically-last wall-cap as boss. if ready("boss_layout_sync",0.9) then call("DungeonRunService","RequestZoneLayout") end discoverWorld() stopMotion() setStatus(dungeonId=="Catacombs" and "Waiting for Verath room to generate" or "Waiting for boss room") return end stopMotion() setStatus(dungeonId=="Catacombs" and "Waiting for Verath" or "Waiting for the boss") end end local bound={} local function bind(service,event,fn) local r=remote(service,event,"RE") if not r or bound[r] then return end bound[r]=true track(r.OnClientEvent:Connect(function(...) if S.Alive then local ok,err=pcall(fn,...); if not ok then S.Errors[event]=tostring(err) end end end)) end local function bindEvents() bind("DungeonChestService","ChestCollected",function(uid) markLootCollected("chest",uid) S.LootCount=(tonumber(S.LootCount) or 0)+1 setStatus("Dungeon chest collected") end) bind("DungeonChestService","BookCollected",function(uid) markLootCollected("book",uid) S.LootCount=(tonumber(S.LootCount) or 0)+1 setStatus("EXP book collected") end) bind("DungeonRunService","ZoneEntered",function(index) local idx=tonumber(index) S.Zone=idx or S.Zone if idx then S.LayoutCurrentIndex=math.max(tonumber(S.LayoutCurrentIndex) or 0,idx) S.ServerRoomIndex=math.max(tonumber(S.ServerRoomIndex) or 0,idx) S.LastZoneEnteredIndex=math.max(tonumber(S.LastZoneEnteredIndex) or 0,idx) S.LastPhysicalRoom=math.max(tonumber(S.LastPhysicalRoom) or 0,idx) S.LastConfirmedRoom=math.max(tonumber(S.LastConfirmedRoom) or 0,idx) S.LastConfirmedRoomAt=os.clock() end if idx and S.RoomTransitionIndex and idx>=S.RoomTransitionIndex then S.RoomTransitionIndex=nil; S.RoomTransitionAt=0; S.RoomTransitionPoint=nil end local final=bossRoom() if idx and final and (S.Phase=="Boss" or S.Phase=="BossPhase" or S.Phase=="BossWarp") then S.BossEntryAt=0; S.BossEntryStage=0; S.BossEntryPoint=nil; S.BossEntryCycleAt=0; S.BossTouchAt=0 end S.ZoneAt=os.clock(); S.LastProgress=os.clock() end) bind("DungeonRunService","PhaseChange",function(phase,info) S.Phase=phase; S.LastProgress=os.clock(); info=type(info)=="table" and info or {} if phase=="RoomCleared" and info.RoomIndex then local idx=tonumber(info.RoomIndex) or info.RoomIndex S.Cleared[idx]=true if (tonumber(S.BlockingEnemyCount) or 0)<=0 then S.NoEnemySince=os.clock() if (S.RoomClearQuietSince or 0)<=0 then S.RoomClearQuietSince=os.clock() end end end if phase=="BossWarp" or phase=="Boss" or phase=="BossPhase" then S.BossEntryAt=0; S.BossEntryStage=0; S.BossEntryPoint=nil; S.BossEntryCycleAt=0; S.BossTouchAt=0 end if phase=="BossWarp" then S.HoldUntil=os.clock()+2 end if phase=="Failed" then S.Complete={IsVictory=false}; S.CompleteAt=os.clock() end end) bind("DungeonRunService","StateUpdate",function(phase,a,b,index,total) if not S.Complete then S.Phase=phase end S.Kills=a; S.RequiredKills=b; S.TotalRooms=total local idx=tonumber(index) if idx and idx>0 then S.LayoutCurrentIndex=math.max(tonumber(S.LayoutCurrentIndex) or 0,idx) -- StateUpdate is progression data, not proof that the character crossed the -- doorway. Keep it for clear/difficulty logic but never release a travel lock here. S.ServerRoomIndex=math.max(tonumber(S.ServerRoomIndex) or 0,idx) end end) bind("DungeonRunService","RoomLayoutUpdate",function(layout,currentIndex) layoutRebuild(layout,currentIndex) S.LastProgress=os.clock() job(function() task.wait(0.03); discoverWorld() end) end) bind("DungeonRunService","ReplayStarting",function() resetRun() end) bind("DungeonRunService","EndlessDecision",function(info) S.PendingEndless=info or {} end) bind("DungeonRunService","EndlessTransition",function(state) if state=="Begin" then stopMotion(); S.HoldUntil=os.clock()+8 elseif state=="Done" then resetRun(); S.HoldUntil=0 end end) bind("DungeonRunService","DungeonComplete",function(result) if S.Complete and S.Complete==result then return end S.Complete=type(result)=="table" and result or {}; S.CompleteAt=os.clock() S.EndChestResolved=false S.ChestSelectionGraceUntil=S.CompleteAt+6 S.Runs=S.Runs+1 if S.Complete.IsVictory then S.Wins=S.Wins+1 -- A successful fallback proves progression is healthy again. Clear temporary -- candidate cooldowns so the very next run can retry the strongest unlocked target. if O.Difficulty=="Auto" then S.Failures={} end else local name=S.Complete.LocationId or S.SelectedDungeon local diff=S.Complete.Difficulty or S.SelectedDifficulty -- Do not globally reduce Auto difficulty. Cool down only the exact run that failed, -- then prefer the next-lower difficulty on the same highest-tier map. if name and diff and O.Difficulty=="Auto" then S.Failures[name..":"..diff]=os.clock()+90 end end end) bind("DungeonBuffService","BuffSelection",function(items) S.PendingBuff={Items=items,At=os.clock()}; S.BlessingSelection=nil end) bind("DungeonRunService","PotionStationUsed",function() confirmPotionStation() setStatus("Potions refilled") end) bind("DungeonRunService","ChestSelection",function(items,extra) S.PendingChest={Items=items,Extra=extra==true,Source="Dungeon",At=os.clock()} S.ChestSelectionState=nil; S.EndChestResolved=false S.ChestSelectionGraceUntil=math.max(S.ChestSelectionGraceUntil or 0,os.clock()+12) end) bind("DungeonRunService","MidRunChestSelection",function(items,extra) S.PendingChest={Items=items,Extra=extra==true,Source="MidRun",At=os.clock()} S.ChestSelectionState=nil end) bind("DungeonQueueService","PodQueueUpdate",function(queued,canStart) S.Queued=queued==true S.QueueCanStart=canStart==true if S.Queued then if S.QueueAt<=0 then S.QueueAt=os.clock() end if S.QueueCanStart and active() then job(function() task.wait(0.35) if active() and S.Queued and S.QueueCanStart and ready("queuedstart",1.5) then local ok,a,b=call("DungeonQueueService","RequestStartNow") if not accepted(ok,a) then setStatus("Queue start retry — "..resultMessage(a,b)) end end end) end else clearQueueState() end end) -- The game's drop controller already validates and collects batches. Move to -- real spawn positions and let that controller preserve IDs/count semantics. bind("DropService","SpawnDrops",function(position,kind,amount,value,source,material,batch) if typeof(position)=="Vector3" and kind~="CoinBurst" then S.Drops=S.Drops or {}; table.insert(S.Drops,{Position=position,Kind=kind,At=os.clock()}) if #S.Drops>100 then table.remove(S.Drops,1) end end end) end local UI for attempt=1,3 do local ok,v=pcall(function() local source=game:HttpGet("https://raw.githubusercontent.com/PuckAFK/Puck-Loader/refs/heads/main/ui/PuckUI.lua") local chunk,err=loadstring(source); assert(chunk,err); return chunk() end) if ok and type(v)=="table" and v.CreateWindow then UI=v; break end if attempt<3 then task.wait(0.8) end end if not UI then S.Alive=false; env[KEY]=nil; warn("PuckAFK: PuckUI could not load. Please run the script again."); return end local Window=UI:CreateWindow({ Name="PuckAFK | Dungeon Lootr", Title="PuckAFK | Dungeon Lootr", Subtitle="Smart Autofarm", GuiName="PuckAFK_DungeonLootr", ConfigId="DungeonLootr", Width=560, Height=590 }) local Main=Window:CreateTab("Autofarm") local Combat=Window:CreateTab("Combat") local Gear=Window:CreateTab("Upgrades") local Settings=Window:CreateTab("Settings") local statusLabel=Main:CreateLabel("Ready") local statsLabel=Main:CreateLabel("Runs 0 • Wins 0") Main:CreateParagraph({ Title="Start here", Content="Turn on Smart Autofarm and leave Dungeon, Difficulty and Run type on Auto for the recommended full progression loop.", Height=58 }) local controls={} local function toggle(tab,key,label) controls[key]=tab:CreateToggle({ Name=label, CurrentValue=O[key], Flag="DL_"..key, Callback=function(v) O[key]=v==true if key=="Noclip" and not O.Noclip then restoreCharacterCollisions() end if key=="Enabled" then if O.Enabled then S.QueueSanitized=false S.Cooldown.queue=0 if not inRun() then setStatus("Preparing next dungeon") end else stopMotion() restoreCharacterCollisions() setStatus("Paused") if not inRun() then leaveQueueDetached() clearQueueState() S.QueueSanitized=false end end end end }) end local function dropdown(tab,key,label,values) controls[key]=tab:CreateDropdown({ Name=label, Options=values, CurrentOption={O[key]}, Flag="DL_"..key, Callback=function(v) if type(v)=="table" then v=v[1] end if type(v)=="string" then O[key]=v if key=="QueueMode" and not inRun() then leaveQueueDetached() clearQueueState() S.QueueSanitized=false S.Cooldown.queue=0 end end end }) end local function slider(tab,key,label,min,max,step) controls[key]=tab:CreateSlider({ Name=label, Range={min,max}, Increment=step or 1, CurrentValue=O[key], Flag="DL_"..key, Callback=function(v) O[key]=math.clamp(tonumber(v) or O[key],min,max) end }) end local function setControl(key,value) O[key]=value local c=controls[key] if c and c.Set then pcall(function() c:Set(value) end) end end -- AUTOFARM ------------------------------------------------------------ Main:CreateSection("Run") toggle(Main,"Enabled","Smart Autofarm") S.EnableControl=controls.Enabled dropdown(Main,"Dungeon","Dungeon",{"Auto","Bandits Den"}) dropdown(Main,"Difficulty","Difficulty",{"Auto","Easy","Normal","Hard","Nightmare"}) dropdown(Main,"QueueMode","Run type",{"Auto","Solo","Matchmaking"}) dropdown(Main,"RunAction","After dungeon",{"Prepare and repeat","Replay dungeon","Stop after run"}) Main:CreateSection("Loot & blessings") toggle(Main,"Loot","Collect chests, books and drops") toggle(Main,"Buffs","Use blessings") dropdown(Main,"Blessing","Blessing focus",{"Balanced","Damage","Experience"}) -- COMBAT -------------------------------------------------------------- Combat:CreateParagraph({ Title="Recommended combat", Content="The default settings are designed for fast AFK clears. Change these only if you prefer a different fighting position or distance.", Height=58 }) Combat:CreateSection("Fighting") toggle(Combat,"Skills","Use abilities automatically") toggle(Combat,"Ultimate","Use ultimate automatically") dropdown(Combat,"Position","Fight from",{"Behind","Orbit","Above"}) slider(Combat,"Distance","Distance from enemies",3,10,0.5) Combat:CreateSection("Safety") toggle(Combat,"AutoDodge","Dodge dangerous attacks") toggle(Combat,"Heal","Use healing potions") slider(Combat,"HealAt","Use potion below health %",20,85,5) Combat:CreateSection("Travel") toggle(Combat,"FlyNavigation","Fast travel between rooms") toggle(Combat,"Noclip","Move through obstacles") slider(Combat,"Speed","Travel speed",25,130,5) -- UPGRADES ------------------------------------------------------------ Gear:CreateParagraph({ Title="Automatic upgrades", Content="Smart lobby upgrades prepares your character before every dungeon: gear, stats, weapons, crafting, potions, rewards and inventory cleanup.", Height=68 }) Gear:CreateSection("Power") toggle(Gear,"LobbyOptimize","Smart lobby upgrades") toggle(Gear,"Equip","Equip strongest gear") toggle(Gear,"Stats","Build stats for current weapon") toggle(Gear,"Buy","Buy better armour and rings") toggle(Gear,"BuyWeapons","Buy better permanent weapons") toggle(Gear,"CraftGear","Craft stronger Exotic gear") toggle(Gear,"CraftClassUnlocks","Craft available class weapons") toggle(Gear,"SafeForge","Safely upgrade equipped gear") toggle(Gear,"BestPotion","Use best healing potion") Gear:CreateSection("Rewards & inventory") toggle(Gear,"Rewards","Claim earned rewards") toggle(Gear,"OpenGearPacks","Open useful reward boxes") toggle(Gear,"Sell","Sell weak spare gear") dropdown(Gear,"SellRarity","Sell spare gear up to",{"Common","Uncommon","Rare"}) slider(Gear,"Reserve","Keep at least this many coins",0,1000000,1000) Gear:CreateSection("Rare currency") toggle(Gear,"RaidPower","Use Mage Coins for permanent raid weapons") Gear:CreateParagraph({ Title="Mage Coins", Content="Off by default. Turn this on only if you want the autofarm to spend rare Mage Coins on permanent raid weapons.", Height=58 }) -- SETTINGS ------------------------------------------------------------ Settings:CreateSection("General") toggle(Settings,"ContinueTeleport","Keep autofarm running after teleports") toggle(Settings,"AntiIdle","Prevent idle disconnects") Settings:CreateSection("Recommended setup") Settings:CreateButton({ Name="Restore recommended settings", Callback=function() local recommended={ Dungeon="Auto", Difficulty="Auto", QueueMode="Auto", RunAction="Prepare and repeat", Loot=true, Buffs=true, Blessing="Balanced", Skills=true, Ultimate=true, Position="Behind", Distance=5, AutoDodge=true, Heal=true, HealAt=55, FlyNavigation=true, Noclip=true, Speed=85, LobbyOptimize=true, Equip=true, Stats=true, Buy=true, BuyWeapons=true, CraftGear=true, CraftClassUnlocks=true, SafeForge=true, BestPotion=true, Rewards=true, OpenGearPacks=true, Sell=true, SellRarity="Uncommon", Reserve=2500, RaidPower=false, ContinueTeleport=true, AntiIdle=true, } for k,v in pairs(recommended) do setControl(k,v) end -- Keep the hidden helper systems aligned with the simple recommended preset. O.AvoidCatacombsNormal=true O.DodgeTelegraphs=true O.DodgeBasics=true O.DodgeProjectiles=true O.Movement="Fast" O.FlyHeight=8 O.RetreatAt=22 O.RespecStats=true O.Identify=true O.Expand=true O.RestockPotions=true O.SmartBank=true O.SynthesizeMaterials=true O.FreeChests=true O.PatchRewards=true O.ExtraRewards=true O.QueueTimeout=55 setStatus("Recommended settings restored") end }) Settings:CreateParagraph({ Title="Profiles", Content="Use the Configs tab to save or load your preferred setup. The autofarm continues through Dungeon Lootr teleports automatically.", Height=58 }) Settings:CreateSection("Session") Settings:CreateButton({ Name="Return to lobby", Callback=function() O.Enabled=false if controls.Enabled then controls.Enabled:Set(false) end stopMotion() local r=remote("DungeonRunService","RequestReturn") if r then job(function() pcall(function() r:InvokeServer() end) end) end end }) -- Convert internal recovery/technical states into short player-facing status text. local function friendlyStatus(value) local s=tostring(value or "Ready") local l=s:lower() if l:find("recovering",1,true) then return "Resuming autofarm" end if l:find("syncing dungeon rooms",1,true) or l:find("waiting for dungeon rooms",1,true) or l:find("requestzonelayout",1,true) then return "Loading dungeon" end if l:find("releasing a stale movement lock",1,true) or l:find("rebuilding the route",1,true) or l:find("routing around a blocked doorway",1,true) then return "Moving to objective" end if l:find("correcting boss chase",1,true) then return "Following boss" end if l:find("rearming boss room trigger",1,true) or l:find("crossing boss room trigger",1,true) or l:find("arming knights boss encounter",1,true) or l:find("waiting for knights boss spawn",1,true) or l:find("triggering boss encounter",1,true) then return "Starting boss fight" end if l:find("catacombs normal teleport",1,true) then return "Preparing Catacombs" end if l:find("server to confirm",1,true) then return "Preparing dungeon" end if l:find("teleport failed",1,true) then return "Retrying dungeon" end if l:find("layout",1,true) and l:find("waiting",1,true) then return "Loading dungeon" end return s end local originalDestroy=Window.Destroy function S.Unload() if not S.Alive then return end if not inRun() then leaveQueueDetached() end O.Enabled=false; stopMotion(); restoreCharacterCollisions(); S.Alive=false if S.HeldPrompt then pcall(function() S.HeldPrompt:InputHoldEnd() end); S.HeldPrompt=nil end for _,c in ipairs(S.Connections) do pcall(function() c:Disconnect() end) end for t in pairs(S.Jobs) do if t~=coroutine.running() then pcall(task.cancel,t) end end S.Connections={}; S.Jobs={} if env[KEY]==S then env[KEY]=nil end pcall(originalDestroy,Window) end Window.Destroy=function() S.Unload() end Settings:CreateButton({Name="Unload",Callback=S.Unload}) track(Window.ScreenGui.Destroying:Connect(function() if S.Alive then S.Unload() end end)) local function onCharacter(c) restoreCharacterCollisions() S.CollisionOriginal={} S.Target=nil; S.Goal=nil; S.Path=nil; S.LastPosition=nil; S.RotationHumanoid=nil S.FlyCruiseY=nil; S.FlyGoal=nil; S.FlyCombat=nil S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 S.LastSafePosition=nil; S.LastSafeAt=0; S.FlightRecoveryAt=0; S.CharacterSpawnAt=os.clock() S.DodgeUntil=0; S.LastDodgeAt=0; S.DodgeTrackState={}; S.TelegraphState={}; S.ProjectileState={}; S.DodgeThreatUntil={} job(function() local h=c:WaitForChild("Humanoid",10) if h and S.Alive then track(h.Died:Connect(function() S.Deaths=S.Deaths+1; S.Target=nil; S.Goal=nil; S.Path=nil S.FlyGoal=nil; S.FlyCruiseY=nil; S.FlyCombat=nil S.CombatLock=nil; S.CombatLockRoom=nil; S.CombatLockLostAt=0 S.DodgeUntil=0; S.DodgeTrackState={}; S.TelegraphState={}; S.ProjectileState={}; S.DodgeThreatUntil={} setStatus("Waiting to respawn") end)) end end) end track(LP.CharacterAdded:Connect(onCharacter)); if LP.Character then onCharacter(LP.Character) end for _,tag in ipairs({"Enemy","NPC"}) do track(Collection:GetInstanceAddedSignal(tag):Connect(function(m) if S.World and m:IsA("Model") and m:IsDescendantOf(S.World) and not m:FindFirstAncestor("Dialogue_NPCS") then S.Enemies[m]=true end end)) track(Collection:GetInstanceRemovedSignal(tag):Connect(function(m) if not m:HasTag("Enemy") and not m:HasTag("NPC") then S.Enemies[m]=nil end end)) end track(workspace.DescendantAdded:Connect(function(v) if not v:IsA("ProximityPrompt") then return end if (S.World and v:IsDescendantOf(S.World)) or v:FindFirstAncestor("_LocalBossLootChests") then S.Prompts[v]=true end end)) local wasIn=inRun() track(LP:GetAttributeChangedSignal("InDungeon"):Connect(function() local now=inRun() if now and not wasIn then resetRun() end if not now and wasIn and not S.Complete then stopMotion() end wasIn=now end)) track(TeleportService.TeleportInitFailed:Connect(function(player,result,errorMessage,placeId,teleportOptions) if player~=LP or not active() then return end local name=S.SelectedDungeon local diff=S.SelectedDifficulty if name and diff then local key=name..":"..diff S.Failures[key]=os.clock()+120 end clearQueueState() S.LobbyPrepared=false S.Cooldown.queue=os.clock()+1.5 if name=="Catacombs" and diff=="Normal" then setStatus("Catacombs Normal teleport failed — switching difficulty") else setStatus("Teleport failed — retrying safely") end end)) track(LP.Idled:Connect(function() if not active() or not O.AntiIdle then return end pcall(function() local vu=game:GetService("VirtualUser"); vu:CaptureController(); vu:ClickButton2(Vector2.new(0,0)) end) end)) local function encodeLua(t) local parts={"{"} for k,v in pairs(t) do local value if type(v)=="boolean" or type(v)=="number" then value=tostring(v) elseif type(v)=="string" then value=string.format("%q",v) end if value then table.insert(parts,"["..string.format("%q",k).."]="..value..",") end end table.insert(parts,"}"); return table.concat(parts) end local function resumeSnapshot() local t={} for k,v in pairs(O) do t[k]=v end t.__Runs=S.Runs t.__Wins=S.Wins t.__Deaths=S.Deaths t.__LootCount=S.LootCount t.__LastOptimizedClass=S.LastOptimizedClass or "" return t end local teleportQueued=false local function queueHook() local candidates={ rawget(env,"queue_on_teleport"), rawget(env,"queueonteleport"), rawget(_G,"queue_on_teleport"), rawget(_G,"queueonteleport"), syn and syn.queue_on_teleport, fluxus and fluxus.queue_on_teleport, } for _,v in ipairs(candidates) do if type(v)=="function" then return v end end end local function queueContinuation() if not O.ContinueTeleport or teleportQueued then return false end local queue=queueHook() local currentSource=env.__PUCK_DUNGEON_SOURCE if type(queue)~="function" or type(currentSource)~="string" then return false end -- Allow every present/future subplace inside Dungeon Lootr's live or test -- universe, instead of only the two place IDs known when this script was built. local guard="local u=game.GameId;local p=game.PlaceId;local ok=(u==9656201728 or u==10004532523 or p==106484206883664 or p==132285059959516 or p==129976314326660 or p==95503046258891 or p==89954929513873 or p==103006040833273);if not ok then return end;" local code=guard.."local s="..string.format("%q",currentSource)..";getgenv().__PUCK_DUNGEON_SOURCE=s;getgenv().__PUCK_DUNGEON_RESUME="..encodeLua(resumeSnapshot())..";loadstring(s)()" local ok=pcall(queue,code) teleportQueued=ok return ok end track(LP.OnTeleport:Connect(function(state) if not S.Alive or not O.ContinueTeleport or teleportQueued then return end if state~=Enum.TeleportState.Started and state~=Enum.TeleportState.InProgress then return end if not queueContinuation() then setStatus("Re-run the script after teleport; continuation unavailable") end end)) track(TeleportService.TeleportInitFailed:Connect(function(player) if player==LP then teleportQueued=false; S.Cooldown.queue=os.clock()+6; setStatus("Teleport failed — retrying shortly") end end)) local function loop(interval,name,fn) job(function() while S.Alive do local start=os.clock() local ok,err=pcall(fn) if not ok then S.Errors[name]=tostring(err) if name=="farming" then -- A bad transient objective must never leave the main loop permanently owned. -- Do not wipe progression/combat state; only release disposable interaction -- state so the next successful farming tick can choose a fresh objective. if S.LockedPrompt and (not S.LockedPrompt.Parent or not S.LockedPrompt.Enabled) then releasePrompt(S.LockedPrompt,true) end S.InteractingUntil=0 end if active() then setStatus("Recovering "..name) end end task.wait(math.max(0.03,interval-(os.clock()-start))) end end) end loop(3,"game data",function() loadModules(); bindEvents() if inRun() then local haveRooms=discoverWorld() if active() then if not haveRooms then if ready("layout_missing",1.1) then call("DungeonRunService","RequestZoneLayout") end elseif ready("layout",20) then call("DungeonRunService","RequestZoneLayout") end end end if not S.DungeonOptionsLoaded and S.Modules.DungeonData then local opts={"Auto"} for _,name in ipairs(dungeonNames()) do table.insert(opts,name) end if controls.Dungeon and controls.Dungeon.Refresh then controls.Dungeon:Refresh(opts); controls.Dungeon:Set(O.Dungeon) end S.DungeonOptionsLoaded=true end end) -- Generated dungeon models can replicate after StateUpdate/ZoneEntered and some maps nest -- Room_n under a container. Keep physical room discovery responsive without touching lobby work. loop(0.45,"room discovery",function() if not inRun() then return end local haveRooms=discoverWorld() if active() and not haveRooms and ready("layout_fast_sync",1.05) then call("DungeonRunService","RequestZoneLayout") end end) loop(0.035,"predictive dodge",dodgeStep) loop(0.1,"combat",attackStep) loop(0.5,"healing",healStep) -- Re-apply noclip every physics step because character states/game scripts can restore -- CanCollide. Original collision values are restored whenever farming/noclip stops. track(RunService.Stepped:Connect(function() if active() and inRun() and O.Noclip then applyCharacterNoclip() elseif S.NoclipApplied then restoreCharacterCollisions() end end)) track(RunService.Heartbeat:Connect(function(dt) if active() then local ok,err=pcall(moveStep,dt); if not ok then S.Errors.Movement=tostring(err) end end end)) loop(0.2,"farming",function() if not active() then return end if not data() then setStatus("Waiting for player data"); return end -- Keep potion confirmation truly asynchronous so combat or room travel never has to wait for it. pollPotionStation(os.clock()) if LP:GetAttribute("InBossRush") or LP:GetAttribute("InChallenge") or LP:GetAttribute("InRaid") then stopMotion(); setStatus("Choose a regular dungeon for autofarming"); return end if handleChoices() then return end if S.PendingEndless and ready("endless",3) then setStatus("Extracting at the endless checkpoint") local ok,a=call("DungeonRunService","SubmitEndlessChoice",false) if accepted(ok,a) then S.PendingEndless=nil end return end if S.Complete then finishRun(); return end if not character() then stopMotion(); setStatus("Waiting to respawn"); return end if not inRun() then if placeRole=="AFK" then stopMotion(); setStatus("AFK subplace — ready to resume after teleport"); return end if os.clock()-S.Boot<3 then return end queueRun(); return end if S.Preparing then stopMotion(); return end navigate() end) loop(12,"equipment",function() if active() and inRun() and not S.Preparing and not S.Complete then Lobby.equipBest() if O.Stats and data() and (tonumber(data().UnspentSkillPoints) or 0)>0 then call("StatService","AutoAllocate") end end end) loop(0.5,"status",function() if statusLabel.Set then statusLabel:Set(O.Enabled and friendlyStatus(S.Status) or "Paused") end local d=data() local summary="Runs "..S.Runs.." • Wins "..S.Wins if d then local gs=0 for _,slot in ipairs({"Head","Body","Ring"}) do gs=gs+Lobby.officialGearScore((d.Equipment or {})[slot]) end local level=tonumber(d.PlayerLevel) if level then summary=summary.." • Level "..math.floor(level) end summary=summary.." • Gear Score "..math.floor(gs) end if statsLabel.Set then statsLabel:Set(summary) end end) -- Explicit teleport resume wins over profile auto-load for the current session. if type(resumed)=="table" then for k,v in pairs(resumed) do if controls[k] and controls[k].Set then pcall(function() controls[k]:Set(v) end) end end end setStatus("Ready") UI:Notify({Title="Dungeon Lootr",Content="Smart Autofarm is ready. Recommended settings are enabled by default; use the Autofarm tab to start.",Duration=6}) ]====] local environment = (getgenv and getgenv()) or _G environment.__PUCK_DUNGEON_SOURCE = source local run, err = loadstring(source) assert(run, err) return run()