@@ -0,0 +1,769 @@
|
||||
-- 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 = {
|
||||
"Home", "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 = 75,
|
||||
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 = "isJunk", value = true }
|
||||
},
|
||||
matchMode = "any",
|
||||
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,
|
||||
},
|
||||
["Home"] = {
|
||||
name = "Home",
|
||||
icon = "Interface\\Icons\\INV_Misc_Rune_01",
|
||||
rules = {},
|
||||
matchMode = "all",
|
||||
priority = 0,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
hideControls = 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
|
||||
|
||||
-- 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
|
||||
|
||||
-- Migrate Junk category to use isJunk rule type
|
||||
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
|
||||
-- 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 = "isJunk", value = true } }
|
||||
needsUpdate = true
|
||||
end
|
||||
if needsUpdate then
|
||||
addon:Debug("CategoryManager: Migrated Junk category to use isJunk rule")
|
||||
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
|
||||
-- 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
|
||||
-- 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
|
||||
return false
|
||||
end
|
||||
local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link)
|
||||
return isBoE == ruleValue
|
||||
|
||||
elseif ruleType == "isQuestItem" then
|
||||
-- Use consolidated quest detection from Utils
|
||||
local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false)
|
||||
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 == tonumber(id) then return true end
|
||||
end
|
||||
return false
|
||||
else
|
||||
-- Convert string to number if needed
|
||||
return itemID == tonumber(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
|
||||
|
||||
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 ""
|
||||
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
|
||||
|
||||
local isJunk = isGray or isWhiteEquip
|
||||
return isJunk == 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()
|
||||
|
||||
-- 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
|
||||
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
|
||||
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 = "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" },
|
||||
{ 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
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
|
||||
+200
-240
@@ -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
|
||||
|
||||
|
||||
|
||||
+241
-210
@@ -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
|
||||
@@ -2,17 +2,19 @@
|
||||
## 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
|
||||
|
||||
Localization.lua
|
||||
|
||||
Core\Init.lua
|
||||
Core\Constants.lua
|
||||
Core\Database.lua
|
||||
DB\QuestItems.lua
|
||||
Core\Events.lua
|
||||
Core\Utils.lua
|
||||
Core\CategoryManager.lua
|
||||
Core\Tooltip.lua
|
||||
|
||||
Data\BagScanner.lua
|
||||
|
||||
+19
-5
@@ -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)
|
||||
|
||||
+156
-3
@@ -385,6 +385,9 @@ function BagFrame:Update()
|
||||
-- Update money
|
||||
self:UpdateMoney()
|
||||
|
||||
-- Update hearthstone
|
||||
self:UpdateHearthstone()
|
||||
|
||||
-- Update bag slots info
|
||||
self:UpdateBagSlotsInfo(bagData, isOtherChar)
|
||||
|
||||
@@ -504,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) .. "..."
|
||||
@@ -571,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
|
||||
@@ -935,6 +949,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 +2038,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
|
||||
|
||||
|
||||
+65
-35
@@ -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,8 +33,20 @@ 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})
|
||||
-- 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})
|
||||
end
|
||||
-- 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})
|
||||
@@ -44,42 +64,26 @@ 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")
|
||||
|
||||
if isQuestCategory then
|
||||
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
|
||||
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
|
||||
@@ -106,7 +110,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 +121,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 +135,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 = {},
|
||||
|
||||
+347
-227
@@ -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()
|
||||
|
||||
@@ -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,798 @@ 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(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", 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")
|
||||
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
|
||||
|
||||
-- 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.editBtn:Show()
|
||||
row.upBtn:Show()
|
||||
row.downBtn:Show()
|
||||
|
||||
-- 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
|
||||
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 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 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 = "Custom " .. counter,
|
||||
icon = "Interface\\Icons\\INV_Misc_QuestionMark",
|
||||
rules = {},
|
||||
matchMode = "any",
|
||||
priority = 80,
|
||||
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
|
||||
|
||||
-- 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
|
||||
|
||||
-------------------------------------------
|
||||
-- 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 = "itemID", name = "Item ID" },
|
||||
{ id = "quality", name = "Quality (exact)" },
|
||||
{ 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" },
|
||||
}
|
||||
|
||||
-- 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" },
|
||||
isJunk = { "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")
|
||||
|
||||
+379
-5
@@ -62,7 +62,7 @@
|
||||
<!-- Tab Buttons (GudaPlates style) -->
|
||||
<Button name="Guda_SettingsPopup_GeneralTabButton" enableMouse="true">
|
||||
<Size>
|
||||
<AbsDimension x="150" y="28"/>
|
||||
<AbsDimension x="113" y="28"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<Button name="Guda_SettingsPopup_IconsTabButton" enableMouse="true">
|
||||
<Size>
|
||||
<AbsDimension x="150" y="28"/>
|
||||
<AbsDimension x="113" y="28"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_GeneralTabButton" relativePoint="RIGHT">
|
||||
@@ -124,9 +124,9 @@
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Button name="Guda_SettingsPopup_GuideTabButton" enableMouse="true">
|
||||
<Button name="Guda_SettingsPopup_CategoriesTabButton" enableMouse="true">
|
||||
<Size>
|
||||
<AbsDimension x="150" y="28"/>
|
||||
<AbsDimension x="113" y="28"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_IconsTabButton" relativePoint="RIGHT">
|
||||
@@ -142,7 +142,39 @@
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parent_Text" inherits="GameFontNormal" text="Quick Guide">
|
||||
<FontString name="$parent_Text" inherits="GameFontNormal" text="Categories">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_SettingsPopup_SelectTab("categories")
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<Button name="Guda_SettingsPopup_GuideTabButton" enableMouse="true">
|
||||
<Size>
|
||||
<AbsDimension x="113" y="28"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_CategoriesTabButton" relativePoint="RIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="2" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parent_Bg" setAllPoints="true">
|
||||
<Color r="1" g="1" b="1" a="0.1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parent_Text" inherits="GameFontNormal" text="Guide">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
@@ -600,6 +632,106 @@
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- Categories Tab Content -->
|
||||
<Frame name="Guda_SettingsPopup_CategoriesTab" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="15" y="-90"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parent" relativePoint="BOTTOMRIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="-15" y="15"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parent_Header" inherits="GameFontNormal" text="Manage item categories and their display order:">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="5" y="-5"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<!-- Scroll Frame for Category List -->
|
||||
<ScrollFrame name="Guda_SettingsPopup_CategoriesScrollFrame" inherits="FauxScrollFrameTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="440" y="320"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="5" y="-25"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnVerticalScroll>
|
||||
FauxScrollFrame_OnVerticalScroll(18, Guda_SettingsPopup_CategoriesTab_Update)
|
||||
</OnVerticalScroll>
|
||||
<OnShow>
|
||||
Guda_SettingsPopup_CategoriesTab_Update()
|
||||
</OnShow>
|
||||
</Scripts>
|
||||
</ScrollFrame>
|
||||
|
||||
<!-- Category Row Container -->
|
||||
<Frame name="Guda_SettingsPopup_CategoryListContainer">
|
||||
<Size>
|
||||
<AbsDimension x="420" y="320"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="Guda_SettingsPopup_CategoriesScrollFrame" relativePoint="TOPLEFT"/>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
|
||||
<!-- Add Category Button -->
|
||||
<Button name="Guda_SettingsPopup_AddCategoryButton" inherits="UIPanelButtonTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="120" y="25"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="BOTTOMLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="5" y="10"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_SettingsPopup_AddCategory_OnClick()
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- Reset to Defaults Button -->
|
||||
<Button name="Guda_SettingsPopup_ResetCategoriesButton" inherits="UIPanelButtonTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="120" y="25"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_AddCategoryButton" relativePoint="RIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="10" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_SettingsPopup_ResetCategories_OnClick()
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
</Frame>
|
||||
|
||||
<!-- Guide Tab Content -->
|
||||
<Frame name="Guda_SettingsPopup_GuideTab" hidden="true">
|
||||
<Anchors>
|
||||
@@ -651,4 +783,246 @@
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- Category Editor Popup -->
|
||||
<Frame name="Guda_CategoryEditor" toplevel="true" movable="true" enableMouse="true" hidden="true" parent="UIParent" frameStrata="DIALOG">
|
||||
<Size>
|
||||
<AbsDimension x="400" y="380"/>
|
||||
</Size>
|
||||
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" relativePoint="CENTER" relativeTo="UIParent">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="50"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="11" right="12" top="12" bottom="11"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize>
|
||||
<AbsValue val="32"/>
|
||||
</TileSize>
|
||||
<EdgeSize>
|
||||
<AbsValue val="32"/>
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<!-- Title -->
|
||||
<FontString name="$parent_Title" inherits="GameFontNormalLarge" text="Edit Category">
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="-15"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Color r="1" g="0.82" b="0"/>
|
||||
</FontString>
|
||||
|
||||
<!-- Name Label -->
|
||||
<FontString name="$parent_NameLabel" inherits="GameFontNormal" text="Name:">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="20" y="-45"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
|
||||
<!-- Match Mode Label -->
|
||||
<FontString name="$parent_MatchModeLabel" inherits="GameFontNormal" text="Match Mode:">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="20" y="-80"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
|
||||
<!-- Rules Label -->
|
||||
<FontString name="$parent_RulesLabel" inherits="GameFontNormal" text="Rules:">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="20" y="-115"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
|
||||
<Frames>
|
||||
<!-- Close Button -->
|
||||
<Button name="$parent_CloseButton" inherits="UIPanelCloseButton">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="-5" y="-5"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_CategoryEditor:Hide()
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- Name EditBox -->
|
||||
<EditBox name="Guda_CategoryEditor_NameEditBox" autoFocus="false" inherits="InputBoxTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="200" y="20"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="80" y="-42"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnEscapePressed>
|
||||
this:ClearFocus()
|
||||
</OnEscapePressed>
|
||||
<OnEnterPressed>
|
||||
this:ClearFocus()
|
||||
</OnEnterPressed>
|
||||
</Scripts>
|
||||
</EditBox>
|
||||
|
||||
<!-- Match Mode: Any Radio -->
|
||||
<CheckButton name="Guda_CategoryEditor_MatchAny" inherits="UIRadioButtonTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="20" y="20"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="100" y="-77"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_CategoryEditor_SetMatchMode("any")
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
|
||||
<!-- Match Mode: All Radio -->
|
||||
<CheckButton name="Guda_CategoryEditor_MatchAll" inherits="UIRadioButtonTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="20" y="20"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="Guda_CategoryEditor_MatchAny" relativePoint="RIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="60" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_CategoryEditor_SetMatchMode("all")
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
|
||||
<!-- Rules Container -->
|
||||
<Frame name="Guda_CategoryEditor_RulesContainer">
|
||||
<Size>
|
||||
<AbsDimension x="360" y="180"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="20" y="-130"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Frame>
|
||||
|
||||
<!-- Add Rule Button -->
|
||||
<Button name="Guda_CategoryEditor_AddRuleButton" inherits="UIPanelButtonTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="100" y="22"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="-20" y="-112"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_CategoryEditor_AddRule()
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- Save Button -->
|
||||
<Button name="Guda_CategoryEditor_SaveButton" inherits="UIPanelButtonTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="100" y="25"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="-20" y="15"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_CategoryEditor_Save()
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
<!-- Cancel Button -->
|
||||
<Button name="Guda_CategoryEditor_CancelButton" inherits="UIPanelButtonTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="100" y="25"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="Guda_CategoryEditor_SaveButton" relativePoint="LEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="-10" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
Guda_CategoryEditor:Hide()
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
Guda_CategoryEditor_OnLoad(this)
|
||||
</OnLoad>
|
||||
<OnShow>
|
||||
Guda_CategoryEditor_OnShow(this)
|
||||
</OnShow>
|
||||
<OnMouseDown>
|
||||
if (arg1 == "LeftButton") then
|
||||
this:StartMoving()
|
||||
end
|
||||
</OnMouseDown>
|
||||
<OnMouseUp>
|
||||
this:StopMovingOrSizing()
|
||||
</OnMouseUp>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
</Ui>
|
||||
|
||||
Reference in New Issue
Block a user