From 29a68d4853ad7073295cebc67db21f9a36a8c6b8 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 13:11:12 +0400 Subject: [PATCH 01/10] feat: adding categories for Guda Bags --- Core/CategoryManager.lua | 686 +++++++++++++++++++++++++++++++++++++++ Core/Database.lua | 5 + Guda.toc | 1 + UI/FrameHelpers.lua | 63 +++- UI/SettingsPopup.lua | 236 ++++++++++++++ UI/SettingsPopup.xml | 128 +++++++- 6 files changed, 1102 insertions(+), 17 deletions(-) create mode 100644 Core/CategoryManager.lua diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua new file mode 100644 index 0000000..f43ce18 --- /dev/null +++ b/Core/CategoryManager.lua @@ -0,0 +1,686 @@ +-- Guda Category Manager +-- Handles custom category definitions and rule-based item categorization + +local addon = Guda + +local CategoryManager = {} +addon.Modules.CategoryManager = CategoryManager + +-- Rule Types: +-- itemType: Match by GetItemInfo type (Armor, Weapon, Consumable, etc.) +-- itemSubtype: Match by subtype (Cloth, Potion, Herb, etc.) +-- namePattern: Lua pattern match on item name +-- quality: Match by quality level (0=Gray, 1=White, 2=Green, 3=Blue, 4=Purple, 5=Orange) +-- isBoE: Boolean for Bind on Equip items +-- isQuestItem: Boolean for quest items +-- texturePattern: Match icon texture path +-- itemID: Specific item IDs (table of IDs) + +-- Default category definitions that replicate the existing hardcoded behavior +local DEFAULT_CATEGORIES = { + order = { + "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", + "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", + "Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" + }, + definitions = { + ["BoE"] = { + name = "BoE", + icon = "Interface\\Icons\\INV_Misc_Orb_01", + rules = { + { type = "isBoE", value = true } + }, + matchMode = "all", + priority = 60, + enabled = true, + isBuiltIn = true, + }, + ["Weapon"] = { + name = "Weapon", + icon = "Interface\\Icons\\INV_Sword_04", + rules = { + { type = "itemType", value = "Weapon" } + }, + matchMode = "all", + priority = 70, + enabled = true, + isBuiltIn = true, + }, + ["Armor"] = { + name = "Armor", + icon = "Interface\\Icons\\INV_Chest_Chain", + rules = { + { type = "itemType", value = "Armor" } + }, + matchMode = "all", + priority = 70, + enabled = true, + isBuiltIn = true, + }, + ["Consumable"] = { + name = "Consumable", + icon = "Interface\\Icons\\INV_Potion_54", + rules = { + { type = "itemType", value = "Consumable" } + }, + matchMode = "all", + priority = 50, + enabled = true, + isBuiltIn = true, + }, + ["Food"] = { + name = "Food", + icon = "Interface\\Icons\\INV_Misc_Food_14", + rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "eat" } + }, + matchMode = "all", + priority = 55, + enabled = true, + isBuiltIn = true, + }, + ["Drink"] = { + name = "Drink", + icon = "Interface\\Icons\\INV_Drink_07", + rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "drink" } + }, + matchMode = "all", + priority = 55, + enabled = true, + isBuiltIn = true, + }, + ["Trade Goods"] = { + name = "Trade Goods", + icon = "Interface\\Icons\\INV_Fabric_Silk_02", + rules = { + { type = "itemType", value = "Trade Goods" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Reagent"] = { + name = "Reagent", + icon = "Interface\\Icons\\INV_Misc_Dust_02", + rules = { + { type = "itemType", value = "Reagent" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Recipe"] = { + name = "Recipe", + icon = "Interface\\Icons\\INV_Scroll_03", + rules = { + { type = "itemType", value = "Recipe" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Quiver"] = { + name = "Quiver", + icon = "Interface\\Icons\\INV_Misc_Quiver_03", + rules = { + { type = "itemType", value = "Quiver" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Container"] = { + name = "Container", + icon = "Interface\\Icons\\INV_Misc_Bag_07", + rules = { + { type = "itemType", value = "Container" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Soul Bag"] = { + name = "Soul Bag", + icon = "Interface\\Icons\\INV_Misc_Bag_EnchantedMageweave", + rules = { + { type = "itemSubtype", value = "Soul Bag" } + }, + matchMode = "all", + priority = 45, + enabled = true, + isBuiltIn = true, + }, + ["Miscellaneous"] = { + name = "Miscellaneous", + icon = "Interface\\Icons\\INV_Misc_Rune_01", + rules = {}, + matchMode = "any", + priority = 0, + enabled = true, + isBuiltIn = true, + isFallback = true, + }, + ["Quest"] = { + name = "Quest", + icon = "Interface\\Icons\\INV_Misc_Book_08", + rules = { + { type = "isQuestItem", value = true } + }, + matchMode = "all", + priority = 80, + enabled = true, + isBuiltIn = true, + }, + ["Junk"] = { + name = "Junk", + icon = "Interface\\Icons\\INV_Misc_Gear_06", + rules = { + { type = "quality", value = 0 } + }, + matchMode = "all", + priority = 85, + enabled = true, + isBuiltIn = true, + }, + ["Class Items"] = { + name = "Class Items", + icon = "Interface\\Icons\\INV_Misc_Ammo_Arrow_01", + rules = { + { type = "itemType", value = "Projectile" } + }, + matchMode = "any", + priority = 90, + enabled = true, + isBuiltIn = true, + }, + ["Keyring"] = { + name = "Keyring", + icon = "Interface\\Icons\\INV_Misc_Key_04", + rules = { + { type = "itemType", value = "Key" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + } +} + +-- Deep copy a table +local function deepCopy(orig) + local copy + if type(orig) == "table" then + copy = {} + for k, v in pairs(orig) do + copy[k] = deepCopy(v) + end + else + copy = orig + end + return copy +end + +-- Get default categories (returns a deep copy) +function CategoryManager:GetDefaultCategories() + return deepCopy(DEFAULT_CATEGORIES) +end + +-- Initialize categories from database or defaults +function CategoryManager:Initialize() + if not Guda_CharDB then return end + + if not Guda_CharDB.categories then + Guda_CharDB.categories = self:GetDefaultCategories() + addon:Debug("CategoryManager: Initialized with default categories") + else + -- Ensure all built-in categories exist (migration support) + self:MigrateCategories() + end +end + +-- Migrate/update categories to ensure all built-ins exist +function CategoryManager:MigrateCategories() + local cats = Guda_CharDB.categories + if not cats then return end + + -- Ensure definitions table exists + if not cats.definitions then + cats.definitions = {} + end + + -- Ensure order table exists + if not cats.order then + cats.order = {} + end + + -- Add any missing built-in categories + for id, def in pairs(DEFAULT_CATEGORIES.definitions) do + if not cats.definitions[id] then + cats.definitions[id] = deepCopy(def) + -- Add to end of order if not present + local found = false + for _, orderId in ipairs(cats.order) do + if orderId == id then + found = true + break + end + end + if not found then + table.insert(cats.order, id) + end + addon:Debug("CategoryManager: Added missing built-in category: " .. id) + end + end + + -- Migrate Food/Drink categories to use restoreTag instead of itemSubtype + -- This fixes the issue where subtype "Food & Drink" matched both categories + local foodCat = cats.definitions["Food"] + if foodCat and foodCat.isBuiltIn then + local needsUpdate = false + if foodCat.rules then + for _, rule in ipairs(foodCat.rules) do + if rule.type == "itemSubtype" then + needsUpdate = true + break + end + end + end + if needsUpdate then + foodCat.rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "eat" } + } + addon:Debug("CategoryManager: Migrated Food category to use restoreTag") + end + end + + local drinkCat = cats.definitions["Drink"] + if drinkCat and drinkCat.isBuiltIn then + local needsUpdate = false + if drinkCat.rules then + for _, rule in ipairs(drinkCat.rules) do + if rule.type == "itemSubtype" then + needsUpdate = true + break + end + end + end + if needsUpdate then + drinkCat.rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "drink" } + } + addon:Debug("CategoryManager: Migrated Drink category to use restoreTag") + end + end +end + +-- Get all categories +function CategoryManager:GetCategories() + if not Guda_CharDB or not Guda_CharDB.categories then + return self:GetDefaultCategories() + end + return Guda_CharDB.categories +end + +-- Get category order +function CategoryManager:GetCategoryOrder() + local cats = self:GetCategories() + return cats.order or {} +end + +-- Get category definition by ID +function CategoryManager:GetCategory(categoryId) + local cats = self:GetCategories() + if cats.definitions then + return cats.definitions[categoryId] + end + return nil +end + +-- Save categories to database +function CategoryManager:SaveCategories(categories) + if not Guda_CharDB then return end + Guda_CharDB.categories = categories +end + +-- Add a new custom category +function CategoryManager:AddCategory(categoryId, definition) + local cats = self:GetCategories() + + if cats.definitions[categoryId] then + addon:Debug("CategoryManager: Category already exists: " .. categoryId) + return false + end + + definition.isBuiltIn = false + cats.definitions[categoryId] = definition + table.insert(cats.order, categoryId) + + self:SaveCategories(cats) + return true +end + +-- Update an existing category +function CategoryManager:UpdateCategory(categoryId, definition) + local cats = self:GetCategories() + + if not cats.definitions[categoryId] then + addon:Debug("CategoryManager: Category not found: " .. categoryId) + return false + end + + -- Preserve isBuiltIn flag + definition.isBuiltIn = cats.definitions[categoryId].isBuiltIn + cats.definitions[categoryId] = definition + + self:SaveCategories(cats) + return true +end + +-- Delete a category (only custom categories can be deleted) +function CategoryManager:DeleteCategory(categoryId) + local cats = self:GetCategories() + local def = cats.definitions[categoryId] + + if not def then + return false + end + + if def.isBuiltIn then + addon:Debug("CategoryManager: Cannot delete built-in category: " .. categoryId) + return false + end + + cats.definitions[categoryId] = nil + + -- Remove from order + for i, id in ipairs(cats.order) do + if id == categoryId then + table.remove(cats.order, i) + break + end + end + + self:SaveCategories(cats) + return true +end + +-- Move category up in order +function CategoryManager:MoveCategoryUp(categoryId) + local cats = self:GetCategories() + + for i, id in ipairs(cats.order) do + if id == categoryId and i > 1 then + cats.order[i] = cats.order[i - 1] + cats.order[i - 1] = categoryId + self:SaveCategories(cats) + return true + end + end + return false +end + +-- Move category down in order +function CategoryManager:MoveCategoryDown(categoryId) + local cats = self:GetCategories() + local count = table.getn(cats.order) + + for i, id in ipairs(cats.order) do + if id == categoryId and i < count then + cats.order[i] = cats.order[i + 1] + cats.order[i + 1] = categoryId + self:SaveCategories(cats) + return true + end + end + return false +end + +-- Toggle category enabled state +function CategoryManager:ToggleCategory(categoryId) + local cats = self:GetCategories() + local def = cats.definitions[categoryId] + + if def then + def.enabled = not def.enabled + self:SaveCategories(cats) + return true + end + return false +end + +-- Reset all categories to defaults +function CategoryManager:ResetToDefaults() + Guda_CharDB.categories = self:GetDefaultCategories() + addon:Debug("CategoryManager: Reset to default categories") +end + +------------------------------------------- +-- Rule Evaluation Engine +------------------------------------------- + +-- Evaluate a single rule against item data +function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar) + local ruleType = rule.type + local ruleValue = rule.value + + if ruleType == "itemType" then + return (itemData.class == ruleValue) or (itemData.type == ruleValue) + + elseif ruleType == "itemSubtype" then + local subclass = itemData.subclass or "" + -- Check for partial match (e.g., "Food" matches "Food & Drink") + if string.find(subclass, ruleValue) then + return true + end + return subclass == ruleValue + + elseif ruleType == "namePattern" then + local itemName = itemData.name or "" + return string.find(itemName, ruleValue) ~= nil + + elseif ruleType == "quality" then + return itemData.quality == ruleValue + + elseif ruleType == "isBoE" then + if isOtherChar then return false end + if itemData.class ~= "Weapon" and itemData.class ~= "Armor" then + return false + end + local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link) + return isBoE == ruleValue + + elseif ruleType == "isQuestItem" then + local isQuestItem = false + local itemCategory = itemData.class or itemData.category or "" + local itemType = itemData.type or "" + local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor") + local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") + + if isQuestCategory then + isQuestItem = true + elseif not isOtherChar then + isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) + if isQuestItem and isEquipment and not isQuestCategory then + isQuestItem = false + end + end + + -- Also check QuestItemsDB + if not isQuestItem and itemData.link then + local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) + if itemID and addon.IsQuestItemByID then + local playerFaction = UnitFactionGroup("player") + isQuestItem = addon:IsQuestItemByID(itemID, playerFaction) + end + end + + return isQuestItem == ruleValue + + elseif ruleType == "texturePattern" then + local texture = itemData.texture or "" + return string.find(string.lower(texture), string.lower(ruleValue)) ~= nil + + elseif ruleType == "itemID" then + if not itemData.link then return false end + local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) + if not itemID then return false end + + -- ruleValue can be a single ID or a table of IDs + if type(ruleValue) == "table" then + for _, id in ipairs(ruleValue) do + if itemID == id then return true end + end + return false + else + return itemID == ruleValue + end + + elseif ruleType == "isSoulShard" then + return addon.Modules.Utils:IsSoulShard(itemData.link) == ruleValue + + elseif ruleType == "isProjectile" then + local isProj = (itemData.class == "Projectile" or + itemData.subclass == "Arrow" or + itemData.subclass == "Bullet") + return isProj == ruleValue + + elseif ruleType == "restoreTag" then + -- restoreTag is set by tooltip scanning: "eat", "drink", or "restore" + local tag = itemData.restoreTag + if not tag then return false end + return tag == ruleValue + end + + return false +end + +-- Evaluate all rules for a category +function CategoryManager:EvaluateCategoryRules(categoryDef, itemData, bagID, slotID, isOtherChar) + if not categoryDef.enabled then + return false + end + + local rules = categoryDef.rules or {} + + -- No rules = fallback category (matches everything) + if table.getn(rules) == 0 then + return categoryDef.isFallback == true + end + + local matchMode = categoryDef.matchMode or "any" + + if matchMode == "all" then + -- All rules must match + for _, rule in ipairs(rules) do + if not self:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar) then + return false + end + end + return true + else + -- Any rule must match + for _, rule in ipairs(rules) do + if self:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar) then + return true + end + end + return false + end +end + +-- Get sorted categories by priority (highest first) +function CategoryManager:GetCategoriesByPriority() + local cats = self:GetCategories() + local sorted = {} + + for id, def in pairs(cats.definitions) do + if def.enabled then + table.insert(sorted, { id = id, def = def }) + end + end + + table.sort(sorted, function(a, b) + return (a.def.priority or 0) > (b.def.priority or 0) + end) + + return sorted +end + +-- Categorize an item using the rule engine +-- Returns category ID or "Miscellaneous" as fallback +function CategoryManager:CategorizeItem(itemData, bagID, slotID, isOtherChar) + local sortedCats = self:GetCategoriesByPriority() + + for _, entry in ipairs(sortedCats) do + if not entry.def.isFallback then + if self:EvaluateCategoryRules(entry.def, itemData, bagID, slotID, isOtherChar) then + return entry.id + end + end + end + + return "Miscellaneous" +end + +-- Build the Guda_CategoryList from current category order (for compatibility) +function CategoryManager:BuildCategoryList() + local order = self:GetCategoryOrder() + local list = {} + + for _, id in ipairs(order) do + local def = self:GetCategory(id) + if def and def.enabled then + table.insert(list, id) + end + end + + return list +end + +-- Get available rule types for UI +function CategoryManager:GetRuleTypes() + return { + { id = "itemType", name = "Item Type", description = "Match by item class (Armor, Weapon, etc.)" }, + { id = "itemSubtype", name = "Item Subtype", description = "Match by item subclass (Cloth, Potion, etc.)" }, + { id = "namePattern", name = "Name Pattern", description = "Match item name (supports Lua patterns)" }, + { id = "quality", name = "Quality", description = "Match by item quality (0=Gray to 5=Legendary)" }, + { id = "isBoE", name = "Bind on Equip", description = "Match items that bind when equipped" }, + { id = "isQuestItem", name = "Quest Item", description = "Match quest items" }, + { id = "texturePattern", name = "Icon Pattern", description = "Match icon texture path" }, + { id = "itemID", name = "Item ID", description = "Match specific item IDs" }, + { id = "isSoulShard", name = "Soul Shard", description = "Match soul shards" }, + { id = "isProjectile", name = "Projectile", description = "Match arrows and bullets" }, + { id = "restoreTag", name = "Restore Type", description = "Match by consumable type (eat, drink, restore)" }, + } +end + +-- Get common item types for UI dropdowns +function CategoryManager:GetItemTypes() + return { + "Armor", "Weapon", "Consumable", "Container", "Trade Goods", + "Projectile", "Quiver", "Reagent", "Recipe", "Key", "Miscellaneous", "Quest" + } +end + +-- Get quality names for UI +function CategoryManager:GetQualityNames() + return { + [0] = "Poor (Gray)", + [1] = "Common (White)", + [2] = "Uncommon (Green)", + [3] = "Rare (Blue)", + [4] = "Epic (Purple)", + [5] = "Legendary (Orange)", + } +end diff --git a/Core/Database.lua b/Core/Database.lua index be0ef7d..bf2a4b9 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -122,6 +122,11 @@ function DB:Initialize() Guda_CharDB.settings.markUnusableItems = true end + -- Initialize CategoryManager for custom categories + if addon.Modules.CategoryManager then + addon.Modules.CategoryManager:Initialize() + end + -- Initialize this character's data if not Guda_DB.characters[fullName] then local localizedClass, englishClass = UnitClass("player") diff --git a/Guda.toc b/Guda.toc index 5f887d9..029233b 100644 --- a/Guda.toc +++ b/Guda.toc @@ -13,6 +13,7 @@ Core\Database.lua DB\QuestItems.lua Core\Events.lua Core\Utils.lua +Core\CategoryManager.lua Core\Tooltip.lua Data\BagScanner.lua diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index e27e4cb..7dac7b9 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -5,10 +5,18 @@ local FrameHelpers = {} addon.Modules.FrameHelpers = FrameHelpers -- Standard category list used by both Bag and Bank frames +-- Now dynamically built from CategoryManager if available Guda_CategoryList = { "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" } +-- Rebuild category list from CategoryManager +function Guda_RefreshCategoryList() + if addon.Modules.CategoryManager then + Guda_CategoryList = addon.Modules.CategoryManager:BuildCategoryList() + end +end + -- Categorize a single item into categories and specialItems tables -- Returns nothing, modifies tables in place function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar) @@ -25,6 +33,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, end -- Priority 1: Special items (Hearthstone, Mounts, Tools) + -- These are handled separately and go into specialItems table, not categories if string.find(itemName, "Hearthstone") then table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData}) return @@ -44,22 +53,28 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end + -- Use CategoryManager rule engine if available, otherwise fall back to legacy logic + if addon.Modules.CategoryManager then + cat = addon.Modules.CategoryManager:CategorizeItem(itemData, bagID, slotID, isOtherChar) + if not categories[cat] then cat = "Miscellaneous" end + table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData}) + return + end + + -- Legacy categorization logic (fallback if CategoryManager not available) -- Priority 2: Class Items (Soul Shards, Arrows, Bullets) - if addon.Modules.Utils:IsSoulShard(itemData.link) or - itemData.class == "Projectile" or - itemData.subclass == "Arrow" or + if addon.Modules.Utils:IsSoulShard(itemData.link) or + itemData.class == "Projectile" or + itemData.subclass == "Arrow" or itemData.subclass == "Bullet" then table.insert(categories["Class Items"], {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 3: Quest Items - -- Check multiple sources: tooltip scan, itemClass, itemType, and QuestItemsDB local isQuestItem = false local itemCategory = itemData.class or itemData.category or "" local itemType = itemData.type or "" - - -- If it's a Weapon or Armor, it shouldn't be a QuestItem unless it's specifically categorized as Quest local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor") local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") @@ -67,12 +82,10 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isQuestItem = true elseif not isOtherChar then isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) - -- If tooltip said it is quest, but it is equipment and not quest category, reject it if isQuestItem and isEquipment and not isQuestCategory then isQuestItem = false end end - -- Also check the QuestItemsDB for known faction-specific quest items if not isQuestItem and itemData.link then local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) if itemID and addon.IsQuestItemByID then @@ -106,7 +119,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end - -- Priority 6: BoE Equipment (Armor/Weapons that bind when equipped) + -- Priority 6: BoE Equipment if (itemData.class == "Weapon" or itemData.class == "Armor") and not isOtherChar then local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link) if isBoE then @@ -117,7 +130,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end - -- Priority 7: Equipment for other characters (can't scan tooltip) + -- Priority 7: Equipment for other characters if (itemData.class == "Weapon" or itemData.class == "Armor") and isOtherChar then table.insert(categories[itemData.class], {bagID = bagID, slotID = slotID, itemData = itemData}) return @@ -131,10 +144,36 @@ end -- Initialize empty category tables function Guda_InitCategories() + -- Refresh category list from CategoryManager (enabled categories only for display) + Guda_RefreshCategoryList() + + -- Create tables for ALL categories (not just enabled) so items can be categorized + -- Items in disabled categories simply won't be displayed local categories = {} - for _, cat in ipairs(Guda_CategoryList) do - categories[cat] = {} + + if addon.Modules.CategoryManager then + -- Get full category order (all categories, not just enabled) + local allCategories = addon.Modules.CategoryManager:GetCategoryOrder() + for _, cat in ipairs(allCategories) do + categories[cat] = {} + end + else + -- Fallback: use the display list + for _, cat in ipairs(Guda_CategoryList) do + categories[cat] = {} + end end + + -- Always ensure Miscellaneous exists as fallback + if not categories["Miscellaneous"] then + categories["Miscellaneous"] = {} + end + + -- Always ensure Keyring exists (handled specially in BagFrame) + if not categories["Keyring"] then + categories["Keyring"] = {} + end + local specialItems = { Hearthstone = {}, Mount = {}, diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua index 1fbf4b1..59a1591 100644 --- a/UI/SettingsPopup.lua +++ b/UI/SettingsPopup.lua @@ -52,19 +52,23 @@ function Guda_SettingsPopup_SelectTab(tabName) -- Hide all tab content frames local generalTab = getglobal("Guda_SettingsPopup_GeneralTab") local iconsTab = getglobal("Guda_SettingsPopup_IconsTab") + local categoriesTab = getglobal("Guda_SettingsPopup_CategoriesTab") local guideTab = getglobal("Guda_SettingsPopup_GuideTab") if generalTab then generalTab:Hide() end if iconsTab then iconsTab:Hide() end + if categoriesTab then categoriesTab:Hide() end if guideTab then guideTab:Hide() end -- Reset all tab button backgrounds to inactive (0.1 alpha) local generalBg = getglobal("Guda_SettingsPopup_GeneralTabButton_Bg") local iconsBg = getglobal("Guda_SettingsPopup_IconsTabButton_Bg") + local categoriesBg = getglobal("Guda_SettingsPopup_CategoriesTabButton_Bg") local guideBg = getglobal("Guda_SettingsPopup_GuideTabButton_Bg") if generalBg then generalBg:SetTexture(1, 1, 1, 0.1) end if iconsBg then iconsBg:SetTexture(1, 1, 1, 0.1) end + if categoriesBg then categoriesBg:SetTexture(1, 1, 1, 0.1) end if guideBg then guideBg:SetTexture(1, 1, 1, 0.1) end -- Show selected tab and highlight its button @@ -74,6 +78,10 @@ function Guda_SettingsPopup_SelectTab(tabName) elseif tabName == "icons" then if iconsTab then iconsTab:Show() end if iconsBg then iconsBg:SetTexture(1, 1, 1, 0.3) end + elseif tabName == "categories" then + if categoriesTab then categoriesTab:Show() end + if categoriesBg then categoriesBg:SetTexture(1, 1, 1, 0.3) end + Guda_SettingsPopup_CategoriesTab_Update() elseif tabName == "guide" then if guideTab then guideTab:Show() end if guideBg then guideBg:SetTexture(1, 1, 1, 0.3) end @@ -1160,6 +1168,234 @@ function Guda_SettingsPopup_ReverseStackSortCheckbox_OnClick(self) -- No immediate UI update needed end +------------------------------------------- +-- Categories Tab Functions +------------------------------------------- + +-- Number of visible rows in the category list +local CATEGORY_ROW_HEIGHT = 22 +local CATEGORY_VISIBLE_ROWS = 14 +local categoryRowFrames = {} + +-- Create or get a category row frame +local function GetCategoryRowFrame(index) + if categoryRowFrames[index] then + return categoryRowFrames[index] + end + + local container = getglobal("Guda_SettingsPopup_CategoryListContainer") + if not container then return nil end + + local rowName = "Guda_SettingsPopup_CategoryRow" .. index + local row = CreateFrame("Frame", rowName, container) + row:SetHeight(CATEGORY_ROW_HEIGHT) + row:SetWidth(420) + row:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -((index - 1) * CATEGORY_ROW_HEIGHT)) + + -- Background highlight + local bg = row:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(row) + bg:SetTexture(1, 1, 1, 0) + row.bg = bg + + -- Enable checkbox + local checkbox = CreateFrame("CheckButton", rowName .. "_Checkbox", row, "UICheckButtonTemplate") + checkbox:SetWidth(20) + checkbox:SetHeight(20) + checkbox:SetPoint("LEFT", row, "LEFT", 0, 0) + checkbox:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:ToggleCategory(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end) + row.checkbox = checkbox + + -- Category name + local nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") + nameText:SetPoint("LEFT", checkbox, "RIGHT", 5, 0) + nameText:SetWidth(200) + nameText:SetJustifyH("LEFT") + row.nameText = nameText + + -- Move Up button + local upBtn = CreateFrame("Button", rowName .. "_UpBtn", row) + upBtn:SetWidth(20) + upBtn:SetHeight(20) + upBtn:SetPoint("LEFT", nameText, "RIGHT", 10, 0) + upBtn:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up") + upBtn:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down") + upBtn:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight") + upBtn:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:MoveCategoryUp(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end) + row.upBtn = upBtn + + -- Move Down button + local downBtn = CreateFrame("Button", rowName .. "_DownBtn", row) + downBtn:SetWidth(20) + downBtn:SetHeight(20) + downBtn:SetPoint("LEFT", upBtn, "RIGHT", 2, 0) + downBtn:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up") + downBtn:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down") + downBtn:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Highlight") + downBtn:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:MoveCategoryDown(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end) + row.downBtn = downBtn + + -- Delete button (only for custom categories) + local deleteBtn = CreateFrame("Button", rowName .. "_DeleteBtn", row, "UIPanelCloseButton") + deleteBtn:SetWidth(20) + deleteBtn:SetHeight(20) + deleteBtn:SetPoint("LEFT", downBtn, "RIGHT", 5, 0) + deleteBtn:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + local def = Guda.Modules.CategoryManager:GetCategory(catId) + if def and not def.isBuiltIn then + Guda.Modules.CategoryManager:DeleteCategory(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end + end) + row.deleteBtn = deleteBtn + + -- Built-in indicator + local builtInText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + builtInText:SetPoint("LEFT", deleteBtn, "RIGHT", 5, 0) + builtInText:SetText("(Built-in)") + builtInText:SetTextColor(0.5, 0.5, 0.5) + row.builtInText = builtInText + + -- Hover highlight + row:EnableMouse(true) + row:SetScript("OnEnter", function() + this.bg:SetTexture(1, 1, 1, 0.1) + end) + row:SetScript("OnLeave", function() + this.bg:SetTexture(1, 1, 1, 0) + end) + + categoryRowFrames[index] = row + return row +end + +-- Update the category list display +function Guda_SettingsPopup_CategoriesTab_Update() + if not Guda.Modules.CategoryManager then return end + + local scrollFrame = getglobal("Guda_SettingsPopup_CategoriesScrollFrame") + if not scrollFrame then return end + + local categoryOrder = Guda.Modules.CategoryManager:GetCategoryOrder() + local totalCategories = table.getn(categoryOrder) + + -- Update scroll frame + FauxScrollFrame_Update(scrollFrame, totalCategories, CATEGORY_VISIBLE_ROWS, CATEGORY_ROW_HEIGHT) + + local offset = FauxScrollFrame_GetOffset(scrollFrame) + + for i = 1, CATEGORY_VISIBLE_ROWS do + local row = GetCategoryRowFrame(i) + if row then + local dataIndex = i + offset + if dataIndex <= totalCategories then + local categoryId = categoryOrder[dataIndex] + local categoryDef = Guda.Modules.CategoryManager:GetCategory(categoryId) + + if categoryDef then + row.categoryId = categoryId + row.nameText:SetText(categoryDef.name or categoryId) + row.checkbox:SetChecked(categoryDef.enabled and 1 or 0) + + -- Show/hide delete button based on whether it's built-in + if categoryDef.isBuiltIn then + row.deleteBtn:Hide() + row.builtInText:Show() + else + row.deleteBtn:Show() + row.builtInText:Hide() + end + + -- Enable/disable move buttons based on position + if dataIndex == 1 then + row.upBtn:Disable() + else + row.upBtn:Enable() + end + + if dataIndex == totalCategories then + row.downBtn:Disable() + else + row.downBtn:Enable() + end + + -- Set text color based on enabled state + if categoryDef.enabled then + row.nameText:SetTextColor(1, 1, 1) + else + row.nameText:SetTextColor(0.5, 0.5, 0.5) + end + + row:Show() + else + row:Hide() + end + else + row:Hide() + end + end + end + + -- Set reset button text + local resetBtn = getglobal("Guda_SettingsPopup_ResetCategoriesButton") + if resetBtn then + resetBtn:SetText("Reset to Defaults") + end +end + +-- Reset categories to defaults +function Guda_SettingsPopup_ResetCategories_OnClick() + if Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:ResetToDefaults() + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + Guda:Print("Categories reset to defaults.") + end +end + +-- Refresh bag and bank frames after category changes +function Guda_SettingsPopup_RefreshBagFrames() + -- Refresh category list + Guda_RefreshCategoryList() + + -- Update bag frame if visible + local bagFrame = getglobal("Guda_BagFrame") + if bagFrame and bagFrame:IsShown() then + Guda.Modules.BagFrame:Update() + end + + -- Update bank frame if visible + local bankFrame = getglobal("Guda_BankFrame") + if bankFrame and bankFrame:IsShown() then + Guda.Modules.BankFrame:Update() + end +end + -- Initialize function SettingsPopup:Initialize() Guda:Debug("Settings popup initialized") diff --git a/UI/SettingsPopup.xml b/UI/SettingsPopup.xml index 8b67f0a..20652b3 100644 --- a/UI/SettingsPopup.xml +++ b/UI/SettingsPopup.xml @@ -62,7 +62,7 @@ - + + + + + From d282ec27b94df8d429105c412759f9615dc974cb Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 14:06:25 +0400 Subject: [PATCH 02/10] feat: category type rules --- Core/CategoryManager.lua | 13 +- UI/SettingsPopup.lua | 505 ++++++++++++++++++++++++++++++++++++++- UI/SettingsPopup.xml | 242 +++++++++++++++++++ 3 files changed, 757 insertions(+), 3 deletions(-) diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index f43ce18..49bf35f 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -31,7 +31,7 @@ local DEFAULT_CATEGORIES = { { type = "isBoE", value = true } }, matchMode = "all", - priority = 60, + priority = 75, enabled = true, isBuiltIn = true, }, @@ -322,6 +322,13 @@ function CategoryManager:MigrateCategories() addon:Debug("CategoryManager: Migrated Drink category to use restoreTag") end end + + -- Migrate BoE priority to be higher than Weapon/Armor (75 > 70) + local boeCat = cats.definitions["BoE"] + if boeCat and boeCat.isBuiltIn and boeCat.priority and boeCat.priority < 75 then + boeCat.priority = 75 + addon:Debug("CategoryManager: Migrated BoE priority to 75") + end end -- Get all categories @@ -492,6 +499,10 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar elseif ruleType == "quality" then return itemData.quality == ruleValue + elseif ruleType == "qualityMin" then + -- Minimum quality check (item quality >= ruleValue) + return (itemData.quality or 0) >= (ruleValue or 0) + elseif ruleType == "isBoE" then if isOtherChar then return false end if itemData.class ~= "Weapon" and itemData.class ~= "Armor" then diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua index 59a1591..1329e93 100644 --- a/UI/SettingsPopup.lua +++ b/UI/SettingsPopup.lua @@ -1216,15 +1216,29 @@ local function GetCategoryRowFrame(index) -- Category name local nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") nameText:SetPoint("LEFT", checkbox, "RIGHT", 5, 0) - nameText:SetWidth(200) + nameText:SetWidth(160) nameText:SetJustifyH("LEFT") row.nameText = nameText + -- Edit button + local editBtn = CreateFrame("Button", rowName .. "_EditBtn", row, "UIPanelButtonTemplate") + editBtn:SetWidth(40) + editBtn:SetHeight(18) + editBtn:SetPoint("LEFT", nameText, "RIGHT", 5, 0) + editBtn:SetText("Edit") + editBtn:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId then + Guda_CategoryEditor_Open(catId) + end + end) + row.editBtn = editBtn + -- Move Up button local upBtn = CreateFrame("Button", rowName .. "_UpBtn", row) upBtn:SetWidth(20) upBtn:SetHeight(20) - upBtn:SetPoint("LEFT", nameText, "RIGHT", 10, 0) + upBtn:SetPoint("LEFT", editBtn, "RIGHT", 5, 0) upBtn:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up") upBtn:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down") upBtn:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight") @@ -1396,6 +1410,493 @@ function Guda_SettingsPopup_RefreshBagFrames() end end +------------------------------------------- +-- Category Editor Functions +------------------------------------------- + +local editorCategoryId = nil +local editorMatchMode = "any" +local editorRules = {} +local editorRuleFrames = {} +local RULE_ROW_HEIGHT = 28 +local MAX_RULES = 6 + +-- Rule type options for dropdown +local RULE_TYPE_OPTIONS = { + { id = "itemType", name = "Item Type" }, + { id = "itemSubtype", name = "Item Subtype" }, + { id = "namePattern", name = "Name Contains" }, + { id = "quality", name = "Quality (exact)" }, + { id = "qualityMin", name = "Quality (min)" }, + { id = "isBoE", name = "Bind on Equip" }, + { id = "isQuestItem", name = "Quest Item" }, + { id = "restoreTag", name = "Restore Type" }, + { id = "isSoulShard", name = "Soul Shard" }, + { id = "isProjectile", name = "Projectile" }, +} + +-- Value options for specific rule types +local RULE_VALUE_OPTIONS = { + itemType = { "Armor", "Weapon", "Consumable", "Container", "Trade Goods", "Projectile", "Quiver", "Reagent", "Recipe", "Key", "Miscellaneous", "Quest" }, + quality = { "0 - Poor", "1 - Common", "2 - Uncommon", "3 - Rare", "4 - Epic", "5 - Legendary" }, + qualityMin = { "0 - Poor", "1 - Common", "2 - Uncommon", "3 - Rare", "4 - Epic", "5 - Legendary" }, + isBoE = { "true", "false" }, + isQuestItem = { "true", "false" }, + isSoulShard = { "true", "false" }, + isProjectile = { "true", "false" }, + restoreTag = { "eat", "drink", "restore" }, +} + +-- OnLoad for Category Editor +function Guda_CategoryEditor_OnLoad(self) + Guda:ApplyBackdrop(self, "DEFAULT_FRAME") + + -- Set button texts + local addBtn = getglobal("Guda_CategoryEditor_AddRuleButton") + if addBtn then addBtn:SetText("+ Add Rule") end + + local saveBtn = getglobal("Guda_CategoryEditor_SaveButton") + if saveBtn then saveBtn:SetText("Save") end + + local cancelBtn = getglobal("Guda_CategoryEditor_CancelButton") + if cancelBtn then cancelBtn:SetText("Cancel") end + + -- Create radio button labels + local anyRadio = getglobal("Guda_CategoryEditor_MatchAny") + if anyRadio then + local label = anyRadio:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + label:SetPoint("LEFT", anyRadio, "RIGHT", 2, 0) + label:SetText("Any rule") + end + + local allRadio = getglobal("Guda_CategoryEditor_MatchAll") + if allRadio then + local label = allRadio:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + label:SetPoint("LEFT", allRadio, "RIGHT", 2, 0) + label:SetText("All rules") + end +end + +-- OnShow for Category Editor +function Guda_CategoryEditor_OnShow(self) + Guda_CategoryEditor_UpdateRulesDisplay() +end + +-- Open Category Editor for a specific category +function Guda_CategoryEditor_Open(categoryId) + if not Guda.Modules.CategoryManager then return end + + local categoryDef = Guda.Modules.CategoryManager:GetCategory(categoryId) + if not categoryDef then return end + + editorCategoryId = categoryId + editorMatchMode = categoryDef.matchMode or "any" + + -- Copy rules + editorRules = {} + if categoryDef.rules then + for i, rule in ipairs(categoryDef.rules) do + table.insert(editorRules, { type = rule.type, value = rule.value }) + end + end + + -- Set title + local title = getglobal("Guda_CategoryEditor_Title") + if title then + if categoryDef.isBuiltIn then + title:SetText("Edit Category (Built-in)") + else + title:SetText("Edit Category") + end + end + + -- Set name + local nameBox = getglobal("Guda_CategoryEditor_NameEditBox") + if nameBox then + nameBox:SetText(categoryDef.name or categoryId) + -- Disable name editing for built-in categories + if categoryDef.isBuiltIn then + nameBox:EnableMouse(false) + nameBox:EnableKeyboard(false) + nameBox:SetTextColor(0.5, 0.5, 0.5) + else + nameBox:EnableMouse(true) + nameBox:EnableKeyboard(true) + nameBox:SetTextColor(1, 1, 1) + end + end + + -- Set match mode + Guda_CategoryEditor_SetMatchMode(editorMatchMode) + + -- Show editor + local editor = getglobal("Guda_CategoryEditor") + if editor then + editor:Show() + end +end + +-- Set match mode (radio buttons) +function Guda_CategoryEditor_SetMatchMode(mode) + editorMatchMode = mode + + local anyRadio = getglobal("Guda_CategoryEditor_MatchAny") + local allRadio = getglobal("Guda_CategoryEditor_MatchAll") + + if anyRadio then anyRadio:SetChecked(mode == "any" and 1 or 0) end + if allRadio then allRadio:SetChecked(mode == "all" and 1 or 0) end +end + +-- Get or create a rule row frame +local function GetRuleRowFrame(index) + if editorRuleFrames[index] then + return editorRuleFrames[index] + end + + local container = getglobal("Guda_CategoryEditor_RulesContainer") + if not container then return nil end + + local rowName = "Guda_CategoryEditor_RuleRow" .. index + local row = CreateFrame("Frame", rowName, container) + row:SetHeight(RULE_ROW_HEIGHT) + row:SetWidth(360) + row:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -((index - 1) * RULE_ROW_HEIGHT)) + + -- Rule type dropdown button + local typeBtn = CreateFrame("Button", rowName .. "_TypeBtn", row, "UIPanelButtonTemplate") + typeBtn:SetWidth(120) + typeBtn:SetHeight(22) + typeBtn:SetPoint("LEFT", row, "LEFT", 0, 0) + typeBtn:SetText("Select Type") + typeBtn.ruleIndex = index + typeBtn:SetScript("OnClick", function() + Guda_CategoryEditor_ShowTypeDropdown(this, this.ruleIndex) + end) + row.typeBtn = typeBtn + + -- Value input (editbox for text, button for dropdowns) + local valueBox = CreateFrame("EditBox", rowName .. "_ValueBox", row, "InputBoxTemplate") + valueBox:SetWidth(140) + valueBox:SetHeight(22) + valueBox:SetPoint("LEFT", typeBtn, "RIGHT", 5, 0) + valueBox:SetAutoFocus(false) + valueBox.ruleIndex = index + valueBox:SetScript("OnTextChanged", function() + local idx = this.ruleIndex + if editorRules[idx] then + editorRules[idx].value = this:GetText() + end + end) + valueBox:SetScript("OnEscapePressed", function() this:ClearFocus() end) + valueBox:SetScript("OnEnterPressed", function() this:ClearFocus() end) + row.valueBox = valueBox + + -- Value dropdown button (for predefined values) + local valueBtn = CreateFrame("Button", rowName .. "_ValueBtn", row, "UIPanelButtonTemplate") + valueBtn:SetWidth(140) + valueBtn:SetHeight(22) + valueBtn:SetPoint("LEFT", typeBtn, "RIGHT", 5, 0) + valueBtn:SetText("Select Value") + valueBtn.ruleIndex = index + valueBtn:SetScript("OnClick", function() + Guda_CategoryEditor_ShowValueDropdown(this, this.ruleIndex) + end) + valueBtn:Hide() + row.valueBtn = valueBtn + + -- Delete button + local deleteBtn = CreateFrame("Button", rowName .. "_DeleteBtn", row, "UIPanelCloseButton") + deleteBtn:SetWidth(22) + deleteBtn:SetHeight(22) + deleteBtn:SetPoint("LEFT", valueBox, "RIGHT", 5, 0) + deleteBtn.ruleIndex = index + deleteBtn:SetScript("OnClick", function() + Guda_CategoryEditor_RemoveRule(this.ruleIndex) + end) + row.deleteBtn = deleteBtn + + editorRuleFrames[index] = row + return row +end + +-- Update rules display +function Guda_CategoryEditor_UpdateRulesDisplay() + local numRules = table.getn(editorRules) + + for i = 1, MAX_RULES do + local row = GetRuleRowFrame(i) + if row then + if i <= numRules then + local rule = editorRules[i] + row.ruleIndex = i + row.typeBtn.ruleIndex = i + row.valueBox.ruleIndex = i + row.valueBtn.ruleIndex = i + row.deleteBtn.ruleIndex = i + + -- Set type button text + local typeName = "Select Type" + for _, opt in ipairs(RULE_TYPE_OPTIONS) do + if opt.id == rule.type then + typeName = opt.name + break + end + end + row.typeBtn:SetText(typeName) + + -- Show appropriate value input + if RULE_VALUE_OPTIONS[rule.type] then + -- Use dropdown for predefined values + row.valueBox:Hide() + row.valueBtn:Show() + local displayValue = tostring(rule.value or "Select") + -- Format quality display + if (rule.type == "quality" or rule.type == "qualityMin") and type(rule.value) == "number" then + local qualNames = { [0]="Poor", [1]="Common", [2]="Uncommon", [3]="Rare", [4]="Epic", [5]="Legendary" } + displayValue = rule.value .. " - " .. (qualNames[rule.value] or "") + end + row.valueBtn:SetText(displayValue) + else + -- Use editbox for text input + row.valueBtn:Hide() + row.valueBox:Show() + row.valueBox:SetText(tostring(rule.value or "")) + end + + row:Show() + else + row:Hide() + end + end + end + + -- Enable/disable Add Rule button + local addBtn = getglobal("Guda_CategoryEditor_AddRuleButton") + if addBtn then + if numRules >= MAX_RULES then + addBtn:Disable() + else + addBtn:Enable() + end + end +end + +-- Add a new rule +function Guda_CategoryEditor_AddRule() + if table.getn(editorRules) >= MAX_RULES then return end + + table.insert(editorRules, { type = "itemType", value = "Consumable" }) + Guda_CategoryEditor_UpdateRulesDisplay() +end + +-- Remove a rule +function Guda_CategoryEditor_RemoveRule(index) + if index > 0 and index <= table.getn(editorRules) then + table.remove(editorRules, index) + Guda_CategoryEditor_UpdateRulesDisplay() + end +end + +-- Helper to set rule type (called from dropdown) +function Guda_CategoryEditor_SetRuleType(ruleIndex, typeId) + if not editorRules[ruleIndex] then return end + + editorRules[ruleIndex].type = typeId + -- Reset value when type changes + if RULE_VALUE_OPTIONS[typeId] then + editorRules[ruleIndex].value = RULE_VALUE_OPTIONS[typeId][1] + -- Convert to proper type + if typeId == "quality" or typeId == "qualityMin" then + editorRules[ruleIndex].value = 0 + elseif typeId == "isBoE" or typeId == "isQuestItem" or typeId == "isSoulShard" or typeId == "isProjectile" then + editorRules[ruleIndex].value = true + end + else + editorRules[ruleIndex].value = "" + end + Guda_CategoryEditor_UpdateRulesDisplay() +end + +-- Helper to set rule value (called from dropdown) +function Guda_CategoryEditor_SetRuleValue(ruleIndex, val, ruleType) + if not editorRules[ruleIndex] then return end + + if ruleType == "quality" or ruleType == "qualityMin" then + local num = tonumber(string.sub(val, 1, 1)) + editorRules[ruleIndex].value = num or 0 + elseif ruleType == "isBoE" or ruleType == "isQuestItem" or ruleType == "isSoulShard" or ruleType == "isProjectile" then + editorRules[ruleIndex].value = (val == "true") + else + editorRules[ruleIndex].value = val + end + Guda_CategoryEditor_UpdateRulesDisplay() +end + +-- Show type dropdown menu +function Guda_CategoryEditor_ShowTypeDropdown(button, ruleIndex) + local menu = {} + for i = 1, table.getn(RULE_TYPE_OPTIONS) do + local opt = RULE_TYPE_OPTIONS[i] + table.insert(menu, { + text = opt.name, + ruleIndex = ruleIndex, + typeId = opt.id, + }) + end + + Guda_ShowSimpleDropdown(button, menu, "type") +end + +-- Show value dropdown menu +function Guda_CategoryEditor_ShowValueDropdown(button, ruleIndex) + local rule = editorRules[ruleIndex] + if not rule then return end + + local options = RULE_VALUE_OPTIONS[rule.type] + if not options then return end + + local menu = {} + for i = 1, table.getn(options) do + local val = options[i] + table.insert(menu, { + text = val, + ruleIndex = ruleIndex, + ruleType = rule.type, + value = val, + }) + end + + Guda_ShowSimpleDropdown(button, menu, "value") +end + +-- Simple dropdown menu helper +local dropdownFrame = nil +function Guda_ShowSimpleDropdown(anchor, menuItems, menuType) + if not dropdownFrame then + dropdownFrame = CreateFrame("Frame", "Guda_SimpleDropdown", UIParent) + dropdownFrame:SetFrameStrata("FULLSCREEN_DIALOG") + dropdownFrame:SetWidth(150) + dropdownFrame:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + tile = true, tileSize = 16, edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 } + }) + dropdownFrame:SetBackdropColor(0, 0, 0, 1) + dropdownFrame:EnableMouse(true) + dropdownFrame:Hide() + + dropdownFrame:SetScript("OnLeave", function() + -- Hide after a short delay if mouse leaves + this.hideTimer = 0.5 + end) + dropdownFrame:SetScript("OnUpdate", function() + if this.hideTimer then + this.hideTimer = this.hideTimer - arg1 + if this.hideTimer <= 0 then + this.hideTimer = nil + -- Check if mouse is over any child + if not MouseIsOver(this) then + this:Hide() + end + end + end + end) + end + + -- Clear old buttons + local children = { dropdownFrame:GetChildren() } + for _, child in ipairs(children) do + child:Hide() + child:SetParent(nil) + end + + -- Create menu buttons + local btnHeight = 20 + local totalHeight = 10 + for i, item in ipairs(menuItems) do + local btn = CreateFrame("Button", nil, dropdownFrame) + btn:SetWidth(140) + btn:SetHeight(btnHeight) + btn:SetPoint("TOPLEFT", dropdownFrame, "TOPLEFT", 5, -(5 + (i-1) * btnHeight)) + + local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + text:SetPoint("LEFT", btn, "LEFT", 5, 0) + text:SetText(item.text) + btn.text = text + + local highlight = btn:CreateTexture(nil, "HIGHLIGHT") + highlight:SetAllPoints(btn) + highlight:SetTexture(1, 1, 1, 0.2) + + -- Store data on button for vanilla Lua closure compatibility + btn.menuType = menuType + btn.ruleIndex = item.ruleIndex + btn.typeId = item.typeId + btn.ruleType = item.ruleType + btn.value = item.value + + btn:SetScript("OnClick", function() + dropdownFrame:Hide() + if this.menuType == "type" then + Guda_CategoryEditor_SetRuleType(this.ruleIndex, this.typeId) + elseif this.menuType == "value" then + Guda_CategoryEditor_SetRuleValue(this.ruleIndex, this.value, this.ruleType) + end + end) + btn:SetScript("OnEnter", function() + dropdownFrame.hideTimer = nil + end) + + totalHeight = totalHeight + btnHeight + end + totalHeight = totalHeight + 5 + + dropdownFrame:SetHeight(totalHeight) + dropdownFrame:ClearAllPoints() + dropdownFrame:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, 0) + dropdownFrame:Show() + dropdownFrame.hideTimer = nil +end + +-- Save category changes +function Guda_CategoryEditor_Save() + if not editorCategoryId or not Guda.Modules.CategoryManager then return end + + local categoryDef = Guda.Modules.CategoryManager:GetCategory(editorCategoryId) + if not categoryDef then return end + + -- Get name (only for custom categories) + local nameBox = getglobal("Guda_CategoryEditor_NameEditBox") + if nameBox and not categoryDef.isBuiltIn then + categoryDef.name = nameBox:GetText() + end + + -- Set match mode + categoryDef.matchMode = editorMatchMode + + -- Set rules + categoryDef.rules = {} + for _, rule in ipairs(editorRules) do + if rule.type and rule.type ~= "" then + table.insert(categoryDef.rules, { type = rule.type, value = rule.value }) + end + end + + -- Save to database + Guda.Modules.CategoryManager:UpdateCategory(editorCategoryId, categoryDef) + + -- Refresh displays + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + + -- Close editor + local editor = getglobal("Guda_CategoryEditor") + if editor then editor:Hide() end + + Guda:Print("Category '" .. (categoryDef.name or editorCategoryId) .. "' saved.") +end + -- Initialize function SettingsPopup:Initialize() Guda:Debug("Settings popup initialized") diff --git a/UI/SettingsPopup.xml b/UI/SettingsPopup.xml index 20652b3..3dd7e30 100644 --- a/UI/SettingsPopup.xml +++ b/UI/SettingsPopup.xml @@ -769,4 +769,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + this:ClearFocus() + + + this:ClearFocus() + + + + + + + + + + + + + + + + + + + Guda_CategoryEditor_SetMatchMode("any") + + + + + + + + + + + + + + + + + + + Guda_CategoryEditor_SetMatchMode("all") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Guda_CategoryEditor_OnLoad(this) + + + Guda_CategoryEditor_OnShow(this) + + + if (arg1 == "LeftButton") then + this:StartMoving() + end + + + this:StopMovingOrSizing() + + + + From a2d373e95ce1befbbc5bea6d7ef3692ff4b3fe61 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 14:20:46 +0400 Subject: [PATCH 03/10] feat: add custom category --- Core/CategoryManager.lua | 30 +++++++++++++++++++++++++++ UI/SettingsPopup.lua | 44 ++++++++++++++++++++++++++++++++++++++-- UI/SettingsPopup.xml | 30 +++++++++++++++++++-------- 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index 49bf35f..ead4426 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -329,6 +329,25 @@ function CategoryManager:MigrateCategories() boeCat.priority = 75 addon:Debug("CategoryManager: Migrated BoE priority to 75") end + + -- Migrate Junk category to ensure proper rules and priority + local junkCat = cats.definitions["Junk"] + if junkCat and junkCat.isBuiltIn then + local needsUpdate = false + -- Check if priority is too low + if not junkCat.priority or junkCat.priority < 85 then + junkCat.priority = 85 + needsUpdate = true + end + -- Check if rules are missing or wrong + if not junkCat.rules or table.getn(junkCat.rules) == 0 then + junkCat.rules = { { type = "quality", value = 0 } } + needsUpdate = true + end + if needsUpdate then + addon:Debug("CategoryManager: Migrated Junk category") + end + end end -- Get all categories @@ -497,6 +516,17 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar return string.find(itemName, ruleValue) ~= nil elseif ruleType == "quality" then + -- For quality 0 (gray/junk), also check tooltip as fallback + if ruleValue == 0 then + if itemData.quality == 0 then + return true + end + -- Tooltip fallback for gray detection (current character only) + if not isOtherChar and addon.Modules.Utils and addon.Modules.Utils.IsItemGrayTooltip then + return addon.Modules.Utils:IsItemGrayTooltip(bagID, slotID, itemData.link) + end + return false + end return itemData.quality == ruleValue elseif ruleType == "qualityMin" then diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua index 1329e93..14831f7 100644 --- a/UI/SettingsPopup.lua +++ b/UI/SettingsPopup.lua @@ -1375,10 +1375,50 @@ function Guda_SettingsPopup_CategoriesTab_Update() end end - -- Set reset button text + -- Set button texts + local addBtn = getglobal("Guda_SettingsPopup_AddCategoryButton") + if addBtn then + addBtn:SetText("+ Add Category") + end + local resetBtn = getglobal("Guda_SettingsPopup_ResetCategoriesButton") if resetBtn then - resetBtn:SetText("Reset to Defaults") + resetBtn:SetText("Reset Defaults") + end +end + +-- Add new custom category +function Guda_SettingsPopup_AddCategory_OnClick() + if not Guda.Modules.CategoryManager then return end + + -- Generate unique ID + local baseId = "Custom" + local counter = 1 + local newId = baseId .. counter + + local cats = Guda.Modules.CategoryManager:GetCategories() + while cats.definitions[newId] do + counter = counter + 1 + newId = baseId .. counter + end + + -- Create new category definition + local newDef = { + name = "New Category " .. counter, + icon = "Interface\\Icons\\INV_Misc_QuestionMark", + rules = {}, + matchMode = "any", + priority = 50, + enabled = true, + isBuiltIn = false, + } + + -- Add to database + if Guda.Modules.CategoryManager:AddCategory(newId, newDef) then + -- Update display + Guda_SettingsPopup_CategoriesTab_Update() + -- Open editor for the new category + Guda_CategoryEditor_Open(newId) end end diff --git a/UI/SettingsPopup.xml b/UI/SettingsPopup.xml index 3dd7e30..84af32c 100644 --- a/UI/SettingsPopup.xml +++ b/UI/SettingsPopup.xml @@ -692,10 +692,10 @@ - - + + + From b0861bd8298f0d608386d544a332abaf08226900 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 14:41:42 +0400 Subject: [PATCH 04/10] feat: white name items in junk --- Core/CategoryManager.lua | 47 +++++++++++++++++++++++++++++++++++----- UI/SettingsPopup.lua | 2 ++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index ead4426..83ec221 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -183,9 +183,9 @@ local DEFAULT_CATEGORIES = { name = "Junk", icon = "Interface\\Icons\\INV_Misc_Gear_06", rules = { - { type = "quality", value = 0 } + { type = "isJunk", value = true } }, - matchMode = "all", + matchMode = "any", priority = 85, enabled = true, isBuiltIn = true, @@ -330,7 +330,7 @@ function CategoryManager:MigrateCategories() addon:Debug("CategoryManager: Migrated BoE priority to 75") end - -- Migrate Junk category to ensure proper rules and priority + -- Migrate Junk category to use isJunk rule type local junkCat = cats.definitions["Junk"] if junkCat and junkCat.isBuiltIn then local needsUpdate = false @@ -339,13 +339,23 @@ function CategoryManager:MigrateCategories() junkCat.priority = 85 needsUpdate = true end + -- Migrate from quality=0 rule to isJunk rule + if junkCat.rules then + for i, rule in ipairs(junkCat.rules) do + if rule.type == "quality" and rule.value == 0 then + junkCat.rules = { { type = "isJunk", value = true } } + needsUpdate = true + break + end + end + end -- Check if rules are missing or wrong if not junkCat.rules or table.getn(junkCat.rules) == 0 then - junkCat.rules = { { type = "quality", value = 0 } } + junkCat.rules = { { type = "isJunk", value = true } } needsUpdate = true end if needsUpdate then - addon:Debug("CategoryManager: Migrated Junk category") + addon:Debug("CategoryManager: Migrated Junk category to use isJunk rule") end end end @@ -601,6 +611,31 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar local tag = itemData.restoreTag if not tag then return false end return tag == ruleValue + + elseif ruleType == "isJunk" then + -- Junk items: gray items (quality 0) OR white equippable items (quality 1 + Weapon/Armor) + local quality = itemData.quality + local isGray = false + local isWhiteEquip = false + + -- Check for gray items (quality 0) + if quality == 0 then + isGray = true + elseif not isOtherChar and addon.Modules.Utils and addon.Modules.Utils.IsItemGrayTooltip then + -- Tooltip fallback for gray detection + isGray = addon.Modules.Utils:IsItemGrayTooltip(bagID, slotID, itemData.link) + end + + -- Check for white equippable items (quality 1 + Weapon/Armor) + if quality == 1 then + local itemClass = itemData.class or "" + if itemClass == "Weapon" or itemClass == "Armor" then + isWhiteEquip = true + end + end + + local isJunk = isGray or isWhiteEquip + return isJunk == ruleValue end return false @@ -696,8 +731,10 @@ function CategoryManager:GetRuleTypes() { id = "itemSubtype", name = "Item Subtype", description = "Match by item subclass (Cloth, Potion, etc.)" }, { id = "namePattern", name = "Name Pattern", description = "Match item name (supports Lua patterns)" }, { id = "quality", name = "Quality", description = "Match by item quality (0=Gray to 5=Legendary)" }, + { id = "qualityMin", name = "Quality (min)", description = "Match items with at least this quality" }, { id = "isBoE", name = "Bind on Equip", description = "Match items that bind when equipped" }, { id = "isQuestItem", name = "Quest Item", description = "Match quest items" }, + { id = "isJunk", name = "Is Junk", description = "Match junk items (gray + white equippable)" }, { id = "texturePattern", name = "Icon Pattern", description = "Match icon texture path" }, { id = "itemID", name = "Item ID", description = "Match specific item IDs" }, { id = "isSoulShard", name = "Soul Shard", description = "Match soul shards" }, diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua index 14831f7..a381d02 100644 --- a/UI/SettingsPopup.lua +++ b/UI/SettingsPopup.lua @@ -1470,6 +1470,7 @@ local RULE_TYPE_OPTIONS = { { id = "qualityMin", name = "Quality (min)" }, { id = "isBoE", name = "Bind on Equip" }, { id = "isQuestItem", name = "Quest Item" }, + { id = "isJunk", name = "Is Junk" }, { id = "restoreTag", name = "Restore Type" }, { id = "isSoulShard", name = "Soul Shard" }, { id = "isProjectile", name = "Projectile" }, @@ -1482,6 +1483,7 @@ local RULE_VALUE_OPTIONS = { qualityMin = { "0 - Poor", "1 - Common", "2 - Uncommon", "3 - Rare", "4 - Epic", "5 - Legendary" }, isBoE = { "true", "false" }, isQuestItem = { "true", "false" }, + isJunk = { "true", "false" }, isSoulShard = { "true", "false" }, isProjectile = { "true", "false" }, restoreTag = { "eat", "drink", "restore" }, From 055441eb31cb701b0228f46f65711d3d8f686805 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 15:10:02 +0400 Subject: [PATCH 05/10] feat: healthstone in footer --- UI/BagFrame.lua | 144 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 4c77081..c01020d 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -385,6 +385,9 @@ function BagFrame:Update() -- Update money self:UpdateMoney() + -- Update hearthstone + self:UpdateHearthstone() + -- Update bag slots info self:UpdateBagSlotsInfo(bagData, isOtherChar) @@ -935,6 +938,141 @@ function BagFrame:CreateMoneyFrame() addon:Debug("MoneyFrame created via CreateMoneyFrame") end +-- Hearthstone item ID +local HEARTHSTONE_ID = 6948 + +-- Create hearthstone display frame +function BagFrame:CreateHearthstoneFrame() + local frameName = "Guda_BagFrame_HearthstoneFrame" + if getglobal(frameName) then return end + + local frame = CreateFrame("Button", frameName, Guda_BagFrame) + frame:SetWidth(20) + frame:SetHeight(20) + frame:SetPoint("BOTTOMRIGHT", Guda_BagFrame, "BOTTOMRIGHT", -150, 16) + frame:SetFrameStrata("HIGH") + frame:SetFrameLevel(10) + + -- Icon texture + local icon = frame:CreateTexture(frameName .. "_Icon", "ARTWORK") + icon:SetAllPoints(frame) + icon:SetTexture("Interface\\Icons\\INV_Misc_Rune_01") + + -- Cooldown frame (use Model type with CooldownFrameTemplate in vanilla) + local cooldown = CreateFrame("Model", frameName .. "_Cooldown", frame, "CooldownFrameTemplate") + cooldown:SetAllPoints(frame) + cooldown:EnableMouse(false) + frame.cooldown = cooldown + + -- Enable mouse and clicks + frame:EnableMouse(true) + frame:RegisterForClicks("LeftButtonUp", "RightButtonUp") + + frame:SetScript("OnEnter", function() + BagFrame:ShowHearthstoneTooltip(this) + end) + frame:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + frame:SetScript("OnClick", function() + BagFrame:UseHearthstone() + end) + frame:SetScript("OnMouseUp", function() + if arg1 == "LeftButton" or arg1 == "RightButton" then + BagFrame:UseHearthstone() + end + end) + + addon:Debug("HearthstoneFrame created") +end + +-- Find hearthstone in bags +function BagFrame:FindHearthstone() + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) + for slot = 1, numSlots do + local link = GetContainerItemLink(bag, slot) + if link then + local itemId = addon.Modules.Utils:ExtractItemID(link) + if itemId and itemId == HEARTHSTONE_ID then + return bag, slot, link + end + end + end + end + return nil, nil, nil +end + +-- Update hearthstone display +function BagFrame:UpdateHearthstone() + local hideFooter = addon.Modules.DB:GetSetting("hideFooter") + local frame = getglobal("Guda_BagFrame_HearthstoneFrame") + + if hideFooter then + if frame then frame:Hide() end + return + end + + if not frame then + self:CreateHearthstoneFrame() + frame = getglobal("Guda_BagFrame_HearthstoneFrame") + end + + if not frame then return end + + local bag, slot, link = self:FindHearthstone() + + if bag then + frame:SetAlpha(1) + frame:Show() + + -- Update cooldown + local start, duration, enable = GetContainerItemCooldown(bag, slot) + if frame.cooldown and start and duration and duration > 0 then + CooldownFrame_SetTimer(frame.cooldown, start, duration, enable) + elseif frame.cooldown then + frame.cooldown:Hide() + end + + -- Store location for use + frame.bag = bag + frame.slot = slot + frame.link = link + else + frame:SetAlpha(0.3) + frame:Show() + frame.bag = nil + frame.slot = nil + frame.link = nil + if frame.cooldown then + frame.cooldown:Hide() + end + end +end + +-- Show hearthstone tooltip +function BagFrame:ShowHearthstoneTooltip(frame) + if frame.bag and frame.slot then + GameTooltip:SetOwner(frame, "ANCHOR_TOP") + GameTooltip:SetBagItem(frame.bag, frame.slot) + GameTooltip:Show() + else + GameTooltip:SetOwner(frame, "ANCHOR_TOP") + GameTooltip:ClearLines() + GameTooltip:AddLine("Hearthstone", 1, 1, 1) + GameTooltip:AddLine("Not in bags", 1, 0, 0) + GameTooltip:Show() + end +end + +-- Use hearthstone +function BagFrame:UseHearthstone() + local frame = getglobal("Guda_BagFrame_HearthstoneFrame") + if frame and frame.bag and frame.slot then + UseContainerItem(frame.bag, frame.slot) + end +end + -- Setup tooltip on toolbar empty space function BagFrame:SetupToolbarTooltip() local toolbar = getglobal("Guda_BagFrame_Toolbar") @@ -1889,17 +2027,21 @@ function BagFrame:UpdateFooterVisibility() local hideFooter = addon.Modules.DB:GetSetting("hideFooter") local toolbar = getglobal("Guda_BagFrame_Toolbar") local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame") + local hearthstoneFrame = getglobal("Guda_BagFrame_HearthstoneFrame") if hideFooter then if toolbar then toolbar:Hide() end if moneyFrame then moneyFrame:Hide() end + if hearthstoneFrame then hearthstoneFrame:Hide() end else if toolbar then toolbar:Show() end if moneyFrame then moneyFrame:Show() end - + if hearthstoneFrame then hearthstoneFrame:Show() end + -- Trigger layout updates to ensure they are correctly positioned self:UpdateBaglineLayout() self:UpdateMoney() + self:UpdateHearthstone() end end From ad8f26bca742370f5265cf93cccc0680724cd3ef Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 17:05:30 +0400 Subject: [PATCH 06/10] feat: rename category --- Core/CategoryManager.lua | 17 +++++++++++++--- UI/BagFrame.lua | 15 ++++++++++++-- UI/FrameHelpers.lua | 15 ++++++++++++-- UI/SettingsPopup.lua | 43 ++++++++++++++++++++++++++++++---------- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index 83ec221..3232c99 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -19,7 +19,7 @@ addon.Modules.CategoryManager = CategoryManager -- Default category definitions that replicate the existing hardcoded behavior local DEFAULT_CATEGORIES = { order = { - "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", + "Home", "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" }, @@ -212,6 +212,16 @@ local DEFAULT_CATEGORIES = { enabled = true, isBuiltIn = true, }, + ["Home"] = { + name = "Home", + icon = "Interface\\Icons\\INV_Misc_Rune_01", + rules = {}, + matchMode = "all", + priority = 0, + enabled = true, + isBuiltIn = true, + hideControls = true, + }, } } @@ -590,11 +600,12 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar -- ruleValue can be a single ID or a table of IDs if type(ruleValue) == "table" then for _, id in ipairs(ruleValue) do - if itemID == id then return true end + if itemID == tonumber(id) then return true end end return false else - return itemID == ruleValue + -- Convert string to number if needed + return itemID == tonumber(ruleValue) end elseif ruleType == "isSoulShard" then diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index c01020d..1510546 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -507,9 +507,16 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName) headerIdx = headerIdx + 1 header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY) header:SetWidth(blockWidth) - + + -- Get display name from category definition local displayName = catName - header.fullName = catName + if addon.Modules.CategoryManager then + local catDef = addon.Modules.CategoryManager:GetCategory(catName) + if catDef and catDef.name then + displayName = catDef.name + end + end + header.fullName = displayName header.isShortened = false if string.len(displayName) > 8 and numItems < 2 then displayName = string.sub(displayName, 1, 6) .. "..." @@ -574,6 +581,10 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName) break end end + -- Also check if Empty section should show + if totalFreeSlots > 0 then + hasAnyBottom = true + end if hasAnyBottom then y = y - 10 diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index 7dac7b9..6c48877 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -35,8 +35,19 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, -- Priority 1: Special items (Hearthstone, Mounts, Tools) -- These are handled separately and go into specialItems table, not categories if string.find(itemName, "Hearthstone") then - table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData}) - return + -- Only show in Home section if Home category is enabled + local showHome = true + if addon.Modules.CategoryManager then + local homeCat = addon.Modules.CategoryManager:GetCategory("Home") + if homeCat then + showHome = homeCat.enabled + end + end + if showHome then + table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData}) + return + end + -- If Home is disabled, fall through to normal categorization elseif addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData}) return diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua index a381d02..854dc3d 100644 --- a/UI/SettingsPopup.lua +++ b/UI/SettingsPopup.lua @@ -1345,17 +1345,37 @@ function Guda_SettingsPopup_CategoriesTab_Update() row.builtInText:Hide() end - -- Enable/disable move buttons based on position - if dataIndex == 1 then - row.upBtn:Disable() + -- Hide all controls for hideControls categories (only checkbox visible) + if categoryDef.hideControls then + row.editBtn:Hide() + row.upBtn:Hide() + row.downBtn:Hide() + row.deleteBtn:Hide() + row.builtInText:Hide() else - row.upBtn:Enable() - end + row.editBtn:Show() + row.upBtn:Show() + row.downBtn:Show() - if dataIndex == totalCategories then - row.downBtn:Disable() - else - row.downBtn:Enable() + -- Enable/disable move buttons based on position + if dataIndex == 1 then + row.upBtn:Disable() + else + -- Check if category above has hideControls (can't move above it) + local aboveCatId = categoryOrder[dataIndex - 1] + local aboveCatDef = Guda.Modules.CategoryManager:GetCategory(aboveCatId) + if aboveCatDef and aboveCatDef.hideControls then + row.upBtn:Disable() + else + row.upBtn:Enable() + end + end + + if dataIndex == totalCategories then + row.downBtn:Disable() + else + row.downBtn:Enable() + end end -- Set text color based on enabled state @@ -1404,11 +1424,11 @@ function Guda_SettingsPopup_AddCategory_OnClick() -- Create new category definition local newDef = { - name = "New Category " .. counter, + name = "Custom " .. counter, icon = "Interface\\Icons\\INV_Misc_QuestionMark", rules = {}, matchMode = "any", - priority = 50, + priority = 80, enabled = true, isBuiltIn = false, } @@ -1466,6 +1486,7 @@ local RULE_TYPE_OPTIONS = { { id = "itemType", name = "Item Type" }, { id = "itemSubtype", name = "Item Subtype" }, { id = "namePattern", name = "Name Contains" }, + { id = "itemID", name = "Item ID" }, { id = "quality", name = "Quality (exact)" }, { id = "qualityMin", name = "Quality (min)" }, { id = "isBoE", name = "Bind on Equip" }, From 28307aa1a2406e604505b33bd37d6b3b86dfd6e4 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 17:08:12 +0400 Subject: [PATCH 07/10] Hide hearthstone from bags --- UI/FrameHelpers.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index 6c48877..8c0de9f 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -45,9 +45,9 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, end if showHome then table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData}) - return end - -- If Home is disabled, fall through to normal categorization + -- Always return - if Home is disabled, Hearthstone is hidden completely + return elseif addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData}) return From 60e2d99799988b93f78b99fb57a2bebcf3895285 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 17:41:57 +0400 Subject: [PATCH 08/10] refactory: redunant code --- Core/CategoryManager.lua | 26 +- Core/Constants.lua | 166 +++++++++++ Core/Tooltip.lua | 440 ++++++++++++++---------------- Core/Utils.lua | 451 ++++++++++++++++-------------- Guda.toc | 1 + UI/FrameHelpers.lua | 24 +- UI/ItemButton.lua | 574 +++++++++++++++++++++++---------------- 7 files changed, 959 insertions(+), 723 deletions(-) create mode 100644 Core/Constants.lua diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index 3232c99..cc913b4 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -562,30 +562,8 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar return isBoE == ruleValue elseif ruleType == "isQuestItem" then - local isQuestItem = false - local itemCategory = itemData.class or itemData.category or "" - local itemType = itemData.type or "" - local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor") - local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") - - if isQuestCategory then - isQuestItem = true - elseif not isOtherChar then - isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) - if isQuestItem and isEquipment and not isQuestCategory then - isQuestItem = false - end - end - - -- Also check QuestItemsDB - if not isQuestItem and itemData.link then - local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) - if itemID and addon.IsQuestItemByID then - local playerFaction = UnitFactionGroup("player") - isQuestItem = addon:IsQuestItemByID(itemID, playerFaction) - end - end - + -- Use consolidated quest detection from Utils + local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false) return isQuestItem == ruleValue elseif ruleType == "texturePattern" then diff --git a/Core/Constants.lua b/Core/Constants.lua new file mode 100644 index 0000000..c7531da --- /dev/null +++ b/Core/Constants.lua @@ -0,0 +1,166 @@ +-- Guda Constants Module +-- Centralized location for all magic numbers and hardcoded values + +local addon = Guda + +-- Extend the existing Constants table from Init.lua +local C = addon.Constants + +--============================================================================= +-- Bag IDs +--============================================================================= +C.BAG_BACKPACK = 0 +C.BAG_FIRST = 1 +C.BAG_LAST = 4 +C.BANK_FIRST = 5 +C.BANK_LAST = 11 +C.KEYRING_BAG = -2 +C.BANK_CONTAINER = -1 + +-- Bag ID ranges for iteration +C.BAG_IDS = {0, 1, 2, 3, 4} +C.BANK_BAG_IDS = {5, 6, 7, 8, 9, 10, 11} +C.ALL_BAG_IDS = {0, 1, 2, 3, 4, -2} -- Including keyring + +--============================================================================= +-- Equipment Slots +--============================================================================= +C.EQUIPMENT_SLOT_FIRST = 1 +C.EQUIPMENT_SLOT_LAST = 19 + +--============================================================================= +-- Item IDs +--============================================================================= +C.SOUL_SHARD_ID = 6265 +C.HEARTHSTONE_ID = 6948 + +--============================================================================= +-- Colors (r, g, b tables for easy unpacking) +--============================================================================= +C.COLORS = { + -- Special item borders + KEYRING_CYAN = {r = 0.2, g = 0.8, b = 1.0}, + QUEST_GOLD = {r = 1.0, g = 0.82, b = 0}, + + -- Text colors + GRAY_TEXT = {r = 0.5, g = 0.5, b = 0.5}, + WHITE_TEXT = {r = 1.0, g = 1.0, b = 1.0}, + GOLD_TITLE = {r = 1.0, g = 0.82, b = 0}, + CYAN_LABEL = {r = 0, g = 1.0, b = 1.0}, + + -- Unusable item tint + UNUSABLE_RED = {r = 0.9, g = 0.2, b = 0.2, a = 0.45}, + + -- Lock/desaturate + LOCKED_GRAY = {r = 0.5, g = 0.5, b = 0.5}, +} + +--============================================================================= +-- UI Thresholds +--============================================================================= +C.ICON_SIZE_THRESHOLD = 44 -- Below this, use smaller insets/padding +C.ICON_INSET_SMALL = 10 -- Inset for small icons +C.ICON_INSET_LARGE = 15 -- Inset for large icons +C.ICON_TEXCOORD_CROP = 0.08 -- Texture coordinate crop amount + +--============================================================================= +-- Database / Cleanup +--============================================================================= +C.CLEANUP_OLD_CHARS_DAYS = 90 + +--============================================================================= +-- Tooltip Scanning +--============================================================================= +C.QUEST_TOOLTIP_PATTERNS = { + "quest starter", + "this item begins a quest", + "starts a quest", + "quest item", + "manual", +} + +C.BIND_ON_EQUIP_PATTERN = "binds when equipped" + +--============================================================================= +-- Specialized Bag Types +--============================================================================= +C.BAG_TYPES = { + SOUL = "soul", + HERB = "herb", + ENCHANT = "enchant", + QUIVER = "quiver", + AMMO = "ammo", +} + +-- Tooltip patterns for bag type detection +C.BAG_TYPE_PATTERNS = { + soul = {"soul bag", "soul pouch"}, + herb = {"herb bag"}, + enchant = {"enchanting bag"}, + quiver = {"quiver"}, + ammo = {"ammo pouch"}, +} + +--============================================================================= +-- Item Categories (for GetItemInfo) +--============================================================================= +C.ITEM_CATEGORIES = { + WEAPON = "Weapon", + ARMOR = "Armor", + CONSUMABLE = "Consumable", + CONTAINER = "Container", + TRADE_GOODS = "Trade Goods", + PROJECTILE = "Projectile", + QUIVER = "Quiver", + REAGENT = "Reagent", + RECIPE = "Recipe", + KEY = "Key", + MISCELLANEOUS = "Miscellaneous", + QUEST = "Quest", +} + +--============================================================================= +-- Money Formatting +--============================================================================= +C.MONEY = { + COPPER_PER_SILVER = 100, + SILVER_PER_GOLD = 100, + COPPER_PER_GOLD = 10000, +} + +-- Color codes for money display +C.MONEY_COLORS = { + GOLD = "|cFFFFD700", + SILVER = "|cFFC7C7CF", + COPPER = "|cFFEDA55F", + WHITE = "|cFFFFFFFF", +} + +--============================================================================= +-- Frame Constants +--============================================================================= +C.FRAME = { + TITLE_HEIGHT = 40, + SEARCH_BAR_HEIGHT = 30, + FOOTER_HEIGHT = 45, + FOOTER_HEIGHT_HIDDEN = 10, + MIN_WIDTH = 200, + MIN_HEIGHT = 150, + MAX_WIDTH = 1250, + MAX_HEIGHT = 1000, +} + +--============================================================================= +-- Bank Slots +--============================================================================= +C.BANK_MAIN_SLOTS = 24 -- Number of slots in main bank container + +--============================================================================= +-- Tooltip Hook Settings +--============================================================================= +C.TOOLTIP = { + DEBOUNCE_TIME = 0.2, -- Seconds to wait before clearing cache + MAX_MONEY_FRAMES = 8, -- Maximum number of money frames to search +} + +addon:Debug("Constants module loaded") diff --git a/Core/Tooltip.lua b/Core/Tooltip.lua index 01285ac..7c34881 100644 --- a/Core/Tooltip.lua +++ b/Core/Tooltip.lua @@ -4,261 +4,221 @@ local addon = Guda local Tooltip = {} addon.Modules.Tooltip = Tooltip +--============================================================================= +-- Item Counting Helper Functions (extracted for clarity and reuse) +--============================================================================= + -- Helper function to get item ID from link (Lua 5.0 compatible) local function GetItemIDFromLink(link) - if not link then return nil end + if not link then return nil end if type(link) == "number" then return link end - - -- Try to find itemID in a standard link or a raw item:ID string - local _, _, itemID = string.find(link, "item:(%d+)") - return itemID and tonumber(itemID) or nil + local _, _, itemID = string.find(link, "item:(%d+)") + return itemID and tonumber(itemID) or nil end + +-- Count items in saved bag/bank data structure +-- Used for both bags and bank counting from saved character data +local function CountFromSavedContainers(containersData, itemID) + local count = 0 + if not containersData or type(containersData) ~= "table" then + return count + end + + for bagID, bagData in pairs(containersData) do + if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then + for slotID, itemData in pairs(bagData.slots) do + if itemData and type(itemData) == "table" and itemData.link then + local slotItemID = GetItemIDFromLink(itemData.link) + if slotItemID == itemID then + count = count + (itemData.count or 1) + end + end + end + end + end + + return count +end + +-- Count items in saved mailbox data structure +local function CountFromSavedMailbox(mailboxData, itemID) + local count = 0 + if not mailboxData or type(mailboxData) ~= "table" then + return count + end + + for _, mail in ipairs(mailboxData) do + local itemsToCheck = mail.items or (mail.item and {mail.item}) or {} + for _, item in ipairs(itemsToCheck) do + local slotItemID = item.link and GetItemIDFromLink(item.link) + if slotItemID == itemID then + count = count + (item.count or 1) + elseif not slotItemID and item.name then + -- Fallback to name matching if link is missing + local targetName = GetItemInfo(itemID) + if targetName == item.name then + count = count + (item.count or 1) + end + end + end + end + + return count +end + +-- Count items in saved equipped data structure +local function CountFromSavedEquipped(equippedData, itemID) + local count = 0 + if not equippedData or type(equippedData) ~= "table" then + return count + end + + for slotName, itemData in pairs(equippedData) do + if itemData and type(itemData) == "table" and itemData.link then + local slotItemID = GetItemIDFromLink(itemData.link) + if slotItemID == itemID then + count = count + 1 + end + end + end + + return count +end + +-- Count items in live container (bags or bank) +local function CountFromLiveContainer(bagIDs, itemID) + local count = 0 + + for _, bagID in ipairs(bagIDs) do + local numSlots = GetContainerNumSlots(bagID) + if numSlots and numSlots > 0 then + for slot = 1, numSlots do + local link = GetContainerItemLink(bagID, slot) + if link then + local slotItemID = GetItemIDFromLink(link) + if slotItemID == itemID then + local _, itemCount = GetContainerItemInfo(bagID, slot) + count = count + (itemCount or 1) + end + end + end + end + end + + return count +end + +-- Count items in live mailbox +local function CountFromLiveMailbox(itemID) + local count = 0 + + if not (addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen()) then + return count + end + + local numInboxItems = GetInboxNumItems() + for i = 1, numInboxItems do + local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i) + if hasItem then + local numAttachments = GetInboxNumAttachments and GetInboxNumAttachments(i) or 1 + if numAttachments == 0 and hasItem then + numAttachments = 1 + end + + for j = 1, numAttachments do + local name, _, itemCount = GetInboxItem(i, j) + if name then + local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j) + if itemLink then + local slotItemID = GetItemIDFromLink(itemLink) + if slotItemID == itemID then + count = count + (itemCount or 1) + end + end + end + end + end + end + + return count +end + +-- Count items in live equipment slots +local function CountFromLiveEquipped(itemID) + local count = 0 + + for slotID = 1, 19 do + local link = GetInventoryItemLink("player", slotID) + if link then + local slotItemID = GetItemIDFromLink(link) + if slotItemID == itemID then + count = count + 1 + end + end + end + + return count +end + +--============================================================================= +-- Main Counting Functions +--============================================================================= + +-- Count items for current character using live game data local function CountCurrentCharacterItems(itemID) - local bagCount = 0 - local bankCount = 0 - local mailCount = 0 - local equippedCount = 0 + local bagCount = 0 + local bankCount = 0 + local mailCount = 0 + local equippedCount = 0 - -- Count current character's bags in real-time - local bagsToCount = {0, 1, 2, 3, 4, -2} - for _, bagID in ipairs(bagsToCount) do - local numSlots = GetContainerNumSlots(bagID) - for slot = 1, numSlots do - local link = GetContainerItemLink(bagID, slot) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - local _, count = GetContainerItemInfo(bagID, slot) - bagCount = bagCount + (count or 1) - end - end - end - end + -- Count bags in real-time + bagCount = CountFromLiveContainer({0, 1, 2, 3, 4, -2}, itemID) - -- Count current character's bank in real-time if bank is open - local bankFrame = getglobal("BankFrame") - if bankFrame and bankFrame:IsVisible() then - -- Main bank slots (-1) - local numMainSlots = GetContainerNumSlots(-1) or 24 - for slot = 1, numMainSlots do - local link = GetContainerItemLink(-1, slot) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - local _, count = GetContainerItemInfo(-1, slot) - bankCount = bankCount + (count or 1) - end - end - end + -- Count bank: live if open, otherwise from saved data + local bankFrame = getglobal("BankFrame") + if bankFrame and bankFrame:IsVisible() then + -- Main bank + bank bags + bankCount = CountFromLiveContainer({-1, 5, 6, 7, 8, 9, 10, 11}, itemID) + else + -- Use saved data + local playerName = addon.Modules.DB:GetPlayerFullName() + local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] + if charData then + bankCount = CountFromSavedContainers(charData.bank, itemID) + end + end - -- Bank bags (5-11) - for bagID = 5, 11 do - local numSlots = GetContainerNumSlots(bagID) - if numSlots and numSlots > 0 then - for slot = 1, numSlots do - local link = GetContainerItemLink(bagID, slot) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - local _, count = GetContainerItemInfo(bagID, slot) - bankCount = bankCount + (count or 1) - end - end - end - end - end - else - -- Bank not open - use saved data for bank counts - local playerName = addon.Modules.DB:GetPlayerFullName() - local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] - if charData and charData.bank and type(charData.bank) == "table" then - for bagID, bagData in pairs(charData.bank) do - if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then - for slotID, itemData in pairs(bagData.slots) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - bankCount = bankCount + (itemData.count or 1) - end - end - end - end - end - end - end + -- Count mailbox: live if open, otherwise from saved data + if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then + mailCount = CountFromLiveMailbox(itemID) + else + local playerName = addon.Modules.DB:GetPlayerFullName() + local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] + if charData then + mailCount = CountFromSavedMailbox(charData.mailbox, itemID) + end + end - -- Count current character's mailbox in real-time if mailbox is open - if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then - local numInboxItems = GetInboxNumItems() - for i = 1, numInboxItems do - local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i) - if hasItem then - -- Turtle WoW supports up to 12 attachments per mail. - -- We use GetInboxNumAttachments if available to avoid over-scanning. - local numAttachments = 0 - if GetInboxNumAttachments then - numAttachments = GetInboxNumAttachments(i) or 0 - end + -- Count equipped items in real-time + equippedCount = CountFromLiveEquipped(itemID) - -- Fallback: if we don't have the count but header says there's an item, assume at least 1. - if numAttachments == 0 and hasItem then - numAttachments = 1 - end - - for j = 1, numAttachments do -- Turtle WoW supports up to 12 attachments - local name, _, count = GetInboxItem(i, j) - if name then - local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j) - if itemLink then - local slotItemID = GetItemIDFromLink(itemLink) - if slotItemID == itemID then - mailCount = mailCount + (count or 1) - end - end - end - end - end - end - else - -- Mailbox not open - use saved data - local playerName = addon.Modules.DB:GetPlayerFullName() - local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] - if charData and charData.mailbox and type(charData.mailbox) == "table" then - for _, mail in ipairs(charData.mailbox) do - if mail.items then - for _, item in ipairs(mail.items) do - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - elseif mail.item then -- Fallback for single item data structure - local item = mail.item - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - end - end - end - - -- Count equipped items in real-time - for slotID = 1, 19 do -- All equipment slots - local link = GetInventoryItemLink("player", slotID) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - equippedCount = equippedCount + 1 - end - end - end - - return bagCount, bankCount, equippedCount, mailCount + return bagCount, bankCount, equippedCount, mailCount end --- Count items for a specific character with real-time data for current character +-- Count items for a specific character (current or other) local function CountItemsForCharacter(itemID, characterData, isCurrentChar) --- For current character, use real-time counting to avoid database sync issues - if isCurrentChar then - return CountCurrentCharacterItems(itemID) - end + -- For current character, use real-time counting + if isCurrentChar then + return CountCurrentCharacterItems(itemID) + end - -- For other characters, use saved data - local bagCount = 0 - local bankCount = 0 - local mailCount = 0 - local equippedCount = 0 + -- For other characters, use saved data + local bagCount = CountFromSavedContainers(characterData.bags, itemID) + local bankCount = CountFromSavedContainers(characterData.bank, itemID) + local mailCount = CountFromSavedMailbox(characterData.mailbox, itemID) + local equippedCount = CountFromSavedEquipped(characterData.equipped, itemID) - -- Count bags from saved data - if characterData.bags and type(characterData.bags) == "table" then - for bagID, bagData in pairs(characterData.bags) do - if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then - for slotID, itemData in pairs(bagData.slots) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - bagCount = bagCount + (itemData.count or 1) - end - end - end - end - end - end - - -- Count bank from saved data - if characterData.bank and type(characterData.bank) == "table" then - for bagID, bagData in pairs(characterData.bank) do - if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then - for slotID, itemData in pairs(bagData.slots) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - bankCount = bankCount + (itemData.count or 1) - end - end - end - end - end - end - - -- Count mailbox from saved data - if characterData.mailbox and type(characterData.mailbox) == "table" then - for _, mail in ipairs(characterData.mailbox) do - if mail.items then - for _, item in ipairs(mail.items) do - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - elseif mail.item then -- Fallback for single item data structure - local item = mail.item - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - end - end - - -- Count equipped items from saved data - if characterData.equipped and type(characterData.equipped) == "table" then - for slotName, itemData in pairs(characterData.equipped) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - equippedCount = equippedCount + 1 - end - end - end - end - - return bagCount, bankCount, equippedCount, mailCount + return bagCount, bankCount, equippedCount, mailCount end diff --git a/Core/Utils.lua b/Core/Utils.lua index 0564cfd..5875a54 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -5,6 +5,64 @@ local addon = Guda local Utils = {} addon.Modules.Utils = Utils +--============================================================================= +-- SafeCall: Nil-safe module method invocation +-- Replaces verbose nil-checks like: +-- if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.Method then +-- return addon.Modules.Utils:Method(arg1, arg2) +-- end +-- With: +-- return Utils:SafeCall("Utils", "Method", arg1, arg2) +--============================================================================= + +-- Call a method on a module safely, returns nil if module/method doesn't exist +-- Parameters: +-- moduleName: Name of the module in addon.Modules (e.g., "Utils", "DB", "BagFrame") +-- methodName: Name of the method to call (e.g., "GetQualityColor", "GetSetting") +-- ...: Arguments to pass to the method +-- Returns: The return value(s) of the method, or nil if not callable +function Utils:SafeCall(moduleName, methodName, ...) + if not addon or not addon.Modules then + return nil + end + + local module = addon.Modules[moduleName] + if not module then + return nil + end + + local method = module[methodName] + if not method or type(method) ~= "function" then + return nil + end + + -- Call method with module as self (for : style calls) + return method(module, unpack(arg)) +end + +-- Check if a module method exists without calling it +function Utils:HasMethod(moduleName, methodName) + if not addon or not addon.Modules then + return false + end + + local module = addon.Modules[moduleName] + if not module then + return false + end + + local method = module[methodName] + return method ~= nil and type(method) == "function" +end + +-- Get a module reference safely +function Utils:GetModule(moduleName) + if not addon or not addon.Modules then + return nil + end + return addon.Modules[moduleName] +end + -- Format money (copper to gold/silver/copper string) - WoW 1.12.1 version function Utils:FormatMoney(copper, showZero, useColors) if not copper or copper == 0 then @@ -190,49 +248,123 @@ local function GetScanTooltip() return scanTooltip end --- Check if an item is a quest item by scanning its tooltip -function Utils:IsQuestItemTooltip(bagID, slotID) - if not bagID or not slotID then return false end +-- Check if an item is a quest item by scanning its tooltip (internal helper) +-- Returns: isQuestItem, isQuestStarter +local function ScanTooltipForQuest(tooltip, tooltipName) + local isQuestItem = false + local isQuestStarter = false - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetBagItem(bagID, slotID) - - -- Check tooltip lines for explicit quest-related phrases (case-insensitive) for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + local line = getglobal(tooltipName .. "TextLeft" .. i) if line then local text = line:GetText() if text then local tl = string.lower(text) - -- Only match explicit quest markers to avoid misclassifying consumables/recipes - -- Include "manual" which some quest items use (same as ItemButton detection) + -- Check for quest starter patterns first if string.find(tl, "quest starter") or string.find(tl, "this item begins a quest") or - string.find(tl, "starts a quest") or - string.find(tl, "quest item") or - string.find(tl, "manual") then - - -- Double check category to avoid misidentifying equipment with "Use:" - local link = GetContainerItemLink(bagID, slotID) - if link and self.ExtractItemID and self.GetItemInfoSafe then - local itemID = self:ExtractItemID(link) - if itemID then - local _, _, _, _, itemCategory, itemType = self:GetItemInfoSafe(itemID) - -- If it's Weapon or Armor, and NOT categorized as Quest, then it's not a Quest Item - if (itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor") and - (itemCategory ~= "Quest" and itemType ~= "Quest") then - return false - end - end - end - - return true + string.find(tl, "starts a quest") then + isQuestItem = true + isQuestStarter = true + break + -- Check for regular quest item patterns + elseif string.find(tl, "quest item") or + string.find(tl, "manual") then + isQuestItem = true + -- Don't break, might still find a quest starter pattern end end end end - return false + + return isQuestItem, isQuestStarter +end + +-- Consolidated quest item detection function +-- Handles tooltip scanning, category checks, equipment filtering, and QuestItemsDB lookup +-- Parameters: +-- bagID, slotID: Required for tooltip scanning (can be nil for other char items) +-- itemData: Optional item data table with link, class/category, type fields +-- isOtherChar: Boolean, true if checking an item from another character's saved data +-- isBank: Boolean, true if the item is in the bank +-- Returns: isQuestItem (boolean), isQuestStarter (boolean) +function Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, isBank) + bagID = tonumber(bagID) + slotID = tonumber(slotID) + + local isQuestItem = false + local isQuestStarter = false + + -- Get item link and category info + local itemLink = itemData and itemData.link + local itemCategory = itemData and (itemData.class or itemData.category) or "" + local itemType = itemData and itemData.type or "" + local itemID + + -- For live items, query the link directly + if not isOtherChar and bagID and slotID then + itemLink = GetContainerItemLink(bagID, slotID) + end + + if itemLink then + itemID = self:ExtractItemID(itemLink) + if itemID then + local _, _, _, _, cat, typ = self:GetItemInfoSafe(itemID) + itemCategory = cat or itemCategory + itemType = typ or itemType + end + end + + -- Check if item is equipment (should not be classified as quest unless explicitly Quest category) + local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or + itemType == "Weapon" or itemType == "Armor") + local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") + + -- Priority 1: If explicitly categorized as Quest, it's a quest item + if isQuestCategory then + return true, false + end + + -- Priority 2: Tooltip scanning for current character items + if not isOtherChar and bagID and slotID then + local tooltip = GetScanTooltip() + tooltip:ClearLines() + + -- Handle bank items differently + if isBank and bagID == -1 then + if tooltip.SetInventoryItem then + tooltip:SetInventoryItem("player", 39 + slotID) + else + tooltip:SetBagItem(bagID, slotID) + end + else + tooltip:SetBagItem(bagID, slotID) + end + + isQuestItem, isQuestStarter = ScanTooltipForQuest(tooltip, "GudaBagScanTooltip") + + -- Filter out equipment that has quest-like text but isn't categorized as Quest + if isQuestItem and isEquipment and not isQuestCategory then + isQuestItem = false + isQuestStarter = false + end + end + + -- Priority 3: Check QuestItemsDB for known faction-specific quest items + if not isQuestItem and itemID and addon.IsQuestItemByID then + local playerFaction = UnitFactionGroup("player") + if addon:IsQuestItemByID(itemID, playerFaction) then + isQuestItem = true + end + end + + return isQuestItem, isQuestStarter +end + +-- Legacy compatibility wrapper - keep the old function name working +function Utils:IsQuestItemTooltip(bagID, slotID) + local isQuest, _ = self:IsQuestItem(bagID, slotID, nil, false, false) + return isQuest end -- Check if an item has a gray title in its tooltip or link @@ -296,129 +428,8 @@ function Utils:TruncateText(text, maxLen) return string.sub(text, 1, maxLen - 3) .. "..." end -function Utils:IsAmmoQuiverBag(bagID) - -- Skip backpack, bank, and keyring - if bagID == 0 or bagID == -1 or bagID == -2 then - return false - end - - -- Get the bag item - local invSlot = ContainerIDToInventoryID(bagID) - if not invSlot then - return false - end - - local link = GetInventoryItemLink("player", invSlot) - if not link then - return false - end - - -- Use tooltip scanning to get item class (more reliable in 1.12.1) - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetInventoryItem("player", invSlot) - - -- Scan tooltip lines for "Quiver" or "Ammo Pouch" - for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - -- Check if the line contains "Quiver" or "Ammo Pouch" - if string.find(text, "Quiver") or string.find(text, "Ammo Pouch") then - return true - end - end - end - end - - 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 - if bagID == 0 or bagID == -1 or bagID == -2 then - return false - end - - -- Get the bag item - local invSlot = ContainerIDToInventoryID(bagID) - if not invSlot then - return false - end - - local link = GetInventoryItemLink("player", invSlot) - if not link then - return false - end - - -- First try GetSpecializedBagType - local bagType = self:GetSpecializedBagType(bagID) - if bagType == "soul" then - return true - end - - -- Fallback: Use tooltip scanning (more reliable in 1.12.1) - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetInventoryItem("player", invSlot) - - -- Scan tooltip lines for "Soul Bag" or "Soul Pouch" - for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - -- Check if the line contains "Soul Bag" or "Soul Pouch" or just "Soul" in bag name - local textLower = string.lower(text) - if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or - (string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then - return true - end - end - end - end - - return false -end - -- Returns: "soul", "herb", "enchant", "quiver", "ammo", or nil +-- This is the consolidated bag type detection function with tooltip fallback function Utils:GetSpecializedBagType(bagID) -- Skip backpack, bank, and keyring if bagID == 0 or bagID == -1 or bagID == -2 then @@ -437,44 +448,94 @@ function Utils:GetSpecializedBagType(bagID) end local itemID = self:ExtractItemID(link) - if not itemID then - return nil + + -- Try GetItemInfo first (more reliable when available) + if itemID then + local _, _, _, _, _, itemType = self:GetItemInfoSafe(itemID) + if itemType then + local typeLower = string.lower(itemType) + + if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then + return "soul" + end + if string.find(typeLower, "herb bag") then + return "herb" + end + if string.find(typeLower, "enchanting bag") then + return "enchant" + end + if string.find(typeLower, "quiver") then + return "quiver" + end + if string.find(typeLower, "ammo pouch") then + return "ammo" + end + end end - local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType = self:GetItemInfoSafe(itemID) - if itemType then - -- Check for exact subtype matches - local typeLower = string.lower(itemType) + -- Fallback: tooltip scanning for all bag types + local tooltip = GetScanTooltip() + tooltip:ClearLines() + tooltip:SetInventoryItem("player", invSlot) - -- Soul Bag / Soul Pouch - if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then - return "soul" - end + for i = 1, tooltip:NumLines() do + local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + if line then + local text = line:GetText() + if text then + local textLower = string.lower(text) - -- Herb Bag - if string.find(typeLower, "herb bag") then - return "herb" - end + -- Soul Bag / Soul Pouch + if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or + (string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then + return "soul" + end - -- Enchanting Bag - if string.find(typeLower, "enchanting bag") then - return "enchant" - end + -- Herb Bag + if string.find(textLower, "herb bag") then + return "herb" + end - -- Quiver - if string.find(typeLower, "quiver") then - return "quiver" - end + -- Enchanting Bag + if string.find(textLower, "enchanting bag") then + return "enchant" + end - -- Ammo Pouch - if string.find(typeLower, "ammo pouch") then - return "ammo" + -- Quiver + if string.find(textLower, "quiver") then + return "quiver" + end + + -- Ammo Pouch + if string.find(textLower, "ammo pouch") then + return "ammo" + end + end end end return nil end +-- Simple helper to check if a bag is of a specific type +function Utils:IsBagType(bagID, bagType) + return self:GetSpecializedBagType(bagID) == bagType +end + +-- Convenience wrappers for common bag type checks +function Utils:IsAmmoQuiverBag(bagID) + local bagType = self:GetSpecializedBagType(bagID) + return bagType == "quiver" or bagType == "ammo" +end + +function Utils:IsHerbBag(bagID) + return self:IsBagType(bagID, "herb") +end + +function Utils:IsSoulBag(bagID) + return self:IsBagType(bagID, "soul") +end + -- Get container priority for sorting (higher = more important) function Utils:GetContainerPriority(bagID) local bagType = self:GetSpecializedBagType(bagID) @@ -727,37 +788,7 @@ function Utils:IsEnchantingItem(itemLink) return false end --- Check if a bag is Enchanting Bag (parallel to IsHerbBag) +-- Check if a bag is Enchanting Bag function Utils:IsEnchantBag(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 == "enchant" then return true end - - -- Fallback: tooltip scan for "Enchanting 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), "enchanting bag") then - return true - end - end - end - - return false + return self:IsBagType(bagID, "enchant") end \ No newline at end of file diff --git a/Guda.toc b/Guda.toc index 029233b..8daa777 100644 --- a/Guda.toc +++ b/Guda.toc @@ -9,6 +9,7 @@ Localization.lua Core\Init.lua +Core\Constants.lua Core\Database.lua DB\QuestItems.lua Core\Events.lua diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index 8c0de9f..6590377 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -82,28 +82,8 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end - -- Priority 3: Quest Items - local isQuestItem = false - local itemCategory = itemData.class or itemData.category or "" - local itemType = itemData.type or "" - local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor") - local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") - - if isQuestCategory then - isQuestItem = true - elseif not isOtherChar then - isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) - if isQuestItem and isEquipment and not isQuestCategory then - isQuestItem = false - end - end - if not isQuestItem and itemData.link then - local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) - if itemID and addon.IsQuestItemByID then - local playerFaction = UnitFactionGroup("player") - isQuestItem = addon:IsQuestItemByID(itemID, playerFaction) - end - end + -- Priority 3: Quest Items (use consolidated detection) + local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false) if isQuestItem then table.insert(categories["Quest"], {bagID = bagID, slotID = slotID, itemData = itemData}) return diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index e094e5f..67e4b51 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -8,90 +8,13 @@ local nextButtonID = 1 local scanTooltip = CreateFrame("GameTooltip", "Guda_QuestScanTooltip", nil, "GameTooltipTemplate") scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") --- Check if an item is a quest item by scanning its tooltip --- Check if an item is a quest item by scanning its tooltip and determine type +-- Helper function to check if an item is a quest item +-- Delegates to consolidated Utils:IsQuestItem() function local function IsQuestItem(bagID, slotID, isBank) - bagID = tonumber(bagID) - slotID = tonumber(slotID) - if not bagID or not slotID then return false end - - scanTooltip:ClearLines() - if isBank and bagID == -1 then - if scanTooltip.SetInventoryItem then - scanTooltip:SetInventoryItem("player", 39 + slotID) - else - scanTooltip:SetBagItem(bagID, slotID) - end - else - scanTooltip:SetBagItem(bagID, slotID) + if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsQuestItem then + return addon.Modules.Utils:IsQuestItem(bagID, slotID, nil, false, isBank) end - - local isQuestItem = false - local isQuestStarter = false - - -- Check all tooltip lines for quest-related text - for i = 1, scanTooltip:NumLines() do - local line = getglobal("Guda_QuestScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - -- Check for quest starter patterns - if string.find(text, "Quest Starter") or - string.find(text, "This Item Begins a Quest") or - string.find(text, "Use: Starts a Quest") then - isQuestItem = true - isQuestStarter = true - break - -- Check for regular quest item patterns - elseif string.find(text, "Quest Item") or - string.find(text, "Manual") then - isQuestItem = true - -- Don't break, might still find a quest starter pattern - end - end - end - end - - -- Also check item category/type via GetItemInfo for "Quest" - -- Turtle WoW GetItemInfo returns: name, link, rarity, level, itemCategory, itemType, stack, subType, texture, equipLoc, sellPrice - local link = GetContainerItemLink(bagID, slotID) - local itemID - local itemCategory, itemType - - if link and addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.ExtractItemID and addon.Modules.Utils.GetItemInfoSafe then - itemID = addon.Modules.Utils:ExtractItemID(link) - if itemID then - _, _, _, _, itemCategory, itemType = addon.Modules.Utils:GetItemInfoSafe(itemID) - end - end - - -- If it's a Weapon or Armor, it shouldn't be a QuestItem unless it's specifically categorized as Quest - -- This avoids "Use:" equipment showing up in the quest bar - if itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor" then - if itemCategory ~= "Quest" and itemType ~= "Quest" then - isQuestItem = false - isQuestStarter = false - end - end - - if not isQuestItem then - if itemCategory == "Quest" or itemType == "Quest" then - isQuestItem = true - end - end - - -- Check the QuestItemsDB for known faction-specific quest items - if not isQuestItem then - if itemID and addon.IsQuestItemByID then - local playerFaction = UnitFactionGroup("player") - local isDBQuestItem = addon:IsQuestItemByID(itemID, playerFaction) - if isDBQuestItem then - isQuestItem = true - end - end - end - - return isQuestItem, isQuestStarter + return false, false end --===================================================== @@ -466,50 +389,31 @@ function Guda_ItemButton_UpdateCooldown(self) end end --- Set item data -function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) - -- Proactively convert to number to avoid comparisons with strings in downstream functions - bagID = tonumber(bagID) - slotID = tonumber(slotID) - - -- Proactively clear any previous cooldown overlay state before reassigning this pooled button - do - local cd = getglobal(self:GetName().."Cooldown") or self.cooldown - if cd then - if CooldownFrame_SetTimer then - CooldownFrame_SetTimer(cd, 0, 0, 0) - elseif CooldownFrame_Set then - CooldownFrame_Set(cd, 0, 0, 0) - end - if cd.Hide then cd:Hide() end - end - end +--===================================================== +-- Helper functions for SetItem (extracted for clarity) +--===================================================== - -- Reset all visual state before reassigning pooled button +-- Reset all visual state on a button (for reuse from pool) +local function ResetButtonVisualState(self) if self.questBorder then self.questBorder:Hide() end if self.questIcon then self.questIcon:Hide() end if self.qualityBorder then self.qualityBorder:Hide() end if self.unusableOverlay then self.unusableOverlay:Hide() end - self.bagID = bagID - self.slotID = slotID - -- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior - -- ALWAYS set ID to something (0 if nil) to avoid leaking old IDs when button is reused - if self.SetID then - self:SetID(slotID or 0) + -- Clear cooldown overlay + local cd = getglobal(self:GetName().."Cooldown") or self.cooldown + if cd then + if CooldownFrame_SetTimer then + CooldownFrame_SetTimer(cd, 0, 0, 0) + elseif CooldownFrame_Set then + CooldownFrame_Set(cd, 0, 0, 0) + end + if cd.Hide then cd:Hide() end end - -- Explicitly set bag index to avoid Blizzard's ContainerFrameItemButton_OnEnter logic - -- from picking up this button as part of a real bag. - self.bagIndex = bagID or -100 -- Use an invalid bag index for non-bag buttons - self.itemData = itemData - self.isBank = isBank or false - self.otherChar = otherCharName - self.isReadOnly = isReadOnly or false -- Track if this is read-only mode - self.isMail = false -- Clear mailbox flag by default - self.mailIndex = nil - self.mailItemIndex = nil +end - -- Re-register for drag/drop every time (crucial for button reuse in Classic/Vanilla) +-- Configure drag/drop registration based on read-only state +local function SetupDragDrop(self) if not self.isReadOnly and not self.otherChar then if self.RegisterForDrag then self:RegisterForDrag("LeftButton") @@ -529,137 +433,364 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha self:EnableMouse(true) -- Still enable mouse for tooltips end end +end - -- Default to true if not specified (for non-filtered displays) +-- Get display texture and count for an item slot +-- Returns: texture, count, hasItem (boolean) +local function GetItemDisplayInfo(bagID, slotID, itemData, isReadOnly) + local displayTexture, displayCount + local hasItem = false + + if not isReadOnly then + -- LIVE MODE: Query game state directly + local liveTexture, liveCount = GetContainerItemInfo(bagID, slotID) + if liveTexture then + displayTexture = liveTexture + displayCount = liveCount + hasItem = true + elseif bagID == -2 then + -- Fallback for keyring in 1.12.1 + local link = GetContainerItemLink(bagID, slotID) + if link then + hasItem = true + local _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(link) + displayTexture = itemTexture + displayCount = 1 + end + end + else + -- READ-ONLY MODE: Use cached itemData + if itemData and itemData.texture then + displayTexture = itemData.texture + displayCount = itemData.count + hasItem = true + end + end + + return displayTexture, displayCount, hasItem +end + +-- Update the tracking checkmark on an item button +local function UpdateTrackingCheckmark(self, Utils) + local check = getglobal(self:GetName().."_Check") + if not check then return end + + local isTracked = false + if self.hasItem and self.itemData and self.itemData.link then + local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(self.itemData.link) + if itemID then + local trackedItems = Utils and Utils.SafeCall and Utils:SafeCall("DB", "GetSetting", "trackedItems") or {} + if trackedItems[itemID] then + isTracked = true + end + end + end + + if isTracked then + check:Show() + else + check:Hide() + end +end + +-- Resize empty slot background to match icon size +local function UpdateEmptySlotBackground(self, emptySlotBg, iconSize) + if not emptySlotBg then return end + + emptySlotBg:ClearAllPoints() + -- Use smaller padding for small icons + local bgPadding = iconSize < 44 and 1 or 2 + emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", -bgPadding, bgPadding) + emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", bgPadding, -bgPadding) + emptySlotBg:SetTexCoord(0.1, 0.9, 0.1, 0.9) +end + +-- Resize texture elements to match button size +local function ResizeTextureElements(self, iconSize) + -- Pushed texture + local pushedTexture = getglobal(self:GetName().."PushedTexture") + if not pushedTexture and self.GetPushedTexture then + pushedTexture = self:GetPushedTexture() + end + if pushedTexture then + pushedTexture:ClearAllPoints() + pushedTexture:SetPoint("CENTER", self, "CENTER", 0, 0) + pushedTexture:SetWidth(iconSize) + pushedTexture:SetHeight(iconSize) + end + + -- Highlight texture + local highlightTexture = getglobal(self:GetName().."HighlightTexture") + if not highlightTexture and self.GetHighlightTexture then + highlightTexture = self:GetHighlightTexture() + end + if highlightTexture then + highlightTexture:ClearAllPoints() + highlightTexture:SetPoint("CENTER", self, "CENTER", 0, 0) + highlightTexture:SetWidth(iconSize) + highlightTexture:SetHeight(iconSize) + end + + -- Checked texture + local checkedTexture = getglobal(self:GetName().."CheckedTexture") + if not checkedTexture and self.GetCheckedTexture then + checkedTexture = self:GetCheckedTexture() + end + if checkedTexture then + checkedTexture:ClearAllPoints() + checkedTexture:SetPoint("CENTER", self, "CENTER", 0, 0) + checkedTexture:SetWidth(iconSize) + checkedTexture:SetHeight(iconSize) + end +end + +-- Position icon and borders based on icon size +local function PositionIconAndBorders(self, iconSize) + local iconTexture = getglobal(self:GetName().."IconTexture") + if not iconTexture then + iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon + end + + if not iconTexture or not self.hasItem then return end + + -- Calculate icon inset based on size + local iconInset = iconSize < 44 and 10 or 15 + local iconDisplaySize = iconSize - iconInset + + iconTexture:ClearAllPoints() + iconTexture:SetPoint("CENTER", self, "CENTER", -0.5, 0.5) + iconTexture:SetWidth(iconDisplaySize) + iconTexture:SetHeight(iconDisplaySize) + iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92) + iconTexture:Show() + + -- Position quality border around the icon + if self.qualityBorder then + self.qualityBorder:ClearAllPoints() + self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) + self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) + end + + -- Position quest border around the icon + if self.questBorder then + self.questBorder:ClearAllPoints() + self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) + self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) + end + + -- Position quest icon in top-right corner + if self.questIcon then + local questIconSize = math.max(12, math.min(20, iconSize * 0.35)) + self.questIcon:SetWidth(questIconSize) + self.questIcon:SetHeight(questIconSize) + self.questIcon:ClearAllPoints() + self.questIcon:SetPoint("TOPRIGHT", self, "TOPRIGHT", 1, 0) + end +end + +-- Update quality border display +local function UpdateQualityBorder(self, itemQuality, itemLink, bagID, Utils) + if not self.qualityBorder then return end + + if bagID == -2 then + -- Special border for keyring items (cyan/blue) + self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1) + self.qualityBorder:Show() + return + end + + if not itemQuality then + self.qualityBorder:Hide() + return + end + + -- Check settings + local showEquipmentBorder, showOtherBorder + if Utils and Utils.SafeCall then + showEquipmentBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderEquipment") + showOtherBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderOther") + end + + if showEquipmentBorder == nil then showEquipmentBorder = true end + if showOtherBorder == nil then showOtherBorder = true end + + -- Check if item is equipment + local isEquipment = false + if itemLink and Utils and Utils.IsEquipment then + isEquipment = Utils:IsEquipment(itemLink) + end + + local shouldShowBorder = (isEquipment and showEquipmentBorder) or (not isEquipment and showOtherBorder) + + if shouldShowBorder then + local r, g, b = 1, 1, 1 + if Utils and Utils.GetQualityColor then + r, g, b = Utils:GetQualityColor(itemQuality) + end + self.qualityBorder:SetBackdropBorderColor(r, g, b, 1) + self.qualityBorder:Show() + else + self.qualityBorder:Hide() + end +end + +-- Clear item button for empty slot +local function ClearItemButton(self, emptySlotBg, countText, bagID) + self.hasItem = false + + if SetItemButtonTexture then SetItemButtonTexture(self, nil) end + if SetItemButtonCount then SetItemButtonCount(self, 0) end + if SetItemButtonDesaturated then SetItemButtonDesaturated(self, false) end + + -- Clear cooldown overlay + local cooldown = getglobal(self:GetName().."Cooldown") or self.cooldown + if cooldown and cooldown.Hide then cooldown:Hide() end + + -- Clear icon texture + local iconTexture = getglobal(self:GetName().."IconTexture") + if not iconTexture then + iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon + end + if iconTexture then + iconTexture:SetTexture(nil) + iconTexture:Hide() + end + + -- Clear unusable tint + if SetItemButtonTextureVertexColor then + SetItemButtonTextureVertexColor(self, 1.0, 1.0, 1.0) + end + if self.unusableOverlay and self.unusableOverlay.Hide then + self.unusableOverlay:Hide() + end + + -- Hide normal texture + self:SetNormalTexture("") + local normalBorder = getglobal(self:GetName().."NormalTexture") + if normalBorder then normalBorder:SetTexture("") end + + -- Show/hide empty slot background + if emptySlotBg then + emptySlotBg:Show() + emptySlotBg:SetAlpha(0.5) + end + + if countText then countText:Hide() end + + -- Handle quality border for empty keyring slots + if self.qualityBorder then + if bagID == -2 then + self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 0.5) + self.qualityBorder:Show() + else + self.qualityBorder:Hide() + end + end + + -- Hide quest elements + if self.questBorder then self.questBorder:Hide() end + if self.questIcon then self.questIcon:Hide() end +end + +--===================================================== +-- Main SetItem function (orchestrates helper functions) +--===================================================== + +-- Set item data +function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) + -- Proactively convert to number to avoid comparisons with strings in downstream functions + bagID = tonumber(bagID) + slotID = tonumber(slotID) + + -- Reset all visual state before reassigning pooled button + ResetButtonVisualState(self) + + -- Set button properties + self.bagID = bagID + self.slotID = slotID + if self.SetID then + self:SetID(slotID or 0) + end + self.bagIndex = bagID or -100 + self.itemData = itemData + self.isBank = isBank or false + self.otherChar = otherCharName + self.isReadOnly = isReadOnly or false + self.isMail = false + self.mailIndex = nil + self.mailItemIndex = nil + self.mailData = nil + + -- Configure drag/drop + SetupDragDrop(self) + + -- Default to true if not specified if matchesFilter == nil then matchesFilter = true end - self.mailData = nil -- Clear mail metadata by default - - -- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount) + -- Get UI elements local countText = getglobal(self:GetName().."Count") local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg") + local Utils = addon and addon.Modules and addon.Modules.Utils - -- Apply icon size setting (nil-safe) + -- Get icon size setting local iconSize = 37 - if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then - iconSize = addon.Modules.DB:GetSetting("iconSize") or iconSize - elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then - iconSize = Guda.Modules.DB:GetSetting("iconSize") or iconSize + if Utils and Utils.SafeCall then + iconSize = Utils:SafeCall("DB", "GetSetting", "iconSize") or iconSize end - if addon and addon.Constants and addon.Constants.BUTTON_SIZE then + if addon and addon.Constants then iconSize = iconSize or addon.Constants.BUTTON_SIZE end self:SetWidth(iconSize) self:SetHeight(iconSize) - -- In live mode (readOnly=false), query real-time game state instead of cached DB - -- In read-only mode (readOnly=true), use cached itemData from DB - local displayTexture, displayCount + -- Get item display info (texture, count, hasItem) + local displayTexture, displayCount, hasItem = GetItemDisplayInfo(bagID, slotID, itemData, self.isReadOnly) + self.hasItem = hasItem - if not self.isReadOnly then - -- LIVE MODE: Always query game state directly, never use cached itemData - local liveTexture, liveCount = GetContainerItemInfo(bagID, slotID) - if liveTexture then - displayTexture = liveTexture - displayCount = liveCount - self.hasItem = true - elseif bagID == -2 then - -- Fallback for keyring in 1.12.1 - local link = GetContainerItemLink(bagID, slotID) - if link then - self.hasItem = true - -- We might not have the texture from GetContainerItemInfo, try to get it from GetItemInfo - local _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(link) - displayTexture = itemTexture - displayCount = 1 -- Keyring items are usually unique anyway - else - self.hasItem = false - end - else - -- No item in this slot (even if itemData has cached data) - self.hasItem = false - end - else - -- READ-ONLY MODE: Use cached itemData from DB (can't query other characters) - if itemData and itemData.texture then - displayTexture = itemData.texture - displayCount = itemData.count - self.hasItem = true - else - self.hasItem = false - end - end - - -- Apply the determined texture and count + -- Apply display based on whether slot has item if self.hasItem then - if SetItemButtonTexture then - SetItemButtonTexture(self, displayTexture) + -- Set texture + if SetItemButtonTexture then + SetItemButtonTexture(self, displayTexture) end - - -- Explicitly set and show icon texture as SetItemButtonTexture can be unreliable for custom paths in 1.12 local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon if iconTexture and displayTexture then iconTexture:SetTexture(displayTexture) iconTexture:Show() end + -- Set count if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end if emptySlotBg then emptySlotBg:Hide() end + -- Update cooldown overlay for live items if not self.isReadOnly and not self.otherChar and Guda_ItemButton_UpdateCooldown then Guda_ItemButton_UpdateCooldown(self) else - -- Ensure cooldown is hidden for read-only/other character views local cd = getglobal(self:GetName().."Cooldown") or self.cooldown if cd and cd.Hide then cd:Hide() end end + -- Update unusable red overlay tint if Guda_ItemButton_UpdateUsableTint then - Guda_ItemButton_UpdateUsableTint(self) + Guda_ItemButton_UpdateUsableTint(self) end else - -- Fully clear all item button state for empty slots - if SetItemButtonTexture then SetItemButtonTexture(self, nil) end - if SetItemButtonCount then SetItemButtonCount(self, 0) end - if SetItemButtonDesaturated then SetItemButtonDesaturated(self, false) end - - -- Ensure cooldown overlay is hidden for empty slots - local cooldown = getglobal(self:GetName().."Cooldown") or self.cooldown - if cooldown and cooldown.Hide then cooldown:Hide() end - - -- Also clear the icon texture directly - local iconTexture = getglobal(self:GetName().."IconTexture") - if not iconTexture then - iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon - end - if iconTexture then - iconTexture:SetTexture(nil) - iconTexture:Hide() - end - - if emptySlotBg then emptySlotBg:Show() end - - -- Ensure any unusable tint is cleared on empty - if SetItemButtonTextureVertexColor then - SetItemButtonTextureVertexColor(self, 1.0, 1.0, 1.0) - end - - -- Clear unusable overlay for empty slots ✅ FIX: Clear red overlay when item is sold - if self.unusableOverlay and self.unusableOverlay.Hide then - self.unusableOverlay:Hide() - end + -- Clear empty slot + ClearItemButton(self, emptySlotBg, countText, bagID) end -- Update tracking checkmark + UpdateTrackingCheckmark(self, Utils) local check = getglobal(self:GetName().."_Check") if check then local isTracked = false - if self.hasItem then - local itemID = addon.Modules.Utils:ExtractItemID(self.itemData and self.itemData.link) + if self.hasItem and self.itemData and self.itemData.link then + local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(self.itemData.link) if itemID then - local trackedItems = addon.Modules.DB:GetSetting("trackedItems") or {} + local trackedItems = Utils and Utils.SafeCall and Utils:SafeCall("DB", "GetSetting", "trackedItems") or {} if trackedItems[itemID] then isTracked = true end @@ -735,10 +866,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha if countText and countText.GetFont then local font, _, flags = countText:GetFont() local fontSize = 12 - if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then - fontSize = addon.Modules.DB:GetSetting("iconFontSize") or fontSize - elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then - fontSize = Guda.Modules.DB:GetSetting("iconFontSize") or fontSize + if Utils and Utils.SafeCall then + fontSize = Utils:SafeCall("DB", "GetSetting", "iconFontSize") or fontSize end countText:SetFont(font, fontSize, flags) @@ -814,14 +943,11 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1) self.qualityBorder:Show() elseif itemQuality then - -- Check settings to determine if we should show borders (nil-safe) + -- Check settings to determine if we should show borders local showEquipmentBorder, showOtherBorder - if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then - showEquipmentBorder = addon.Modules.DB:GetSetting("showQualityBorderEquipment") - showOtherBorder = addon.Modules.DB:GetSetting("showQualityBorderOther") - elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then - showEquipmentBorder = Guda.Modules.DB:GetSetting("showQualityBorderEquipment") - showOtherBorder = Guda.Modules.DB:GetSetting("showQualityBorderOther") + if Utils and Utils.SafeCall then + showEquipmentBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderEquipment") + showOtherBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderOther") end -- Default to true if settings not found @@ -832,14 +958,10 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha showOtherBorder = true end - -- Check if item is equipment (nil-safe) + -- Check if item is equipment local isEquipment = false - if itemLink then - if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then - isEquipment = addon.Modules.Utils:IsEquipment(itemLink) - elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then - isEquipment = Guda.Modules.Utils:IsEquipment(itemLink) - end + if itemLink and Utils and Utils.IsEquipment then + isEquipment = Utils:IsEquipment(itemLink) end -- Determine if we should show the border based on item type and settings @@ -848,10 +970,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha if shouldShowBorder then -- Show colored border for all items (Poor, Common, Uncommon, Rare, Epic, etc.) local r, g, b = 1, 1, 1 - if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.GetQualityColor then - r, g, b = addon.Modules.Utils:GetQualityColor(itemQuality) - elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.GetQualityColor then - r, g, b = Guda.Modules.Utils:GetQualityColor(itemQuality) + if Utils and Utils.GetQualityColor then + r, g, b = Utils:GetQualityColor(itemQuality) end self.qualityBorder:SetBackdropBorderColor(r, g, b, 1) self.qualityBorder:Show() From e9ccf2a4f982c849dc42ad42effcb6bcc00bbbdb Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 18:06:01 +0400 Subject: [PATCH 09/10] version: 1.6.2 --- Guda.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guda.toc b/Guda.toc index 8daa777..d7a8561 100644 --- a/Guda.toc +++ b/Guda.toc @@ -2,7 +2,7 @@ ## Title: Guda ## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW) ## Author: Vati -## Version: 1.6.1 +## Version: 1.6.2 ## SavedVariables: Guda_DB ## SavedVariablesPerCharacter: Guda_CharDB From 4caed37406634945386f1731fee8452d7a134894 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 18:46:51 +0400 Subject: [PATCH 10/10] white armor,weapon to junk --- Core/CategoryManager.lua | 18 +++++++++++++++++- Sorting/SortEngine.lua | 24 +++++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index cc913b4..b74b50a 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -618,8 +618,10 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar -- Check for white equippable items (quality 1 + Weapon/Armor) if quality == 1 then local itemClass = itemData.class or "" + addon:Debug("isJunk check: quality=%s, class='%s', name='%s'", tostring(quality), tostring(itemClass), tostring(itemData.name)) if itemClass == "Weapon" or itemClass == "Armor" then isWhiteEquip = true + addon:Debug("isJunk: WHITE EQUIP DETECTED - %s", tostring(itemData.name)) end end @@ -687,9 +689,23 @@ end function CategoryManager:CategorizeItem(itemData, bagID, slotID, isOtherChar) local sortedCats = self:GetCategoriesByPriority() + -- Debug: show white item categorization + if itemData.quality == 1 and (itemData.class == "Weapon" or itemData.class == "Armor") then + addon:Debug("Categorizing white equip: %s (class=%s, quality=%s)", tostring(itemData.name), tostring(itemData.class), tostring(itemData.quality)) + for i, entry in ipairs(sortedCats) do + addon:Debug(" Cat %d: %s (priority=%s, enabled=%s)", i, entry.id, tostring(entry.def.priority), tostring(entry.def.enabled)) + if i > 10 then break end -- Limit debug output + end + end + for _, entry in ipairs(sortedCats) do if not entry.def.isFallback then - if self:EvaluateCategoryRules(entry.def, itemData, bagID, slotID, isOtherChar) then + local matches = self:EvaluateCategoryRules(entry.def, itemData, bagID, slotID, isOtherChar) + -- Debug: show which category matched for white equip + if itemData.quality == 1 and (itemData.class == "Weapon" or itemData.class == "Armor") and matches then + addon:Debug(" -> MATCHED: %s", entry.id) + end + if matches then return entry.id end end diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 2eb8b89..708078e 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -590,10 +590,17 @@ local function AddSortKeys(items) item.isMount = isMount -- Class and slot ordering - if itemRarity == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then + -- Check for items that should be treated as junk: + -- 1. Gray items (quality 0) + -- 2. Items with gray tooltip + -- 3. White equippable items (quality 1 Weapon/Armor) - vendor trash + local isGrayItem = itemRarity == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) + local isWhiteEquip = (itemRarity == 1) and (itemCategory == "Weapon" or itemCategory == "Armor") + + if isGrayItem or isWhiteEquip then item.sortedClass = CATEGORY_ORDER["Junk"] or 99 item.equipSlotOrder = 999 - item.isEquippable = false -- Treat gray gear as junk, not gear + item.isEquippable = false -- Treat junk gear as junk, not gear elseif isEquippable then item.sortedClass = 1 -- All equippable gear gets priority class item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999 @@ -1042,12 +1049,19 @@ local function BuildGreyTailPositions(bagIDs, greyCount) return tailSlots end --- Split a list of collected items into non-greys and greys (quality 0) +-- Split a list of collected items into non-junk and junk items +-- Junk includes: gray items (quality 0), gray tooltip items, white equippable items (quality 1 Weapon/Armor) local function SplitGreyItems(items) local nonGreys, greys = {}, {} for _, item in ipairs(items) do - -- Use same logic as AddSortKeys for determining Junk/Grey status (stability) - if tonumber(item.quality or 0) == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then + -- Use same logic as AddSortKeys for determining Junk status (stability) + local quality = tonumber(item.quality or 0) + local isGray = quality == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) + -- White equippable items (Weapon/Armor) are also treated as junk + local itemClass = item.class or "" + local isWhiteEquip = (quality == 1) and (itemClass == "Weapon" or itemClass == "Armor") + + if isGray or isWhiteEquip then table.insert(greys, item) else table.insert(nonGreys, item)