From 60e2d99799988b93f78b99fb57a2bebcf3895285 Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Mon, 19 Jan 2026 17:41:57 +0400 Subject: [PATCH] refactory: redunant code --- Core/CategoryManager.lua | 26 +- Core/Constants.lua | 166 +++++++++++ Core/Tooltip.lua | 440 ++++++++++++++---------------- Core/Utils.lua | 451 ++++++++++++++++-------------- Guda.toc | 1 + UI/FrameHelpers.lua | 24 +- UI/ItemButton.lua | 574 +++++++++++++++++++++++---------------- 7 files changed, 959 insertions(+), 723 deletions(-) create mode 100644 Core/Constants.lua diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua index 3232c99..cc913b4 100644 --- a/Core/CategoryManager.lua +++ b/Core/CategoryManager.lua @@ -562,30 +562,8 @@ function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar return isBoE == ruleValue elseif ruleType == "isQuestItem" then - local isQuestItem = false - local itemCategory = itemData.class or itemData.category or "" - local itemType = itemData.type or "" - local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor") - local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") - - if isQuestCategory then - isQuestItem = true - elseif not isOtherChar then - isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) - if isQuestItem and isEquipment and not isQuestCategory then - isQuestItem = false - end - end - - -- Also check QuestItemsDB - if not isQuestItem and itemData.link then - local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) - if itemID and addon.IsQuestItemByID then - local playerFaction = UnitFactionGroup("player") - isQuestItem = addon:IsQuestItemByID(itemID, playerFaction) - end - end - + -- Use consolidated quest detection from Utils + local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false) return isQuestItem == ruleValue elseif ruleType == "texturePattern" then diff --git a/Core/Constants.lua b/Core/Constants.lua new file mode 100644 index 0000000..c7531da --- /dev/null +++ b/Core/Constants.lua @@ -0,0 +1,166 @@ +-- Guda Constants Module +-- Centralized location for all magic numbers and hardcoded values + +local addon = Guda + +-- Extend the existing Constants table from Init.lua +local C = addon.Constants + +--============================================================================= +-- Bag IDs +--============================================================================= +C.BAG_BACKPACK = 0 +C.BAG_FIRST = 1 +C.BAG_LAST = 4 +C.BANK_FIRST = 5 +C.BANK_LAST = 11 +C.KEYRING_BAG = -2 +C.BANK_CONTAINER = -1 + +-- Bag ID ranges for iteration +C.BAG_IDS = {0, 1, 2, 3, 4} +C.BANK_BAG_IDS = {5, 6, 7, 8, 9, 10, 11} +C.ALL_BAG_IDS = {0, 1, 2, 3, 4, -2} -- Including keyring + +--============================================================================= +-- Equipment Slots +--============================================================================= +C.EQUIPMENT_SLOT_FIRST = 1 +C.EQUIPMENT_SLOT_LAST = 19 + +--============================================================================= +-- Item IDs +--============================================================================= +C.SOUL_SHARD_ID = 6265 +C.HEARTHSTONE_ID = 6948 + +--============================================================================= +-- Colors (r, g, b tables for easy unpacking) +--============================================================================= +C.COLORS = { + -- Special item borders + KEYRING_CYAN = {r = 0.2, g = 0.8, b = 1.0}, + QUEST_GOLD = {r = 1.0, g = 0.82, b = 0}, + + -- Text colors + GRAY_TEXT = {r = 0.5, g = 0.5, b = 0.5}, + WHITE_TEXT = {r = 1.0, g = 1.0, b = 1.0}, + GOLD_TITLE = {r = 1.0, g = 0.82, b = 0}, + CYAN_LABEL = {r = 0, g = 1.0, b = 1.0}, + + -- Unusable item tint + UNUSABLE_RED = {r = 0.9, g = 0.2, b = 0.2, a = 0.45}, + + -- Lock/desaturate + LOCKED_GRAY = {r = 0.5, g = 0.5, b = 0.5}, +} + +--============================================================================= +-- UI Thresholds +--============================================================================= +C.ICON_SIZE_THRESHOLD = 44 -- Below this, use smaller insets/padding +C.ICON_INSET_SMALL = 10 -- Inset for small icons +C.ICON_INSET_LARGE = 15 -- Inset for large icons +C.ICON_TEXCOORD_CROP = 0.08 -- Texture coordinate crop amount + +--============================================================================= +-- Database / Cleanup +--============================================================================= +C.CLEANUP_OLD_CHARS_DAYS = 90 + +--============================================================================= +-- Tooltip Scanning +--============================================================================= +C.QUEST_TOOLTIP_PATTERNS = { + "quest starter", + "this item begins a quest", + "starts a quest", + "quest item", + "manual", +} + +C.BIND_ON_EQUIP_PATTERN = "binds when equipped" + +--============================================================================= +-- Specialized Bag Types +--============================================================================= +C.BAG_TYPES = { + SOUL = "soul", + HERB = "herb", + ENCHANT = "enchant", + QUIVER = "quiver", + AMMO = "ammo", +} + +-- Tooltip patterns for bag type detection +C.BAG_TYPE_PATTERNS = { + soul = {"soul bag", "soul pouch"}, + herb = {"herb bag"}, + enchant = {"enchanting bag"}, + quiver = {"quiver"}, + ammo = {"ammo pouch"}, +} + +--============================================================================= +-- Item Categories (for GetItemInfo) +--============================================================================= +C.ITEM_CATEGORIES = { + WEAPON = "Weapon", + ARMOR = "Armor", + CONSUMABLE = "Consumable", + CONTAINER = "Container", + TRADE_GOODS = "Trade Goods", + PROJECTILE = "Projectile", + QUIVER = "Quiver", + REAGENT = "Reagent", + RECIPE = "Recipe", + KEY = "Key", + MISCELLANEOUS = "Miscellaneous", + QUEST = "Quest", +} + +--============================================================================= +-- Money Formatting +--============================================================================= +C.MONEY = { + COPPER_PER_SILVER = 100, + SILVER_PER_GOLD = 100, + COPPER_PER_GOLD = 10000, +} + +-- Color codes for money display +C.MONEY_COLORS = { + GOLD = "|cFFFFD700", + SILVER = "|cFFC7C7CF", + COPPER = "|cFFEDA55F", + WHITE = "|cFFFFFFFF", +} + +--============================================================================= +-- Frame Constants +--============================================================================= +C.FRAME = { + TITLE_HEIGHT = 40, + SEARCH_BAR_HEIGHT = 30, + FOOTER_HEIGHT = 45, + FOOTER_HEIGHT_HIDDEN = 10, + MIN_WIDTH = 200, + MIN_HEIGHT = 150, + MAX_WIDTH = 1250, + MAX_HEIGHT = 1000, +} + +--============================================================================= +-- Bank Slots +--============================================================================= +C.BANK_MAIN_SLOTS = 24 -- Number of slots in main bank container + +--============================================================================= +-- Tooltip Hook Settings +--============================================================================= +C.TOOLTIP = { + DEBOUNCE_TIME = 0.2, -- Seconds to wait before clearing cache + MAX_MONEY_FRAMES = 8, -- Maximum number of money frames to search +} + +addon:Debug("Constants module loaded") diff --git a/Core/Tooltip.lua b/Core/Tooltip.lua index 01285ac..7c34881 100644 --- a/Core/Tooltip.lua +++ b/Core/Tooltip.lua @@ -4,261 +4,221 @@ local addon = Guda local Tooltip = {} addon.Modules.Tooltip = Tooltip +--============================================================================= +-- Item Counting Helper Functions (extracted for clarity and reuse) +--============================================================================= + -- Helper function to get item ID from link (Lua 5.0 compatible) local function GetItemIDFromLink(link) - if not link then return nil end + if not link then return nil end if type(link) == "number" then return link end - - -- Try to find itemID in a standard link or a raw item:ID string - local _, _, itemID = string.find(link, "item:(%d+)") - return itemID and tonumber(itemID) or nil + local _, _, itemID = string.find(link, "item:(%d+)") + return itemID and tonumber(itemID) or nil end + +-- Count items in saved bag/bank data structure +-- Used for both bags and bank counting from saved character data +local function CountFromSavedContainers(containersData, itemID) + local count = 0 + if not containersData or type(containersData) ~= "table" then + return count + end + + for bagID, bagData in pairs(containersData) do + if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then + for slotID, itemData in pairs(bagData.slots) do + if itemData and type(itemData) == "table" and itemData.link then + local slotItemID = GetItemIDFromLink(itemData.link) + if slotItemID == itemID then + count = count + (itemData.count or 1) + end + end + end + end + end + + return count +end + +-- Count items in saved mailbox data structure +local function CountFromSavedMailbox(mailboxData, itemID) + local count = 0 + if not mailboxData or type(mailboxData) ~= "table" then + return count + end + + for _, mail in ipairs(mailboxData) do + local itemsToCheck = mail.items or (mail.item and {mail.item}) or {} + for _, item in ipairs(itemsToCheck) do + local slotItemID = item.link and GetItemIDFromLink(item.link) + if slotItemID == itemID then + count = count + (item.count or 1) + elseif not slotItemID and item.name then + -- Fallback to name matching if link is missing + local targetName = GetItemInfo(itemID) + if targetName == item.name then + count = count + (item.count or 1) + end + end + end + end + + return count +end + +-- Count items in saved equipped data structure +local function CountFromSavedEquipped(equippedData, itemID) + local count = 0 + if not equippedData or type(equippedData) ~= "table" then + return count + end + + for slotName, itemData in pairs(equippedData) do + if itemData and type(itemData) == "table" and itemData.link then + local slotItemID = GetItemIDFromLink(itemData.link) + if slotItemID == itemID then + count = count + 1 + end + end + end + + return count +end + +-- Count items in live container (bags or bank) +local function CountFromLiveContainer(bagIDs, itemID) + local count = 0 + + for _, bagID in ipairs(bagIDs) do + local numSlots = GetContainerNumSlots(bagID) + if numSlots and numSlots > 0 then + for slot = 1, numSlots do + local link = GetContainerItemLink(bagID, slot) + if link then + local slotItemID = GetItemIDFromLink(link) + if slotItemID == itemID then + local _, itemCount = GetContainerItemInfo(bagID, slot) + count = count + (itemCount or 1) + end + end + end + end + end + + return count +end + +-- Count items in live mailbox +local function CountFromLiveMailbox(itemID) + local count = 0 + + if not (addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen()) then + return count + end + + local numInboxItems = GetInboxNumItems() + for i = 1, numInboxItems do + local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i) + if hasItem then + local numAttachments = GetInboxNumAttachments and GetInboxNumAttachments(i) or 1 + if numAttachments == 0 and hasItem then + numAttachments = 1 + end + + for j = 1, numAttachments do + local name, _, itemCount = GetInboxItem(i, j) + if name then + local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j) + if itemLink then + local slotItemID = GetItemIDFromLink(itemLink) + if slotItemID == itemID then + count = count + (itemCount or 1) + end + end + end + end + end + end + + return count +end + +-- Count items in live equipment slots +local function CountFromLiveEquipped(itemID) + local count = 0 + + for slotID = 1, 19 do + local link = GetInventoryItemLink("player", slotID) + if link then + local slotItemID = GetItemIDFromLink(link) + if slotItemID == itemID then + count = count + 1 + end + end + end + + return count +end + +--============================================================================= +-- Main Counting Functions +--============================================================================= + +-- Count items for current character using live game data local function CountCurrentCharacterItems(itemID) - local bagCount = 0 - local bankCount = 0 - local mailCount = 0 - local equippedCount = 0 + local bagCount = 0 + local bankCount = 0 + local mailCount = 0 + local equippedCount = 0 - -- Count current character's bags in real-time - local bagsToCount = {0, 1, 2, 3, 4, -2} - for _, bagID in ipairs(bagsToCount) do - local numSlots = GetContainerNumSlots(bagID) - for slot = 1, numSlots do - local link = GetContainerItemLink(bagID, slot) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - local _, count = GetContainerItemInfo(bagID, slot) - bagCount = bagCount + (count or 1) - end - end - end - end + -- Count bags in real-time + bagCount = CountFromLiveContainer({0, 1, 2, 3, 4, -2}, itemID) - -- Count current character's bank in real-time if bank is open - local bankFrame = getglobal("BankFrame") - if bankFrame and bankFrame:IsVisible() then - -- Main bank slots (-1) - local numMainSlots = GetContainerNumSlots(-1) or 24 - for slot = 1, numMainSlots do - local link = GetContainerItemLink(-1, slot) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - local _, count = GetContainerItemInfo(-1, slot) - bankCount = bankCount + (count or 1) - end - end - end + -- Count bank: live if open, otherwise from saved data + local bankFrame = getglobal("BankFrame") + if bankFrame and bankFrame:IsVisible() then + -- Main bank + bank bags + bankCount = CountFromLiveContainer({-1, 5, 6, 7, 8, 9, 10, 11}, itemID) + else + -- Use saved data + local playerName = addon.Modules.DB:GetPlayerFullName() + local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] + if charData then + bankCount = CountFromSavedContainers(charData.bank, itemID) + end + end - -- Bank bags (5-11) - for bagID = 5, 11 do - local numSlots = GetContainerNumSlots(bagID) - if numSlots and numSlots > 0 then - for slot = 1, numSlots do - local link = GetContainerItemLink(bagID, slot) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - local _, count = GetContainerItemInfo(bagID, slot) - bankCount = bankCount + (count or 1) - end - end - end - end - end - else - -- Bank not open - use saved data for bank counts - local playerName = addon.Modules.DB:GetPlayerFullName() - local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] - if charData and charData.bank and type(charData.bank) == "table" then - for bagID, bagData in pairs(charData.bank) do - if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then - for slotID, itemData in pairs(bagData.slots) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - bankCount = bankCount + (itemData.count or 1) - end - end - end - end - end - end - end + -- Count mailbox: live if open, otherwise from saved data + if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then + mailCount = CountFromLiveMailbox(itemID) + else + local playerName = addon.Modules.DB:GetPlayerFullName() + local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] + if charData then + mailCount = CountFromSavedMailbox(charData.mailbox, itemID) + end + end - -- Count current character's mailbox in real-time if mailbox is open - if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then - local numInboxItems = GetInboxNumItems() - for i = 1, numInboxItems do - local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i) - if hasItem then - -- Turtle WoW supports up to 12 attachments per mail. - -- We use GetInboxNumAttachments if available to avoid over-scanning. - local numAttachments = 0 - if GetInboxNumAttachments then - numAttachments = GetInboxNumAttachments(i) or 0 - end + -- Count equipped items in real-time + equippedCount = CountFromLiveEquipped(itemID) - -- Fallback: if we don't have the count but header says there's an item, assume at least 1. - if numAttachments == 0 and hasItem then - numAttachments = 1 - end - - for j = 1, numAttachments do -- Turtle WoW supports up to 12 attachments - local name, _, count = GetInboxItem(i, j) - if name then - local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j) - if itemLink then - local slotItemID = GetItemIDFromLink(itemLink) - if slotItemID == itemID then - mailCount = mailCount + (count or 1) - end - end - end - end - end - end - else - -- Mailbox not open - use saved data - local playerName = addon.Modules.DB:GetPlayerFullName() - local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] - if charData and charData.mailbox and type(charData.mailbox) == "table" then - for _, mail in ipairs(charData.mailbox) do - if mail.items then - for _, item in ipairs(mail.items) do - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - elseif mail.item then -- Fallback for single item data structure - local item = mail.item - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - end - end - end - - -- Count equipped items in real-time - for slotID = 1, 19 do -- All equipment slots - local link = GetInventoryItemLink("player", slotID) - if link then - local slotItemID = GetItemIDFromLink(link) - if slotItemID == itemID then - equippedCount = equippedCount + 1 - end - end - end - - return bagCount, bankCount, equippedCount, mailCount + return bagCount, bankCount, equippedCount, mailCount end --- Count items for a specific character with real-time data for current character +-- Count items for a specific character (current or other) local function CountItemsForCharacter(itemID, characterData, isCurrentChar) --- For current character, use real-time counting to avoid database sync issues - if isCurrentChar then - return CountCurrentCharacterItems(itemID) - end + -- For current character, use real-time counting + if isCurrentChar then + return CountCurrentCharacterItems(itemID) + end - -- For other characters, use saved data - local bagCount = 0 - local bankCount = 0 - local mailCount = 0 - local equippedCount = 0 + -- For other characters, use saved data + local bagCount = CountFromSavedContainers(characterData.bags, itemID) + local bankCount = CountFromSavedContainers(characterData.bank, itemID) + local mailCount = CountFromSavedMailbox(characterData.mailbox, itemID) + local equippedCount = CountFromSavedEquipped(characterData.equipped, itemID) - -- Count bags from saved data - if characterData.bags and type(characterData.bags) == "table" then - for bagID, bagData in pairs(characterData.bags) do - if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then - for slotID, itemData in pairs(bagData.slots) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - bagCount = bagCount + (itemData.count or 1) - end - end - end - end - end - end - - -- Count bank from saved data - if characterData.bank and type(characterData.bank) == "table" then - for bagID, bagData in pairs(characterData.bank) do - if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then - for slotID, itemData in pairs(bagData.slots) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - bankCount = bankCount + (itemData.count or 1) - end - end - end - end - end - end - - -- Count mailbox from saved data - if characterData.mailbox and type(characterData.mailbox) == "table" then - for _, mail in ipairs(characterData.mailbox) do - if mail.items then - for _, item in ipairs(mail.items) do - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - elseif mail.item then -- Fallback for single item data structure - local item = mail.item - local slotItemID = item.link and GetItemIDFromLink(item.link) - if slotItemID == itemID then - mailCount = mailCount + (item.count or 1) - elseif not slotItemID and item.name then - -- Fallback to name matching if link is missing - local targetName = GetItemInfo(itemID) - if targetName == item.name then - mailCount = mailCount + (item.count or 1) - end - end - end - end - end - - -- Count equipped items from saved data - if characterData.equipped and type(characterData.equipped) == "table" then - for slotName, itemData in pairs(characterData.equipped) do - if itemData and type(itemData) == "table" and itemData.link then - local slotItemID = GetItemIDFromLink(itemData.link) - if slotItemID == itemID then - equippedCount = equippedCount + 1 - end - end - end - end - - return bagCount, bankCount, equippedCount, mailCount + return bagCount, bankCount, equippedCount, mailCount end diff --git a/Core/Utils.lua b/Core/Utils.lua index 0564cfd..5875a54 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -5,6 +5,64 @@ local addon = Guda local Utils = {} addon.Modules.Utils = Utils +--============================================================================= +-- SafeCall: Nil-safe module method invocation +-- Replaces verbose nil-checks like: +-- if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.Method then +-- return addon.Modules.Utils:Method(arg1, arg2) +-- end +-- With: +-- return Utils:SafeCall("Utils", "Method", arg1, arg2) +--============================================================================= + +-- Call a method on a module safely, returns nil if module/method doesn't exist +-- Parameters: +-- moduleName: Name of the module in addon.Modules (e.g., "Utils", "DB", "BagFrame") +-- methodName: Name of the method to call (e.g., "GetQualityColor", "GetSetting") +-- ...: Arguments to pass to the method +-- Returns: The return value(s) of the method, or nil if not callable +function Utils:SafeCall(moduleName, methodName, ...) + if not addon or not addon.Modules then + return nil + end + + local module = addon.Modules[moduleName] + if not module then + return nil + end + + local method = module[methodName] + if not method or type(method) ~= "function" then + return nil + end + + -- Call method with module as self (for : style calls) + return method(module, unpack(arg)) +end + +-- Check if a module method exists without calling it +function Utils:HasMethod(moduleName, methodName) + if not addon or not addon.Modules then + return false + end + + local module = addon.Modules[moduleName] + if not module then + return false + end + + local method = module[methodName] + return method ~= nil and type(method) == "function" +end + +-- Get a module reference safely +function Utils:GetModule(moduleName) + if not addon or not addon.Modules then + return nil + end + return addon.Modules[moduleName] +end + -- Format money (copper to gold/silver/copper string) - WoW 1.12.1 version function Utils:FormatMoney(copper, showZero, useColors) if not copper or copper == 0 then @@ -190,49 +248,123 @@ local function GetScanTooltip() return scanTooltip end --- Check if an item is a quest item by scanning its tooltip -function Utils:IsQuestItemTooltip(bagID, slotID) - if not bagID or not slotID then return false end +-- Check if an item is a quest item by scanning its tooltip (internal helper) +-- Returns: isQuestItem, isQuestStarter +local function ScanTooltipForQuest(tooltip, tooltipName) + local isQuestItem = false + local isQuestStarter = false - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetBagItem(bagID, slotID) - - -- Check tooltip lines for explicit quest-related phrases (case-insensitive) for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + local line = getglobal(tooltipName .. "TextLeft" .. i) if line then local text = line:GetText() if text then local tl = string.lower(text) - -- Only match explicit quest markers to avoid misclassifying consumables/recipes - -- Include "manual" which some quest items use (same as ItemButton detection) + -- Check for quest starter patterns first if string.find(tl, "quest starter") or string.find(tl, "this item begins a quest") or - string.find(tl, "starts a quest") or - string.find(tl, "quest item") or - string.find(tl, "manual") then - - -- Double check category to avoid misidentifying equipment with "Use:" - local link = GetContainerItemLink(bagID, slotID) - if link and self.ExtractItemID and self.GetItemInfoSafe then - local itemID = self:ExtractItemID(link) - if itemID then - local _, _, _, _, itemCategory, itemType = self:GetItemInfoSafe(itemID) - -- If it's Weapon or Armor, and NOT categorized as Quest, then it's not a Quest Item - if (itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor") and - (itemCategory ~= "Quest" and itemType ~= "Quest") then - return false - end - end - end - - return true + string.find(tl, "starts a quest") then + isQuestItem = true + isQuestStarter = true + break + -- Check for regular quest item patterns + elseif string.find(tl, "quest item") or + string.find(tl, "manual") then + isQuestItem = true + -- Don't break, might still find a quest starter pattern end end end end - return false + + return isQuestItem, isQuestStarter +end + +-- Consolidated quest item detection function +-- Handles tooltip scanning, category checks, equipment filtering, and QuestItemsDB lookup +-- Parameters: +-- bagID, slotID: Required for tooltip scanning (can be nil for other char items) +-- itemData: Optional item data table with link, class/category, type fields +-- isOtherChar: Boolean, true if checking an item from another character's saved data +-- isBank: Boolean, true if the item is in the bank +-- Returns: isQuestItem (boolean), isQuestStarter (boolean) +function Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, isBank) + bagID = tonumber(bagID) + slotID = tonumber(slotID) + + local isQuestItem = false + local isQuestStarter = false + + -- Get item link and category info + local itemLink = itemData and itemData.link + local itemCategory = itemData and (itemData.class or itemData.category) or "" + local itemType = itemData and itemData.type or "" + local itemID + + -- For live items, query the link directly + if not isOtherChar and bagID and slotID then + itemLink = GetContainerItemLink(bagID, slotID) + end + + if itemLink then + itemID = self:ExtractItemID(itemLink) + if itemID then + local _, _, _, _, cat, typ = self:GetItemInfoSafe(itemID) + itemCategory = cat or itemCategory + itemType = typ or itemType + end + end + + -- Check if item is equipment (should not be classified as quest unless explicitly Quest category) + local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or + itemType == "Weapon" or itemType == "Armor") + local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") + + -- Priority 1: If explicitly categorized as Quest, it's a quest item + if isQuestCategory then + return true, false + end + + -- Priority 2: Tooltip scanning for current character items + if not isOtherChar and bagID and slotID then + local tooltip = GetScanTooltip() + tooltip:ClearLines() + + -- Handle bank items differently + if isBank and bagID == -1 then + if tooltip.SetInventoryItem then + tooltip:SetInventoryItem("player", 39 + slotID) + else + tooltip:SetBagItem(bagID, slotID) + end + else + tooltip:SetBagItem(bagID, slotID) + end + + isQuestItem, isQuestStarter = ScanTooltipForQuest(tooltip, "GudaBagScanTooltip") + + -- Filter out equipment that has quest-like text but isn't categorized as Quest + if isQuestItem and isEquipment and not isQuestCategory then + isQuestItem = false + isQuestStarter = false + end + end + + -- Priority 3: Check QuestItemsDB for known faction-specific quest items + if not isQuestItem and itemID and addon.IsQuestItemByID then + local playerFaction = UnitFactionGroup("player") + if addon:IsQuestItemByID(itemID, playerFaction) then + isQuestItem = true + end + end + + return isQuestItem, isQuestStarter +end + +-- Legacy compatibility wrapper - keep the old function name working +function Utils:IsQuestItemTooltip(bagID, slotID) + local isQuest, _ = self:IsQuestItem(bagID, slotID, nil, false, false) + return isQuest end -- Check if an item has a gray title in its tooltip or link @@ -296,129 +428,8 @@ function Utils:TruncateText(text, maxLen) return string.sub(text, 1, maxLen - 3) .. "..." end -function Utils:IsAmmoQuiverBag(bagID) - -- Skip backpack, bank, and keyring - if bagID == 0 or bagID == -1 or bagID == -2 then - return false - end - - -- Get the bag item - local invSlot = ContainerIDToInventoryID(bagID) - if not invSlot then - return false - end - - local link = GetInventoryItemLink("player", invSlot) - if not link then - return false - end - - -- Use tooltip scanning to get item class (more reliable in 1.12.1) - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetInventoryItem("player", invSlot) - - -- Scan tooltip lines for "Quiver" or "Ammo Pouch" - for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - -- Check if the line contains "Quiver" or "Ammo Pouch" - if string.find(text, "Quiver") or string.find(text, "Ammo Pouch") then - return true - end - end - end - end - - return false -end - --- Check if a bag is Herb Bag -function Utils:IsHerbBag(bagID) - -- Skip backpack, bank, and keyring - if bagID == 0 or bagID == -1 or bagID == -2 then - return false - end - - local invSlot = ContainerIDToInventoryID(bagID) - if not invSlot then return false end - - local link = GetInventoryItemLink("player", invSlot) - if not link then return false end - - -- Prefer specialized type detection - local bagType = self:GetSpecializedBagType(bagID) - if bagType == "herb" then return true end - - -- Fallback: tooltip scan for "Herb Bag" - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetInventoryItem("player", invSlot) - - for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text and string.find(string.lower(text), "herb bag") then - return true - end - end - end - - return false -end - --- Check if a bag is Soul Bag -function Utils:IsSoulBag(bagID) - -- Skip backpack, bank, and keyring - if bagID == 0 or bagID == -1 or bagID == -2 then - return false - end - - -- Get the bag item - local invSlot = ContainerIDToInventoryID(bagID) - if not invSlot then - return false - end - - local link = GetInventoryItemLink("player", invSlot) - if not link then - return false - end - - -- First try GetSpecializedBagType - local bagType = self:GetSpecializedBagType(bagID) - if bagType == "soul" then - return true - end - - -- Fallback: Use tooltip scanning (more reliable in 1.12.1) - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetInventoryItem("player", invSlot) - - -- Scan tooltip lines for "Soul Bag" or "Soul Pouch" - for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - -- Check if the line contains "Soul Bag" or "Soul Pouch" or just "Soul" in bag name - local textLower = string.lower(text) - if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or - (string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then - return true - end - end - end - end - - return false -end - -- Returns: "soul", "herb", "enchant", "quiver", "ammo", or nil +-- This is the consolidated bag type detection function with tooltip fallback function Utils:GetSpecializedBagType(bagID) -- Skip backpack, bank, and keyring if bagID == 0 or bagID == -1 or bagID == -2 then @@ -437,44 +448,94 @@ function Utils:GetSpecializedBagType(bagID) end local itemID = self:ExtractItemID(link) - if not itemID then - return nil + + -- Try GetItemInfo first (more reliable when available) + if itemID then + local _, _, _, _, _, itemType = self:GetItemInfoSafe(itemID) + if itemType then + local typeLower = string.lower(itemType) + + if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then + return "soul" + end + if string.find(typeLower, "herb bag") then + return "herb" + end + if string.find(typeLower, "enchanting bag") then + return "enchant" + end + if string.find(typeLower, "quiver") then + return "quiver" + end + if string.find(typeLower, "ammo pouch") then + return "ammo" + end + end end - local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType = self:GetItemInfoSafe(itemID) - if itemType then - -- Check for exact subtype matches - local typeLower = string.lower(itemType) + -- Fallback: tooltip scanning for all bag types + local tooltip = GetScanTooltip() + tooltip:ClearLines() + tooltip:SetInventoryItem("player", invSlot) - -- Soul Bag / Soul Pouch - if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then - return "soul" - end + for i = 1, tooltip:NumLines() do + local line = getglobal("GudaBagScanTooltipTextLeft" .. i) + if line then + local text = line:GetText() + if text then + local textLower = string.lower(text) - -- Herb Bag - if string.find(typeLower, "herb bag") then - return "herb" - end + -- Soul Bag / Soul Pouch + if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or + (string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then + return "soul" + end - -- Enchanting Bag - if string.find(typeLower, "enchanting bag") then - return "enchant" - end + -- Herb Bag + if string.find(textLower, "herb bag") then + return "herb" + end - -- Quiver - if string.find(typeLower, "quiver") then - return "quiver" - end + -- Enchanting Bag + if string.find(textLower, "enchanting bag") then + return "enchant" + end - -- Ammo Pouch - if string.find(typeLower, "ammo pouch") then - return "ammo" + -- Quiver + if string.find(textLower, "quiver") then + return "quiver" + end + + -- Ammo Pouch + if string.find(textLower, "ammo pouch") then + return "ammo" + end + end end end return nil end +-- Simple helper to check if a bag is of a specific type +function Utils:IsBagType(bagID, bagType) + return self:GetSpecializedBagType(bagID) == bagType +end + +-- Convenience wrappers for common bag type checks +function Utils:IsAmmoQuiverBag(bagID) + local bagType = self:GetSpecializedBagType(bagID) + return bagType == "quiver" or bagType == "ammo" +end + +function Utils:IsHerbBag(bagID) + return self:IsBagType(bagID, "herb") +end + +function Utils:IsSoulBag(bagID) + return self:IsBagType(bagID, "soul") +end + -- Get container priority for sorting (higher = more important) function Utils:GetContainerPriority(bagID) local bagType = self:GetSpecializedBagType(bagID) @@ -727,37 +788,7 @@ function Utils:IsEnchantingItem(itemLink) return false end --- Check if a bag is Enchanting Bag (parallel to IsHerbBag) +-- Check if a bag is Enchanting Bag function Utils:IsEnchantBag(bagID) - -- Skip backpack, bank, and keyring - if bagID == 0 or bagID == -1 or bagID == -2 then - return false - end - - local invSlot = ContainerIDToInventoryID(bagID) - if not invSlot then return false end - - local link = GetInventoryItemLink("player", invSlot) - if not link then return false end - - -- Prefer specialized type detection - local bagType = self:GetSpecializedBagType(bagID) - if bagType == "enchant" then return true end - - -- Fallback: tooltip scan for "Enchanting Bag" - local tooltip = GetScanTooltip() - tooltip:ClearLines() - tooltip:SetInventoryItem("player", invSlot) - - for i = 1, tooltip:NumLines() do - local line = getglobal("GudaBagScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text and string.find(string.lower(text), "enchanting bag") then - return true - end - end - end - - return false + return self:IsBagType(bagID, "enchant") end \ No newline at end of file diff --git a/Guda.toc b/Guda.toc index 029233b..8daa777 100644 --- a/Guda.toc +++ b/Guda.toc @@ -9,6 +9,7 @@ Localization.lua Core\Init.lua +Core\Constants.lua Core\Database.lua DB\QuestItems.lua Core\Events.lua diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index 8c0de9f..6590377 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -82,28 +82,8 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end - -- Priority 3: Quest Items - local isQuestItem = false - local itemCategory = itemData.class or itemData.category or "" - local itemType = itemData.type or "" - local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor") - local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") - - if isQuestCategory then - isQuestItem = true - elseif not isOtherChar then - isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) - if isQuestItem and isEquipment and not isQuestCategory then - isQuestItem = false - end - end - if not isQuestItem and itemData.link then - local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) - if itemID and addon.IsQuestItemByID then - local playerFaction = UnitFactionGroup("player") - isQuestItem = addon:IsQuestItemByID(itemID, playerFaction) - end - end + -- Priority 3: Quest Items (use consolidated detection) + local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false) if isQuestItem then table.insert(categories["Quest"], {bagID = bagID, slotID = slotID, itemData = itemData}) return diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index e094e5f..67e4b51 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -8,90 +8,13 @@ local nextButtonID = 1 local scanTooltip = CreateFrame("GameTooltip", "Guda_QuestScanTooltip", nil, "GameTooltipTemplate") scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") --- Check if an item is a quest item by scanning its tooltip --- Check if an item is a quest item by scanning its tooltip and determine type +-- Helper function to check if an item is a quest item +-- Delegates to consolidated Utils:IsQuestItem() function local function IsQuestItem(bagID, slotID, isBank) - bagID = tonumber(bagID) - slotID = tonumber(slotID) - if not bagID or not slotID then return false end - - scanTooltip:ClearLines() - if isBank and bagID == -1 then - if scanTooltip.SetInventoryItem then - scanTooltip:SetInventoryItem("player", 39 + slotID) - else - scanTooltip:SetBagItem(bagID, slotID) - end - else - scanTooltip:SetBagItem(bagID, slotID) + if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsQuestItem then + return addon.Modules.Utils:IsQuestItem(bagID, slotID, nil, false, isBank) end - - local isQuestItem = false - local isQuestStarter = false - - -- Check all tooltip lines for quest-related text - for i = 1, scanTooltip:NumLines() do - local line = getglobal("Guda_QuestScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - -- Check for quest starter patterns - if string.find(text, "Quest Starter") or - string.find(text, "This Item Begins a Quest") or - string.find(text, "Use: Starts a Quest") then - isQuestItem = true - isQuestStarter = true - break - -- Check for regular quest item patterns - elseif string.find(text, "Quest Item") or - string.find(text, "Manual") then - isQuestItem = true - -- Don't break, might still find a quest starter pattern - end - end - end - end - - -- Also check item category/type via GetItemInfo for "Quest" - -- Turtle WoW GetItemInfo returns: name, link, rarity, level, itemCategory, itemType, stack, subType, texture, equipLoc, sellPrice - local link = GetContainerItemLink(bagID, slotID) - local itemID - local itemCategory, itemType - - if link and addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.ExtractItemID and addon.Modules.Utils.GetItemInfoSafe then - itemID = addon.Modules.Utils:ExtractItemID(link) - if itemID then - _, _, _, _, itemCategory, itemType = addon.Modules.Utils:GetItemInfoSafe(itemID) - end - end - - -- If it's a Weapon or Armor, it shouldn't be a QuestItem unless it's specifically categorized as Quest - -- This avoids "Use:" equipment showing up in the quest bar - if itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor" then - if itemCategory ~= "Quest" and itemType ~= "Quest" then - isQuestItem = false - isQuestStarter = false - end - end - - if not isQuestItem then - if itemCategory == "Quest" or itemType == "Quest" then - isQuestItem = true - end - end - - -- Check the QuestItemsDB for known faction-specific quest items - if not isQuestItem then - if itemID and addon.IsQuestItemByID then - local playerFaction = UnitFactionGroup("player") - local isDBQuestItem = addon:IsQuestItemByID(itemID, playerFaction) - if isDBQuestItem then - isQuestItem = true - end - end - end - - return isQuestItem, isQuestStarter + return false, false end --===================================================== @@ -466,50 +389,31 @@ function Guda_ItemButton_UpdateCooldown(self) end end --- Set item data -function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) - -- Proactively convert to number to avoid comparisons with strings in downstream functions - bagID = tonumber(bagID) - slotID = tonumber(slotID) - - -- Proactively clear any previous cooldown overlay state before reassigning this pooled button - do - local cd = getglobal(self:GetName().."Cooldown") or self.cooldown - if cd then - if CooldownFrame_SetTimer then - CooldownFrame_SetTimer(cd, 0, 0, 0) - elseif CooldownFrame_Set then - CooldownFrame_Set(cd, 0, 0, 0) - end - if cd.Hide then cd:Hide() end - end - end +--===================================================== +-- Helper functions for SetItem (extracted for clarity) +--===================================================== - -- Reset all visual state before reassigning pooled button +-- Reset all visual state on a button (for reuse from pool) +local function ResetButtonVisualState(self) if self.questBorder then self.questBorder:Hide() end if self.questIcon then self.questIcon:Hide() end if self.qualityBorder then self.qualityBorder:Hide() end if self.unusableOverlay then self.unusableOverlay:Hide() end - self.bagID = bagID - self.slotID = slotID - -- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior - -- ALWAYS set ID to something (0 if nil) to avoid leaking old IDs when button is reused - if self.SetID then - self:SetID(slotID or 0) + -- Clear cooldown overlay + local cd = getglobal(self:GetName().."Cooldown") or self.cooldown + if cd then + if CooldownFrame_SetTimer then + CooldownFrame_SetTimer(cd, 0, 0, 0) + elseif CooldownFrame_Set then + CooldownFrame_Set(cd, 0, 0, 0) + end + if cd.Hide then cd:Hide() end end - -- Explicitly set bag index to avoid Blizzard's ContainerFrameItemButton_OnEnter logic - -- from picking up this button as part of a real bag. - self.bagIndex = bagID or -100 -- Use an invalid bag index for non-bag buttons - self.itemData = itemData - self.isBank = isBank or false - self.otherChar = otherCharName - self.isReadOnly = isReadOnly or false -- Track if this is read-only mode - self.isMail = false -- Clear mailbox flag by default - self.mailIndex = nil - self.mailItemIndex = nil +end - -- Re-register for drag/drop every time (crucial for button reuse in Classic/Vanilla) +-- Configure drag/drop registration based on read-only state +local function SetupDragDrop(self) if not self.isReadOnly and not self.otherChar then if self.RegisterForDrag then self:RegisterForDrag("LeftButton") @@ -529,137 +433,364 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha self:EnableMouse(true) -- Still enable mouse for tooltips end end +end - -- Default to true if not specified (for non-filtered displays) +-- Get display texture and count for an item slot +-- Returns: texture, count, hasItem (boolean) +local function GetItemDisplayInfo(bagID, slotID, itemData, isReadOnly) + local displayTexture, displayCount + local hasItem = false + + if not isReadOnly then + -- LIVE MODE: Query game state directly + local liveTexture, liveCount = GetContainerItemInfo(bagID, slotID) + if liveTexture then + displayTexture = liveTexture + displayCount = liveCount + hasItem = true + elseif bagID == -2 then + -- Fallback for keyring in 1.12.1 + local link = GetContainerItemLink(bagID, slotID) + if link then + hasItem = true + local _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(link) + displayTexture = itemTexture + displayCount = 1 + end + end + else + -- READ-ONLY MODE: Use cached itemData + if itemData and itemData.texture then + displayTexture = itemData.texture + displayCount = itemData.count + hasItem = true + end + end + + return displayTexture, displayCount, hasItem +end + +-- Update the tracking checkmark on an item button +local function UpdateTrackingCheckmark(self, Utils) + local check = getglobal(self:GetName().."_Check") + if not check then return end + + local isTracked = false + if self.hasItem and self.itemData and self.itemData.link then + local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(self.itemData.link) + if itemID then + local trackedItems = Utils and Utils.SafeCall and Utils:SafeCall("DB", "GetSetting", "trackedItems") or {} + if trackedItems[itemID] then + isTracked = true + end + end + end + + if isTracked then + check:Show() + else + check:Hide() + end +end + +-- Resize empty slot background to match icon size +local function UpdateEmptySlotBackground(self, emptySlotBg, iconSize) + if not emptySlotBg then return end + + emptySlotBg:ClearAllPoints() + -- Use smaller padding for small icons + local bgPadding = iconSize < 44 and 1 or 2 + emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", -bgPadding, bgPadding) + emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", bgPadding, -bgPadding) + emptySlotBg:SetTexCoord(0.1, 0.9, 0.1, 0.9) +end + +-- Resize texture elements to match button size +local function ResizeTextureElements(self, iconSize) + -- Pushed texture + local pushedTexture = getglobal(self:GetName().."PushedTexture") + if not pushedTexture and self.GetPushedTexture then + pushedTexture = self:GetPushedTexture() + end + if pushedTexture then + pushedTexture:ClearAllPoints() + pushedTexture:SetPoint("CENTER", self, "CENTER", 0, 0) + pushedTexture:SetWidth(iconSize) + pushedTexture:SetHeight(iconSize) + end + + -- Highlight texture + local highlightTexture = getglobal(self:GetName().."HighlightTexture") + if not highlightTexture and self.GetHighlightTexture then + highlightTexture = self:GetHighlightTexture() + end + if highlightTexture then + highlightTexture:ClearAllPoints() + highlightTexture:SetPoint("CENTER", self, "CENTER", 0, 0) + highlightTexture:SetWidth(iconSize) + highlightTexture:SetHeight(iconSize) + end + + -- Checked texture + local checkedTexture = getglobal(self:GetName().."CheckedTexture") + if not checkedTexture and self.GetCheckedTexture then + checkedTexture = self:GetCheckedTexture() + end + if checkedTexture then + checkedTexture:ClearAllPoints() + checkedTexture:SetPoint("CENTER", self, "CENTER", 0, 0) + checkedTexture:SetWidth(iconSize) + checkedTexture:SetHeight(iconSize) + end +end + +-- Position icon and borders based on icon size +local function PositionIconAndBorders(self, iconSize) + local iconTexture = getglobal(self:GetName().."IconTexture") + if not iconTexture then + iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon + end + + if not iconTexture or not self.hasItem then return end + + -- Calculate icon inset based on size + local iconInset = iconSize < 44 and 10 or 15 + local iconDisplaySize = iconSize - iconInset + + iconTexture:ClearAllPoints() + iconTexture:SetPoint("CENTER", self, "CENTER", -0.5, 0.5) + iconTexture:SetWidth(iconDisplaySize) + iconTexture:SetHeight(iconDisplaySize) + iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92) + iconTexture:Show() + + -- Position quality border around the icon + if self.qualityBorder then + self.qualityBorder:ClearAllPoints() + self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) + self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) + end + + -- Position quest border around the icon + if self.questBorder then + self.questBorder:ClearAllPoints() + self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) + self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) + end + + -- Position quest icon in top-right corner + if self.questIcon then + local questIconSize = math.max(12, math.min(20, iconSize * 0.35)) + self.questIcon:SetWidth(questIconSize) + self.questIcon:SetHeight(questIconSize) + self.questIcon:ClearAllPoints() + self.questIcon:SetPoint("TOPRIGHT", self, "TOPRIGHT", 1, 0) + end +end + +-- Update quality border display +local function UpdateQualityBorder(self, itemQuality, itemLink, bagID, Utils) + if not self.qualityBorder then return end + + if bagID == -2 then + -- Special border for keyring items (cyan/blue) + self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1) + self.qualityBorder:Show() + return + end + + if not itemQuality then + self.qualityBorder:Hide() + return + end + + -- Check settings + local showEquipmentBorder, showOtherBorder + if Utils and Utils.SafeCall then + showEquipmentBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderEquipment") + showOtherBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderOther") + end + + if showEquipmentBorder == nil then showEquipmentBorder = true end + if showOtherBorder == nil then showOtherBorder = true end + + -- Check if item is equipment + local isEquipment = false + if itemLink and Utils and Utils.IsEquipment then + isEquipment = Utils:IsEquipment(itemLink) + end + + local shouldShowBorder = (isEquipment and showEquipmentBorder) or (not isEquipment and showOtherBorder) + + if shouldShowBorder then + local r, g, b = 1, 1, 1 + if Utils and Utils.GetQualityColor then + r, g, b = Utils:GetQualityColor(itemQuality) + end + self.qualityBorder:SetBackdropBorderColor(r, g, b, 1) + self.qualityBorder:Show() + else + self.qualityBorder:Hide() + end +end + +-- Clear item button for empty slot +local function ClearItemButton(self, emptySlotBg, countText, bagID) + self.hasItem = false + + if SetItemButtonTexture then SetItemButtonTexture(self, nil) end + if SetItemButtonCount then SetItemButtonCount(self, 0) end + if SetItemButtonDesaturated then SetItemButtonDesaturated(self, false) end + + -- Clear cooldown overlay + local cooldown = getglobal(self:GetName().."Cooldown") or self.cooldown + if cooldown and cooldown.Hide then cooldown:Hide() end + + -- Clear icon texture + local iconTexture = getglobal(self:GetName().."IconTexture") + if not iconTexture then + iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon + end + if iconTexture then + iconTexture:SetTexture(nil) + iconTexture:Hide() + end + + -- Clear unusable tint + if SetItemButtonTextureVertexColor then + SetItemButtonTextureVertexColor(self, 1.0, 1.0, 1.0) + end + if self.unusableOverlay and self.unusableOverlay.Hide then + self.unusableOverlay:Hide() + end + + -- Hide normal texture + self:SetNormalTexture("") + local normalBorder = getglobal(self:GetName().."NormalTexture") + if normalBorder then normalBorder:SetTexture("") end + + -- Show/hide empty slot background + if emptySlotBg then + emptySlotBg:Show() + emptySlotBg:SetAlpha(0.5) + end + + if countText then countText:Hide() end + + -- Handle quality border for empty keyring slots + if self.qualityBorder then + if bagID == -2 then + self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 0.5) + self.qualityBorder:Show() + else + self.qualityBorder:Hide() + end + end + + -- Hide quest elements + if self.questBorder then self.questBorder:Hide() end + if self.questIcon then self.questIcon:Hide() end +end + +--===================================================== +-- Main SetItem function (orchestrates helper functions) +--===================================================== + +-- Set item data +function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) + -- Proactively convert to number to avoid comparisons with strings in downstream functions + bagID = tonumber(bagID) + slotID = tonumber(slotID) + + -- Reset all visual state before reassigning pooled button + ResetButtonVisualState(self) + + -- Set button properties + self.bagID = bagID + self.slotID = slotID + if self.SetID then + self:SetID(slotID or 0) + end + self.bagIndex = bagID or -100 + self.itemData = itemData + self.isBank = isBank or false + self.otherChar = otherCharName + self.isReadOnly = isReadOnly or false + self.isMail = false + self.mailIndex = nil + self.mailItemIndex = nil + self.mailData = nil + + -- Configure drag/drop + SetupDragDrop(self) + + -- Default to true if not specified if matchesFilter == nil then matchesFilter = true end - self.mailData = nil -- Clear mail metadata by default - - -- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount) + -- Get UI elements local countText = getglobal(self:GetName().."Count") local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg") + local Utils = addon and addon.Modules and addon.Modules.Utils - -- Apply icon size setting (nil-safe) + -- Get icon size setting local iconSize = 37 - if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then - iconSize = addon.Modules.DB:GetSetting("iconSize") or iconSize - elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then - iconSize = Guda.Modules.DB:GetSetting("iconSize") or iconSize + if Utils and Utils.SafeCall then + iconSize = Utils:SafeCall("DB", "GetSetting", "iconSize") or iconSize end - if addon and addon.Constants and addon.Constants.BUTTON_SIZE then + if addon and addon.Constants then iconSize = iconSize or addon.Constants.BUTTON_SIZE end self:SetWidth(iconSize) self:SetHeight(iconSize) - -- In live mode (readOnly=false), query real-time game state instead of cached DB - -- In read-only mode (readOnly=true), use cached itemData from DB - local displayTexture, displayCount + -- Get item display info (texture, count, hasItem) + local displayTexture, displayCount, hasItem = GetItemDisplayInfo(bagID, slotID, itemData, self.isReadOnly) + self.hasItem = hasItem - if not self.isReadOnly then - -- LIVE MODE: Always query game state directly, never use cached itemData - local liveTexture, liveCount = GetContainerItemInfo(bagID, slotID) - if liveTexture then - displayTexture = liveTexture - displayCount = liveCount - self.hasItem = true - elseif bagID == -2 then - -- Fallback for keyring in 1.12.1 - local link = GetContainerItemLink(bagID, slotID) - if link then - self.hasItem = true - -- We might not have the texture from GetContainerItemInfo, try to get it from GetItemInfo - local _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(link) - displayTexture = itemTexture - displayCount = 1 -- Keyring items are usually unique anyway - else - self.hasItem = false - end - else - -- No item in this slot (even if itemData has cached data) - self.hasItem = false - end - else - -- READ-ONLY MODE: Use cached itemData from DB (can't query other characters) - if itemData and itemData.texture then - displayTexture = itemData.texture - displayCount = itemData.count - self.hasItem = true - else - self.hasItem = false - end - end - - -- Apply the determined texture and count + -- Apply display based on whether slot has item if self.hasItem then - if SetItemButtonTexture then - SetItemButtonTexture(self, displayTexture) + -- Set texture + if SetItemButtonTexture then + SetItemButtonTexture(self, displayTexture) end - - -- Explicitly set and show icon texture as SetItemButtonTexture can be unreliable for custom paths in 1.12 local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon if iconTexture and displayTexture then iconTexture:SetTexture(displayTexture) iconTexture:Show() end + -- Set count if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end if emptySlotBg then emptySlotBg:Hide() end + -- Update cooldown overlay for live items if not self.isReadOnly and not self.otherChar and Guda_ItemButton_UpdateCooldown then Guda_ItemButton_UpdateCooldown(self) else - -- Ensure cooldown is hidden for read-only/other character views local cd = getglobal(self:GetName().."Cooldown") or self.cooldown if cd and cd.Hide then cd:Hide() end end + -- Update unusable red overlay tint if Guda_ItemButton_UpdateUsableTint then - Guda_ItemButton_UpdateUsableTint(self) + Guda_ItemButton_UpdateUsableTint(self) end else - -- Fully clear all item button state for empty slots - if SetItemButtonTexture then SetItemButtonTexture(self, nil) end - if SetItemButtonCount then SetItemButtonCount(self, 0) end - if SetItemButtonDesaturated then SetItemButtonDesaturated(self, false) end - - -- Ensure cooldown overlay is hidden for empty slots - local cooldown = getglobal(self:GetName().."Cooldown") or self.cooldown - if cooldown and cooldown.Hide then cooldown:Hide() end - - -- Also clear the icon texture directly - local iconTexture = getglobal(self:GetName().."IconTexture") - if not iconTexture then - iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon - end - if iconTexture then - iconTexture:SetTexture(nil) - iconTexture:Hide() - end - - if emptySlotBg then emptySlotBg:Show() end - - -- Ensure any unusable tint is cleared on empty - if SetItemButtonTextureVertexColor then - SetItemButtonTextureVertexColor(self, 1.0, 1.0, 1.0) - end - - -- Clear unusable overlay for empty slots ✅ FIX: Clear red overlay when item is sold - if self.unusableOverlay and self.unusableOverlay.Hide then - self.unusableOverlay:Hide() - end + -- Clear empty slot + ClearItemButton(self, emptySlotBg, countText, bagID) end -- Update tracking checkmark + UpdateTrackingCheckmark(self, Utils) local check = getglobal(self:GetName().."_Check") if check then local isTracked = false - if self.hasItem then - local itemID = addon.Modules.Utils:ExtractItemID(self.itemData and self.itemData.link) + if self.hasItem and self.itemData and self.itemData.link then + local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(self.itemData.link) if itemID then - local trackedItems = addon.Modules.DB:GetSetting("trackedItems") or {} + local trackedItems = Utils and Utils.SafeCall and Utils:SafeCall("DB", "GetSetting", "trackedItems") or {} if trackedItems[itemID] then isTracked = true end @@ -735,10 +866,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha if countText and countText.GetFont then local font, _, flags = countText:GetFont() local fontSize = 12 - if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then - fontSize = addon.Modules.DB:GetSetting("iconFontSize") or fontSize - elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then - fontSize = Guda.Modules.DB:GetSetting("iconFontSize") or fontSize + if Utils and Utils.SafeCall then + fontSize = Utils:SafeCall("DB", "GetSetting", "iconFontSize") or fontSize end countText:SetFont(font, fontSize, flags) @@ -814,14 +943,11 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1) self.qualityBorder:Show() elseif itemQuality then - -- Check settings to determine if we should show borders (nil-safe) + -- Check settings to determine if we should show borders local showEquipmentBorder, showOtherBorder - if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then - showEquipmentBorder = addon.Modules.DB:GetSetting("showQualityBorderEquipment") - showOtherBorder = addon.Modules.DB:GetSetting("showQualityBorderOther") - elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then - showEquipmentBorder = Guda.Modules.DB:GetSetting("showQualityBorderEquipment") - showOtherBorder = Guda.Modules.DB:GetSetting("showQualityBorderOther") + if Utils and Utils.SafeCall then + showEquipmentBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderEquipment") + showOtherBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderOther") end -- Default to true if settings not found @@ -832,14 +958,10 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha showOtherBorder = true end - -- Check if item is equipment (nil-safe) + -- Check if item is equipment local isEquipment = false - if itemLink then - if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then - isEquipment = addon.Modules.Utils:IsEquipment(itemLink) - elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then - isEquipment = Guda.Modules.Utils:IsEquipment(itemLink) - end + if itemLink and Utils and Utils.IsEquipment then + isEquipment = Utils:IsEquipment(itemLink) end -- Determine if we should show the border based on item type and settings @@ -848,10 +970,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha if shouldShowBorder then -- Show colored border for all items (Poor, Common, Uncommon, Rare, Epic, etc.) local r, g, b = 1, 1, 1 - if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.GetQualityColor then - r, g, b = addon.Modules.Utils:GetQualityColor(itemQuality) - elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.GetQualityColor then - r, g, b = Guda.Modules.Utils:GetQualityColor(itemQuality) + if Utils and Utils.GetQualityColor then + r, g, b = Utils:GetQualityColor(itemQuality) end self.qualityBorder:SetBackdropBorderColor(r, g, b, 1) self.qualityBorder:Show()