diff --git a/Core/Utils.lua b/Core/Utils.lua index 59d535b..0693e38 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -240,6 +240,41 @@ function Utils:IsAmmoQuiverBag(bagID) return false end +-- Check if a bag is Herb Bag +function Utils:IsHerbBag(bagID) + -- Skip backpack, bank, and keyring + if bagID == 0 or bagID == -1 or bagID == -2 then + return false + end + + local invSlot = ContainerIDToInventoryID(bagID) + if not invSlot then return false end + + local link = GetInventoryItemLink("player", invSlot) + if not link then return false end + + -- Prefer specialized type detection + local bagType = self:GetSpecializedBagType(bagID) + if bagType == "herb" then return true end + + -- Fallback: tooltip scan for "Herb Bag" + local tooltip = GetScanTooltip() + tooltip:ClearLines() + tooltip:SetInventoryItem("player", invSlot) + + for i = 1, tooltip:NumLines() do + local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + if line then + local text = line:GetText() + if text and string.find(string.lower(text), "herb bag") then + return true + end + end + end + + return false +end + -- Check if a bag is Soul Bag function Utils:IsSoulBag(bagID) -- Skip backpack, bank, and keyring @@ -289,7 +324,7 @@ function Utils:IsSoulBag(bagID) end -- Get specialized bag type (for sorting priority) --- Returns: "soul", "quiver", "ammo", or nil +-- Returns: "soul", "herb", "quiver", "ammo", or nil function Utils:GetSpecializedBagType(bagID) -- Skip backpack, bank, and keyring if bagID == 0 or bagID == -1 or bagID == -2 then @@ -322,6 +357,11 @@ function Utils:GetSpecializedBagType(bagID) return "soul" end + -- Herb Bag + if string.find(typeLower, "herb bag") then + return "herb" + end + -- Quiver if string.find(typeLower, "quiver") then return "quiver" @@ -341,6 +381,8 @@ function Utils:GetContainerPriority(bagID) local bagType = self:GetSpecializedBagType(bagID) if bagType == "soul" then return 40 + elseif bagType == "herb" then + return 35 elseif bagType == "quiver" then return 30 elseif bagType == "ammo" then @@ -381,32 +423,37 @@ function Utils:IsAmmo(itemType) end -- Get preferred container type for an item --- Returns: "soul", "quiver", "ammo", or nil +-- Returns: "soul", "herb", "quiver", "ammo", or nil function Utils:GetItemPreferredContainer(itemLink) - if not itemLink then return nil end + if not itemLink then return nil end - -- Check for soul shards first - if self:IsSoulShard(itemLink) then - return "soul" - end + -- Check for soul shards first + if self:IsSoulShard(itemLink) then + return "soul" + end - -- Get item info using utility function - local itemID = self:ExtractItemID(itemLink) - if not itemID then return nil end + -- Get item info using utility function + local itemID = self:ExtractItemID(itemLink) + if not itemID then return nil end - local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType = self:GetItemInfoSafe(itemID) - if not itemType then return nil end + local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture = self:GetItemInfoSafe(itemID) + if not itemType then return nil end - -- Only route PROJECTILE category items that are specifically arrows or bullets - if itemCategory == "Projectile" then - if itemType == "Arrow" then - return "quiver" - elseif itemType == "Bullet" then - return "ammo" - end - end + -- Only route PROJECTILE category items that are specifically arrows or bullets + if itemCategory == "Projectile" then + if itemType == "Arrow" then + return "quiver" + elseif itemType == "Bullet" then + return "ammo" + end + end - return nil + -- Route herbs to herb bags (robust: category/subtype OR texture pattern fallback) + if self:IsHerbItem(itemLink) then + return "herb" + end + + return nil end -- Check if an item is equipment (armor, weapon, or other equippable) @@ -432,5 +479,36 @@ function Utils:IsEquipment(itemLink) end end - return false + return false +end + +-- Determine if an item is a herb (for routing to herb bags) +-- NEW rule (per request): require BOTH +-- 1) itemCategory == "Trade Goods" +-- 2) texture contains "INV_Misc_Herb" (case-insensitive; prefix tolerated) +function Utils:IsHerbItem(itemLink) + if not itemLink then return false end + + local itemID = self:ExtractItemID(itemLink) + if not itemID then return false end + + local name, _, quality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture = self:GetItemInfoSafe(itemID) + + if itemCategory ~= "Trade Goods" then + return false + end + + if not itemTexture then + return false + end + + -- Normalize and check texture name + local tex = string.lower(itemTexture) + tex = string.gsub(tex, "^interface\\\\icons\\\\", "") + + if string.find(tex, "inv_misc_herb") or string.find(tex, "misc_herb") then + return true + end + + return false end \ No newline at end of file diff --git a/Data/BagScanner.lua b/Data/BagScanner.lua index 05cc15c..5ccebd4 100644 --- a/Data/BagScanner.lua +++ b/Data/BagScanner.lua @@ -27,6 +27,8 @@ function BagScanner:ScanBag(bagID) local bagType = "regular" if addon.Modules.Utils:IsSoulBag(bagID) then bagType = "soul" + elseif addon.Modules.Utils:IsHerbBag(bagID) then + bagType = "herb" elseif addon.Modules.Utils:IsAmmoQuiverBag(bagID) then bagType = "ammo" end diff --git a/Data/BankScanner.lua b/Data/BankScanner.lua index 16d0959..904d77d 100644 --- a/Data/BankScanner.lua +++ b/Data/BankScanner.lua @@ -30,6 +30,8 @@ function BankScanner:ScanBankBag(bagID) local bagType = "regular" if addon.Modules.Utils:IsSoulBag(bagID) then bagType = "soul" + elseif addon.Modules.Utils:IsHerbBag(bagID) then + bagType = "herb" elseif addon.Modules.Utils:IsAmmoQuiverBag(bagID) then bagType = "ammo" end diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 2767d22..86bc6d0 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -172,25 +172,28 @@ end --=========================================================================== local function DetectSpecializedBags(bagIDs) - local containers = { - soul = {}, - quiver = {}, - ammo = {}, - regular = {} - } + local containers = { + soul = {}, + herb = {}, + quiver = {}, + ammo = {}, + regular = {} + } for _, bagID in ipairs(bagIDs) do local bagType = addon.Modules.Utils:GetSpecializedBagType(bagID) - if bagType == "soul" then - table.insert(containers.soul, bagID) - elseif bagType == "quiver" then - table.insert(containers.quiver, bagID) - elseif bagType == "ammo" then - table.insert(containers.ammo, bagID) - else - table.insert(containers.regular, bagID) - end - end + if bagType == "soul" then + table.insert(containers.soul, bagID) + elseif bagType == "herb" then + table.insert(containers.herb, bagID) + elseif bagType == "quiver" then + table.insert(containers.quiver, bagID) + elseif bagType == "ammo" then + table.insert(containers.ammo, bagID) + else + table.insert(containers.regular, bagID) + end + end return containers end @@ -749,13 +752,13 @@ function SortEngine:AnalyzeContainer(bagIDs, containerType) local totalOutOfPlace = 0 local totalItems = 0 - -- Analyze specialized bags separately - for _, bagType in ipairs({"soul", "quiver", "ammo"}) do - local specialBags = containers[bagType] - for _, bagID in ipairs(specialBags) do - local items = CollectItems({bagID}) - if table.getn(items) > 0 then - local sortedItems = SortItems(items) + -- Analyze specialized bags separately + for _, bagType in ipairs({"soul", "herb", "quiver", "ammo"}) do + local specialBags = containers[bagType] + for _, bagID in ipairs(specialBags) do + local items = CollectItems({bagID}) + if table.getn(items) > 0 then + local sortedItems = SortItems(items) local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) for i, item in ipairs(sortedItems) do @@ -827,23 +830,27 @@ function SortEngine:AnalyzeContainer(bagIDs, containerType) end end - if totalOutOfPlace == 0 then - return {passes = 0, itemsOutOfPlace = 0, totalItems = totalItems, alreadySorted = true} - end + if totalOutOfPlace == 0 then + return {passes = 0, itemsOutOfPlace = 0, totalItems = totalItems, alreadySorted = true} + end - -- Estimate passes (same logic as before) - local displacementRatio = totalOutOfPlace / math.max(1, totalItems) - local estimatedPasses + -- Estimate passes (aligned to single-pass executor; cap at 6) + local displacementRatio = totalOutOfPlace / math.max(1, totalItems) + local estimatedPasses - if displacementRatio < 0.1 then - estimatedPasses = 1 - elseif displacementRatio < 0.3 then - estimatedPasses = 2 - elseif displacementRatio < 0.5 then - estimatedPasses = 3 - else - estimatedPasses = 4 - end + if displacementRatio < 0.10 then + estimatedPasses = 1 + elseif displacementRatio < 0.30 then + estimatedPasses = 2 + elseif displacementRatio < 0.50 then + estimatedPasses = 3 + elseif displacementRatio < 0.70 then + estimatedPasses = 4 + elseif displacementRatio < 0.85 then + estimatedPasses = 5 + else + estimatedPasses = 6 + end return { passes = estimatedPasses, @@ -853,6 +860,61 @@ function SortEngine:AnalyzeContainer(bagIDs, containerType) } end +-- Execute exactly ONE full sorting pass over bags (used by safety wrapper) +function SortEngine:SortBagsPass() + local bagIDs = addon.Constants.BAGS + + -- Phase 1: Detect specialized bags + local containers = DetectSpecializedBags(bagIDs) + + -- Phase 2: Route specialized items to their bags + local routeCount = RouteSpecializedItems(bagIDs, containers) + + -- Phase 3: Consolidate stacks in ALL bags (including specialized) + local consolidateCount = ConsolidateStacks(bagIDs) + + -- Phase 4: Sort items WITHIN each specialized bag (soul, herb, quiver, ammo) + local specializedMoves = 0 + for _, bagType in ipairs({"soul", "herb", "quiver", "ammo"}) do + local specialBags = containers[bagType] + for _, bagID in ipairs(specialBags) do + local items = CollectItems({bagID}) + if table.getn(items) > 0 then + items = SortItems(items) + local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) + local moveCount = ApplySort({bagID}, items, targetPositions) + specializedMoves = specializedMoves + moveCount + end + end + end + + -- Phase 5: Two-phase sort for regular bags in a single pass + local regularMoves = 0 + local regularBagIDs = containers.regular + if table.getn(regularBagIDs) > 0 then + -- Re-collect current state from regular bags + local allItems = CollectItems(regularBagIDs) + local nonGreys, greys = SplitGreyItems(allItems) + + -- 1) Non-greys: standard sort to the front positions only + if table.getn(nonGreys) > 0 then + local sortedNonGreys = SortItems(nonGreys) + local frontPositions = BuildTargetPositions(regularBagIDs, table.getn(sortedNonGreys)) + regularMoves = regularMoves + (ApplySort(regularBagIDs, sortedNonGreys, frontPositions) or 0) + end + + -- 2) Greys: end->start across bags + local afterItems = CollectItems(regularBagIDs) + local _, greysNow = SplitGreyItems(afterItems) + if table.getn(greysNow) > 0 then + local tailPositions = BuildGreyTailPositions(regularBagIDs, table.getn(greysNow)) + regularMoves = regularMoves + (ApplySort(regularBagIDs, greysNow, tailPositions) or 0) + end + end + + return routeCount + consolidateCount + specializedMoves + regularMoves +end + function SortEngine:SortBags() local bagIDs = addon.Constants.BAGS @@ -865,14 +927,14 @@ function SortEngine:SortBags() -- Phase 3: Consolidate stacks in ALL bags (including specialized) local consolidateCount = ConsolidateStacks(bagIDs) - -- Phase 4: Sort items WITHIN each specialized bag (soul, quiver, ammo) - local specializedMoves = 0 - for _, bagType in ipairs({"soul", "quiver", "ammo"}) do - local specialBags = containers[bagType] - for _, bagID in ipairs(specialBags) do - -- Sort items within this single specialized bag - local items = CollectItems({bagID}) - if table.getn(items) > 0 then + -- Phase 4: Sort items WITHIN each specialized bag (soul, herb, quiver, ammo) + local specializedMoves = 0 + for _, bagType in ipairs({"soul", "herb", "quiver", "ammo"}) do + local specialBags = containers[bagType] + for _, bagID in ipairs(specialBags) do + -- Sort items within this single specialized bag + local items = CollectItems({bagID}) + if table.getn(items) > 0 then items = SortItems(items) local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) local moveCount = ApplySort({bagID}, items, targetPositions) @@ -881,9 +943,7 @@ function SortEngine:SortBags() end end - -- Phase 5: Two-phase sort for regular bags: - -- 1) Sort non-grey items normally across all regular bags (front-compaction) - -- 2) Pack grey items (quality 0) at the end tail (last bag backwards), spilling into previous bags. + -- Phase 5: Two-phase sort for regular bags (multi-pass legacy; kept for direct calls) local regularMoves = 0 local regularBagIDs = containers.regular if table.getn(regularBagIDs) > 0 then @@ -920,6 +980,63 @@ function SortEngine:SortBags() return routeCount + consolidateCount + specializedMoves + regularMoves end +-- Execute exactly ONE full sorting pass over bank (used by safety wrapper) +function SortEngine:SortBankPass() + if not addon.Modules.BankScanner:IsBankOpen() then + addon:Print("Bank must be open to sort!") + return 0 + end + + local bagIDs = addon.Constants.BANK_BAGS + + -- Phase 1: Detect specialized bags + local containers = DetectSpecializedBags(bagIDs) + + -- Phase 2: Route specialized items + local routeCount = RouteSpecializedItems(bagIDs, containers) + + -- Phase 3: Consolidate stacks + local consolidateCount = ConsolidateStacks(bagIDs) + + -- Phase 4: Sort items WITHIN each specialized bag (single pass) + local specializedMoves = 0 + for _, bagType in ipairs({"soul", "herb", "quiver", "ammo"}) do + local specialBags = containers[bagType] + for _, bagID in ipairs(specialBags) do + local items = CollectItems({bagID}) + if table.getn(items) > 0 then + items = SortItems(items) + local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) + local moved = ApplySort({bagID}, items, targetPositions) + specializedMoves = specializedMoves + moved + end + end + end + + -- Phase 5: Regular bank bags — single pass + local regularBagIDs = containers.regular + local regularMoves = 0 + if table.getn(regularBagIDs) > 0 then + local allItems = CollectItems(regularBagIDs) + local nonGreys, greys = SplitGreyItems(allItems) + + if table.getn(nonGreys) > 0 then + local sortedNonGreys = SortItems(nonGreys) + local frontPositions = BuildTargetPositions(regularBagIDs, table.getn(sortedNonGreys)) + regularMoves = regularMoves + (ApplySort(regularBagIDs, sortedNonGreys, frontPositions) or 0) + end + + local afterItems = CollectItems(regularBagIDs) + local _, greysNow = SplitGreyItems(afterItems) + if table.getn(greysNow) > 0 then + local tailPositions = BuildGreyTailPositions(regularBagIDs, table.getn(greysNow)) + regularMoves = regularMoves + (ApplySort(regularBagIDs, greysNow, tailPositions) or 0) + end + end + + return routeCount + consolidateCount + specializedMoves + regularMoves +end + function SortEngine:SortBank() if not addon.Modules.BankScanner:IsBankOpen() then addon:Print("Bank must be open to sort!") @@ -937,23 +1054,23 @@ function SortEngine:SortBank() -- Phase 3: Consolidate stacks local consolidateCount = ConsolidateStacks(bagIDs) - -- Phase 4: Sort items WITHIN each specialized bag (multi-pass to avoid mid-bag holes) - local specializedMoves = 0 - for _, bagType in ipairs({"soul", "quiver", "ammo"}) do - local specialBags = containers[bagType] - for _, bagID in ipairs(specialBags) do - local maxPasses = 4 - for pass = 1, maxPasses do - local items = CollectItems({bagID}) - if table.getn(items) == 0 then break end - items = SortItems(items) - local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) - local moved = ApplySort({bagID}, items, targetPositions) - specializedMoves = specializedMoves + moved - if moved == 0 then break end - end - end - end + -- Phase 4: Sort items WITHIN each specialized bag (multi-pass to avoid mid-bag holes) + local specializedMoves = 0 + for _, bagType in ipairs({"soul", "herb", "quiver", "ammo"}) do + local specialBags = containers[bagType] + for _, bagID in ipairs(specialBags) do + local maxPasses = 4 + for pass = 1, maxPasses do + local items = CollectItems({bagID}) + if table.getn(items) == 0 then break end + items = SortItems(items) + local targetPositions = BuildTargetPositions({bagID}, table.getn(items)) + local moved = ApplySort({bagID}, items, targetPositions) + specializedMoves = specializedMoves + moved + if moved == 0 then break end + end + end + end -- Phase 5: Regular bank bags — same two-phase approach (non-greys first, greys to tail) local regularBagIDs = containers.regular @@ -1005,26 +1122,27 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort addon:Print("Sorting %s... (%d/%d items need sorting, estimated %d passes)", sortType, analysis.itemsOutOfPlace, analysis.totalItems, analysis.passes) - local passCount = 0 - local maxPasses = math.max(analysis.passes, 1) -- Use estimated passes, minimum 1 - local safetyLimit = math.max(maxPasses * 2, 6) -- Reasonable upper bound - local totalMoves = 0 + local passCount = 0 + local maxPasses = math.max(analysis.passes, 1) -- Use estimated passes, minimum 1 + local safetyLimit = math.max(maxPasses * 2, 6) -- Reasonable upper bound + local totalMoves = 0 + local noProgressPasses = 0 addon:Print("Starting %s sort (estimated: %d passes, safety limit: %d)", sortType, maxPasses, safetyLimit) local function DoSortPass() passCount = passCount + 1 - -- Perform one sort pass - local moveCount = sortFunction() - totalMoves = totalMoves + moveCount + -- Perform one sort pass + local moveCount = sortFunction() + totalMoves = totalMoves + moveCount -- Check if sorting is complete by re-analyzing local currentAnalysis = analyzeFunction() - if currentAnalysis.alreadySorted then - -- Sorting is complete! - addon:Print("%s sort complete! (%d passes, %d total moves)", sortType, passCount, totalMoves) + if currentAnalysis.alreadySorted then + -- Sorting is complete! + addon:Print("%s sort complete! (%d passes, %d total moves)", sortType, passCount, totalMoves) -- Final update local frame = CreateFrame("Frame") @@ -1035,10 +1153,10 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort updateFrame() end end) - elseif passCount >= safetyLimit then - -- Hit safety limit but not fully sorted - addon:Print("%s sort stopped at safety limit! (%d/%d items still need sorting after %d passes)", - sortType, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, passCount) + elseif passCount >= safetyLimit then + -- Hit safety limit but not fully sorted + addon:Print("%s sort stopped at safety limit! (%d/%d items still need sorting after %d passes)", + sortType, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, passCount) -- Final update local frame = CreateFrame("Frame") @@ -1049,11 +1167,33 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort updateFrame() end end) - else - -- More sorting needed - local remainingRatio = currentAnalysis.itemsOutOfPlace / math.max(1, currentAnalysis.totalItems) - addon:Print("%s Pass %d: %d moves, %d/%d items remaining (%.1f%%)", - sortType, passCount, moveCount, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, remainingRatio * 100) + else + -- No progress guard: stop if we make no moves repeatedly + if moveCount == 0 then + noProgressPasses = noProgressPasses + 1 + else + noProgressPasses = 0 + end + + if noProgressPasses >= 3 then + addon:Print("%s sort stopped due to no progress after %d passes (items remaining: %d/%d)", + sortType, passCount, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems) + -- Final update + local frame = CreateFrame("Frame") + local startTime = GetTime() + frame:SetScript("OnUpdate", function() + if GetTime() - startTime >= 0.7 then + frame:SetScript("OnUpdate", nil) + updateFrame() + end + end) + return + end + + -- More sorting needed + local remainingRatio = currentAnalysis.itemsOutOfPlace / math.max(1, currentAnalysis.totalItems) + addon:Print("%s Pass %d: %d moves, %d/%d items remaining (%.1f%%)", + sortType, passCount, moveCount, currentAnalysis.itemsOutOfPlace, currentAnalysis.totalItems, remainingRatio * 100) -- PROGRESSIVE DELAY: Calculate delay based on remaining complexity local baseDelay = 0.7 diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 9c8467d..a615106 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -236,10 +236,11 @@ function BagFrame:DisplayItems(bagData, isOtherChar, charName) local perRow = addon.Modules.DB:GetSetting("bagColumns") or 10 local itemContainer = getglobal("Guda_BagFrame_ItemContainer") - -- Separate bags into regular, soul, and ammo/quiver types - local regularBags = {} - local soulBags = {} - local ammoQuiverBags = {} + -- Separate bags into regular, soul, herb, and ammo/quiver types + local regularBags = {} + local soulBags = {} + local herbBags = {} + local ammoQuiverBags = {} for _, bagID in ipairs(addon.Constants.BAGS) do -- Skip hidden bags @@ -251,44 +252,55 @@ function BagFrame:DisplayItems(bagData, isOtherChar, charName) bagType = bag and bag.bagType or "regular" else -- For current character, detect bag type in real-time - if addon.Modules.Utils:IsSoulBag(bagID) then - bagType = "soul" - elseif addon.Modules.Utils:IsAmmoQuiverBag(bagID) then - bagType = "ammo" - else - bagType = "regular" - end - end + if addon.Modules.Utils:IsSoulBag(bagID) then + bagType = "soul" + elseif addon.Modules.Utils:IsHerbBag(bagID) then + bagType = "herb" + elseif addon.Modules.Utils:IsAmmoQuiverBag(bagID) then + bagType = "ammo" + else + bagType = "regular" + end + end - if bagType == "soul" then - table.insert(soulBags, bagID) - elseif bagType == "ammo" then - table.insert(ammoQuiverBags, bagID) - else - table.insert(regularBags, bagID) - end - end - end + if bagType == "soul" then + table.insert(soulBags, bagID) + elseif bagType == "herb" then + table.insert(herbBags, bagID) + elseif bagType == "ammo" then + table.insert(ammoQuiverBags, bagID) + else + table.insert(regularBags, bagID) + end + end + end - -- Build display order: regular bags -> soul bags -> ammo/quiver bags -> keyring - local bagsToShow = {} - for _, bagID in ipairs(regularBags) do - table.insert(bagsToShow, {bagID = bagID, needsSpacing = false}) - end + -- Build display order: regular bags -> soul bags -> herb bags -> ammo/quiver bags -> keyring + local bagsToShow = {} + for _, bagID in ipairs(regularBags) do + table.insert(bagsToShow, {bagID = bagID, needsSpacing = false}) + end -- Add soul bags with spacing marker - if table.getn(soulBags) > 0 then - for i, bagID in ipairs(soulBags) do - table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)}) - end - end + if table.getn(soulBags) > 0 then + for i, bagID in ipairs(soulBags) do + table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)}) + end + end - -- Add ammo/quiver bags with spacing marker - if table.getn(ammoQuiverBags) > 0 then - for i, bagID in ipairs(ammoQuiverBags) do - table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)}) - end - end + -- Add herb bags with spacing marker + if table.getn(herbBags) > 0 then + for i, bagID in ipairs(herbBags) do + table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)}) + end + end + + -- Add ammo/quiver bags with spacing marker + if table.getn(ammoQuiverBags) > 0 then + for i, bagID in ipairs(ammoQuiverBags) do + table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)}) + end + end -- Add keyring at the end if toggled on and not hidden if showKeyring and not hiddenBags[-2] then @@ -299,8 +311,8 @@ function BagFrame:DisplayItems(bagData, isOtherChar, charName) local bagID = bagInfo.bagID local bag = bagData[bagID] - -- Add spacing before soul, ammo/quiver, or keyring sections - if bagInfo.needsSpacing then + -- Add spacing before soul, herb, ammo/quiver, or keyring sections + if bagInfo.needsSpacing then if col > 0 then -- Move to next row if not at start of row col = 0 @@ -1167,12 +1179,12 @@ function Guda_BagFrame_Sort() return end - local success, message = addon.Modules.SortEngine:ExecuteSort( - function() return addon.Modules.SortEngine:SortBags() end, - function() return addon.Modules.SortEngine:AnalyzeBags() end, - function() BagFrame:Update() end, - "bags" - ) + local success, message = addon.Modules.SortEngine:ExecuteSort( + function() return addon.Modules.SortEngine:SortBagsPass() end, + function() return addon.Modules.SortEngine:AnalyzeBags() end, + function() BagFrame:Update() end, + "bags" + ) if not success and message == "already sorted" then addon:Print("Bags are already sorted!") diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index b8c15c7..688cd91 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -196,28 +196,90 @@ function BankFrame:DisplayItems(bankData, isOtherChar, charName) local perRow = addon.Modules.DB:GetSetting("bankColumns") or 10 local itemContainer = getglobal("Guda_BankFrame_ItemContainer") - for _, bagID in ipairs(addon.Constants.BANK_BAGS) do - -- Skip bags that are hidden - if not hiddenBankBags[bagID] then - local bag = bankData[bagID] + -- Separate bank bags into regular, soul, herb, and ammo/quiver types + local regularBags = {} + local soulBags = {} + local herbBags = {} + local ammoQuiverBags = {} - -- Get slot count for this bag - local numSlots - if isOtherChar and bag and bag.numSlots then - -- Use stored slot count for other characters - numSlots = bag.numSlots + for _, bagID in ipairs(addon.Constants.BANK_BAGS) do + if not hiddenBankBags[bagID] then + local bagType + if isOtherChar then + -- Use saved bag type for other characters + local bagSaved = bankData and bankData[bagID] + bagType = bagSaved and bagSaved.bagType or "regular" else - -- Use current character's bag slot count - numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) + -- Live detection for current character when bank is open + if addon.Modules.Utils:IsSoulBag(bagID) then + bagType = "soul" + elseif addon.Modules.Utils:IsHerbBag(bagID) then + bagType = "herb" + elseif addon.Modules.Utils:IsAmmoQuiverBag(bagID) then + bagType = "ammo" + else + bagType = "regular" + end end - -- Only show bags that have slots - if numSlots and numSlots > 0 then - -- Iterate through ALL slots (1 to numSlots) to show empty slots too + if bagType == "soul" then + table.insert(soulBags, bagID) + elseif bagType == "herb" then + table.insert(herbBags, bagID) + elseif bagType == "ammo" then + table.insert(ammoQuiverBags, bagID) + else + table.insert(regularBags, bagID) + end + end + end + + -- Build display order: regular -> soul -> herb -> ammo/quiver + local bagsToShow = {} + for _, bagID in ipairs(regularBags) do + table.insert(bagsToShow, { bagID = bagID, needsSpacing = false }) + end + if table.getn(soulBags) > 0 then + for i, bagID in ipairs(soulBags) do + table.insert(bagsToShow, { bagID = bagID, needsSpacing = (i == 1) }) + end + end + if table.getn(herbBags) > 0 then + for i, bagID in ipairs(herbBags) do + table.insert(bagsToShow, { bagID = bagID, needsSpacing = (i == 1) }) + end + end + if table.getn(ammoQuiverBags) > 0 then + for i, bagID in ipairs(ammoQuiverBags) do + table.insert(bagsToShow, { bagID = bagID, needsSpacing = (i == 1) }) + end + end + + for _, bagInfo in ipairs(bagsToShow) do + local bagID = bagInfo.bagID + local bag = bankData and bankData[bagID] + + -- Add spacing before first of each specialized section + if bagInfo.needsSpacing then + if col > 0 then + col = 0 + row = row + 1 + end + row = row + 0.5 + end + + -- Get slot count for this bag + local numSlots + if isOtherChar and bag and bag.numSlots then + numSlots = bag.numSlots + else + numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) + end + + if numSlots and numSlots > 0 then for slot = 1, numSlots do local itemData = bag and bag.slots and bag.slots[slot] or nil - -- Check if item matches search filter local matchesFilter = self:PassesSearchFilter(itemData) -- Ensure a per-bag parent frame exists and carries the bag ID @@ -232,33 +294,24 @@ function BankFrame:DisplayItems(bankData, isOtherChar, charName) bankBagParent = bankBagParents[bagID] local button = Guda_GetItemButton(bankBagParent) + if button.isBagSlot then break end - -- Ensure this is NOT a bag slot button - if button.isBagSlot then - break - end + button.inUse = true - button.inUse = true -- Mark this button as actively in use - - -- Position button local xPos = x + (col * (buttonSize + spacing)) local yPos = y - (row * (buttonSize + spacing)) button:ClearAllPoints() button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", xPos, yPos) - -- Set item data with filter match info - -- Pass isReadOnlyMode to disable interaction for saved banks Guda_ItemButton_SetItem(button, bagID, slot, itemData, true, isOtherChar and charName or nil, matchesFilter, isReadOnlyMode) - -- Advance position col = col + 1 if col >= perRow then col = 0 row = row + 1 end end - end end end @@ -608,12 +661,12 @@ function Guda_BankFrame_Sort() return end - local success, message = addon.Modules.SortEngine:ExecuteSort( - function() return addon.Modules.SortEngine:SortBank() end, - function() return addon.Modules.SortEngine:AnalyzeBank() end, - function() BankFrame:Update() end, - "bank" - ) + local success, message = addon.Modules.SortEngine:ExecuteSort( + function() return addon.Modules.SortEngine:SortBankPass() end, + function() return addon.Modules.SortEngine:AnalyzeBank() end, + function() BankFrame:Update() end, + "bank" + ) if not success and message == "already sorted" then addon:Print("Bank is already sorted!") diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index e8f7c30..04de837 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -876,6 +876,31 @@ function Guda_ItemButton_OnEnter(self) GameTooltip:SetBagItem(self.bagID, self.slotID) end GameTooltip:Show() + + -- Debug: print hovered item's texture path to chat + -- Uses GetItemInfo on the hovered item's ID + --if self.hasItem then + -- local link = nil + -- if self.itemData and self.itemData.link then + -- link = self.itemData.link + -- else + -- -- Fallback to live bag query + -- link = GetContainerItemLink(self.bagID, self.slotID) + -- end + -- if link and addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.ExtractItemID then + -- local itemID = addon.Modules.Utils:ExtractItemID(link) + -- if itemID and addon.Modules.Utils.GetItemInfoSafe then + -- local name, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, + -- itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfoSafe(itemID) + -- if addon and addon.Print then + -- addon:Print("itemTexture: %s", tostring(itemTexture)) + -- addon:Print("itemCategory: %s", tostring(itemCategory)) + -- addon:Print("itemType: %s", tostring(itemType)) + -- addon:Print("itemType: %s", tostring(itemType)) + -- end + -- end + -- end + --end -- Handle merchant sell cursor (same approach as BagShui) if MerchantFrame:IsShown() and not self.isBank and not self.otherChar and self.hasItem then ShowContainerSellCursor(self.bagID, self.slotID)