← Back to Repository

RLGearscore.lua

Hash: c92992cf9a8e595f23cf15099cc1332203e44f5c

File Content
Raw / Download
local addonName, addon = ...
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("INSPECT_READY")
frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")

-- Default Settings
local defaults = {
    autoInspect = true,
    showGS = true,
    showiLvl = true,
    showTalents = true,
    showOverlay = true,
    language = "Auto",
    minimapPos = 45 -- degrees
}

-- Options Frame
local optionsFrame
local settingsPanel, aboutPanel

local function CreateOptionsFrame()
    if optionsFrame then return end
    
    optionsFrame = CreateFrame("Frame", "RLGearscoreOptions", UIParent, "BackdropTemplate")
    optionsFrame:SetSize(300, 500)
    optionsFrame:SetPoint("CENTER")
    optionsFrame:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left = 11, right = 12, top = 12, bottom = 11 }
    })
    optionsFrame:SetMovable(true)
    optionsFrame:EnableMouse(true)
    optionsFrame:RegisterForDrag("LeftButton")
    optionsFrame:SetScript("OnDragStart", optionsFrame.StartMoving)
    optionsFrame:SetScript("OnDragStop", optionsFrame.StopMovingOrSizing)
    optionsFrame:Hide()
    
    -- Title
    local title = optionsFrame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    title:SetPoint("TOP", 0, -15)
    title:SetText(addon.L["OPTIONS_TITLE"])
    optionsFrame.title = title

    -- Tabs Container
    local tab1 = CreateFrame("Button", nil, optionsFrame, "UIPanelButtonTemplate")
    tab1:SetSize(100, 25)
    tab1:SetPoint("TOPLEFT", 20, -45)
    tab1:SetText(addon.L["TAB_SETTINGS"])
    optionsFrame.tab1 = tab1
    
    local tab2 = CreateFrame("Button", nil, optionsFrame, "UIPanelButtonTemplate")
    tab2:SetSize(100, 25)
    tab2:SetPoint("TOPRIGHT", -20, -45)
    tab2:SetText(addon.L["TAB_ABOUT"])
    optionsFrame.tab2 = tab2

    -- Panels
    settingsPanel = CreateFrame("Frame", nil, optionsFrame)
    settingsPanel:SetPoint("TOPLEFT", 0, -80)
    settingsPanel:SetPoint("BOTTOMRIGHT", 0, 40)
    
    aboutPanel = CreateFrame("Frame", nil, optionsFrame)
    aboutPanel:SetPoint("TOPLEFT", 0, -80)
    aboutPanel:SetPoint("BOTTOMRIGHT", 0, 40)
    aboutPanel:Hide()

    -- Tab Logic
    tab1:SetScript("OnClick", function()
        settingsPanel:Show()
        aboutPanel:Hide()
        tab1:Disable()
        tab2:Enable()
    end)
    
    tab2:SetScript("OnClick", function()
        settingsPanel:Hide()
        aboutPanel:Show()
        tab1:Enable()
        tab2:Disable()
    end)
    tab1:Disable() -- Default active

    -- === Settings Panel Content ===
    
    -- Checkboxes
    local function CreateCheckbox(key, labelText, relativeTo, yOffset)
        local cb = CreateFrame("CheckButton", nil, settingsPanel, "UICheckButtonTemplate")
        if relativeTo then
             cb:SetPoint("TOPLEFT", relativeTo, "BOTTOMLEFT", 0, yOffset)
        else
             cb:SetPoint("TOPLEFT", 20, yOffset)
        end
        cb.text = cb:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        cb.text:SetPoint("LEFT", cb, "RIGHT", 5, 1)
        cb.text:SetText(labelText)
        cb:SetChecked(GearScoreDB[key])
        cb:SetScript("OnClick", function(self)
            GearScoreDB[key] = self:GetChecked()
        end)
        return cb
    end

    local cb1 = CreateCheckbox("showGS", addon.L["ENABLE_GS"], nil, 0)
    settingsPanel.cb1 = cb1
    
    local cb2 = CreateCheckbox("showiLvl", addon.L["ENABLE_ILVL"], cb1, -10)
    settingsPanel.cb2 = cb2
    
    local cb3 = CreateCheckbox("showTalents", addon.L["ENABLE_TALENTS"], cb2, -10)
    settingsPanel.cb3 = cb3
    
    local cb4 = CreateCheckbox("autoInspect", addon.L["ENABLE_AUTO_INSPECT"], cb3, -10)
    settingsPanel.cb4 = cb4

    local cb5 = CreateCheckbox("showOverlay", addon.L["ENABLE_OVERLAY"], cb4, -10)
    settingsPanel.cb5 = cb5

    -- Language Dropdown
    local langLabel = settingsPanel:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    langLabel:SetPoint("TOPLEFT", cb5, "BOTTOMLEFT", 0, -20)
    langLabel:SetText(addon.L["LANGUAGE"])
    settingsPanel.langLabel = langLabel

    local dropDown = CreateFrame("Frame", "RLGearscoreLangDropDown", settingsPanel, "UIDropDownMenuTemplate")
    dropDown:SetPoint("TOPLEFT", langLabel, "BOTTOMLEFT", -15, -10)
    UIDropDownMenu_SetWidth(dropDown, 150)
    UIDropDownMenu_SetText(dropDown, GearScoreDB.language or "Auto")

    local langs = { "Auto", "enUS", "deDE", "ruRU" }
    
    UIDropDownMenu_Initialize(dropDown, function(self, level, menuList)
        local info = UIDropDownMenu_CreateInfo()
        for _, lang in ipairs(langs) do
            info.text = lang
            info.checked = (GearScoreDB.language == lang)
            info.func = function()
                GearScoreDB.language = lang
                UIDropDownMenu_SetText(dropDown, lang)
                addon:SetLanguage(lang)
                -- Update UI texts
                optionsFrame.title:SetText(addon.L["OPTIONS_TITLE"])
                optionsFrame.tab1:SetText(addon.L["TAB_SETTINGS"])
                optionsFrame.tab2:SetText(addon.L["TAB_ABOUT"])
                settingsPanel.cb1.text:SetText(addon.L["ENABLE_GS"])
                settingsPanel.cb2.text:SetText(addon.L["ENABLE_ILVL"])
                settingsPanel.cb3.text:SetText(addon.L["ENABLE_TALENTS"])
                settingsPanel.cb4.text:SetText(addon.L["ENABLE_AUTO_INSPECT"])
                settingsPanel.cb5.text:SetText(addon.L["ENABLE_OVERLAY"])
                settingsPanel.langLabel:SetText(addon.L["LANGUAGE"])
                -- Update About texts
                aboutPanel.copyHeader:SetText(addon.L["COPYRIGHT_HEADER"])
                aboutPanel.creditsHeader:SetText(addon.L["CREDITS_HEADER"])
                aboutPanel.creditsText:SetText(addon.L["CREDITS_TEXT"])
                
                print("|cffffd100RLGearscore:|r " .. addon.L["LOADED_MESSAGE"])
            end
            UIDropDownMenu_AddButton(info)
        end
    end)
    
    -- === About Panel Content ===
    
    -- Logo
    local logo = aboutPanel:CreateTexture(nil, "ARTWORK")
    logo:SetTexture("Interface\\AddOns\\RLGearscore\\img\\logo.png")
    logo:SetSize(64, 64)
    logo:SetPoint("TOP", 0, -10)
    
    -- Copyright Header
    local copyHeader = aboutPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    copyHeader:SetPoint("TOP", logo, "BOTTOM", 0, -10)
    copyHeader:SetText(addon.L["COPYRIGHT_HEADER"])
    aboutPanel.copyHeader = copyHeader
    
    -- Copyright Text
    local copyText = aboutPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    copyText:SetPoint("TOP", copyHeader, "BOTTOM", 0, -5)
    copyText:SetWidth(260)
    copyText:SetJustifyH("CENTER")
    copyText:SetText(addon.L["ABOUT_CONTACT_INFO"])
    
    -- Git URL
    local gitEditBox = CreateFrame("EditBox", nil, aboutPanel, "InputBoxTemplate")
    gitEditBox:SetSize(250, 20)
    gitEditBox:SetPoint("TOP", copyText, "BOTTOM", 5, -10)
    gitEditBox:SetAutoFocus(false)
    gitEditBox:SetText("git clone https://xarmina.eu/git/RLGearscore.git")
    gitEditBox:SetCursorPosition(0)
    
    -- Credits Header
    local creditsHeader = aboutPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
    creditsHeader:SetPoint("TOP", gitEditBox, "BOTTOM", 0, -20)
    creditsHeader:SetText(addon.L["CREDITS_HEADER"])
    aboutPanel.creditsHeader = creditsHeader
    
    -- Credits Text
    local creditsText = aboutPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
    creditsText:SetPoint("TOP", creditsHeader, "BOTTOM", 0, -5)
    creditsText:SetWidth(260)
    creditsText:SetJustifyH("CENTER")
    creditsText:SetText(addon.L["CREDITS_TEXT"])
    aboutPanel.creditsText = creditsText
    
    -- Link
    local linkEditBox = CreateFrame("EditBox", nil, aboutPanel, "InputBoxTemplate")
    linkEditBox:SetSize(250, 20)
    linkEditBox:SetPoint("TOP", creditsText, "BOTTOM", 5, -5)
    linkEditBox:SetAutoFocus(false)
    linkEditBox:SetText("https://github.com/Hubbotu/Gearscore/blob/main/ruRU.lua")
    linkEditBox:SetCursorPosition(0)
    
    -- Close Button
    local closeBtn = CreateFrame("Button", nil, optionsFrame, "UIPanelButtonTemplate")
    closeBtn:SetSize(80, 22)
    closeBtn:SetPoint("BOTTOM", 0, 15)
    closeBtn:SetText(CLOSE)
    closeBtn:SetScript("OnClick", function() optionsFrame:Hide() end)
end

-- Minimap Button
local minimapBtn
local function CreateMinimapButton()
    minimapBtn = CreateFrame("Button", "RLGearscoreMinimapButton", Minimap)
    minimapBtn:SetSize(32, 32)
    minimapBtn:SetFrameLevel(8)
    
    minimapBtn.icon = minimapBtn:CreateTexture(nil, "BACKGROUND")
    minimapBtn.icon:SetTexture("Interface\\AddOns\\RLGearscore\\img\\logo.png")
    minimapBtn.icon:SetSize(20, 20)
    minimapBtn.icon:SetPoint("CENTER")
    
    minimapBtn.border = minimapBtn:CreateTexture(nil, "OVERLAY")
    minimapBtn.border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    minimapBtn.border:SetSize(54, 54)
    minimapBtn.border:SetPoint("TOPLEFT")
    
    minimapBtn:RegisterForDrag("LeftButton")
    minimapBtn:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")

    local function UpdatePosition()
        local angle = math.rad(GearScoreDB.minimapPos or 45)
        local x = math.cos(angle) * 80
        local y = math.sin(angle) * 80
        minimapBtn:SetPoint("CENTER", Minimap, "CENTER", x, y)
    end

    minimapBtn:SetScript("OnDragStart", function(self)
        self:LockHighlight()
        self:SetScript("OnUpdate", function(self)
            local mx, my = Minimap:GetCenter()
            local cx, cy = GetCursorPosition()
            local scale = Minimap:GetEffectiveScale()
            cx, cy = cx / scale, cy / scale
            local dx, dy = cx - mx, cy - my
            local angle = math.deg(math.atan2(dy, dx))
            GearScoreDB.minimapPos = angle
            UpdatePosition()
        end)
    end)

    minimapBtn:SetScript("OnDragStop", function(self)
        self:UnlockHighlight()
        self:SetScript("OnUpdate", nil)
    end)

    minimapBtn:SetScript("OnClick", function(self, button)
        if button == "LeftButton" then
            if not optionsFrame then CreateOptionsFrame() end
            if optionsFrame:IsShown() then optionsFrame:Hide() else optionsFrame:Show() end
        end
    end)

    minimapBtn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:SetText("RLGearscore")
        GameTooltip:AddLine(addon.L["MINIMAP_BUTTON_TOOLTIP"], 1, 1, 1)
        GameTooltip:Show()
    end)
    
    minimapBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
    
    UpdatePosition()
end

-- DB Handling
function frame:OnEvent(event, arg1, arg2)
    if event == "PLAYER_LOGIN" then
        if not GearScoreDB then GearScoreDB = {} end
        for k, v in pairs(defaults) do
            if GearScoreDB[k] == nil then GearScoreDB[k] = v end
        end
        
        -- Apply Language
        addon:SetLanguage(GearScoreDB.language)
        
        -- Init UI
        CreateMinimapButton()
        
        print("|cffffd100" .. addon.L["GEARSCORE_LABEL"] .. "|r " .. addon.L["LOADED_MESSAGE"])
    elseif event == "INSPECT_READY" then
        self:INSPECT_READY(event, arg1)
    elseif event == "GET_ITEM_INFO_RECEIVED" then
        self:GET_ITEM_INFO_RECEIVED(event, arg1, arg2)
    end
end
frame:SetScript("OnEvent", frame.OnEvent)

-- Helper: GetTooltipUnit
local function GetTooltipUnit(tooltip)
    local _, unit = tooltip:GetUnit()
    
    -- Retail support for GetMouseFoci
    if not unit and GetMouseFoci then
        local foci = GetMouseFoci()
        local mouseFocus = foci and foci[1]
        if mouseFocus and mouseFocus.GetAttribute then
            unit = mouseFocus:GetAttribute("unit")
        end
    end
    
    -- Classic / Legacy support
    if not unit and GetMouseFocus then
        local mouseFocus = GetMouseFocus()
        if mouseFocus and mouseFocus.GetAttribute then
            unit = mouseFocus:GetAttribute("unit")
        end
    end
    
    if not unit and UnitExists("mouseover") then unit = "mouseover" end
    if not unit or not UnitIsPlayer(unit) then return nil end
    return unit
end

-- Helper: GetTabPoints (Handles API variations)
local function GetTabPoints(tabIndex, isInspect)
    if not GetTalentTabInfo then return 0, nil end
    local t1, t2, t3, t4, t5 = GetTalentTabInfo(tabIndex, isInspect)
    -- If first return is a number (ID), it's the new API: id, name, desc, icon, points
    if type(t1) == "number" then
        return tonumber(t5) or 0, t2 -- Return points and name (t2)
    else
        -- Old API: name, icon, points, ...
        return tonumber(t3) or 0, t1 -- Return points and name (t1)
    end
end

-- Helper: GetUnitTalentString (Cross-version)
local function GetUnitTalentString(unit, isInspect)
    local talentString = ""
    local validScan = false
    local totalPoints = 0

    if WOW_PROJECT_ID == WOW_PROJECT_MAINLINE then
        -- Retail: Use Specialization
        local specID = 0
        if isInspect then
            specID = GetInspectSpecialization(unit)
        else
            local specIndex = GetSpecialization()
            if specIndex then
                specID = GetSpecializationInfo(specIndex)
            end
        end
        
        if specID and specID > 0 then
            local _, name = GetSpecializationInfoByID(specID)
            talentString = name
            validScan = true
            totalPoints = 1 -- Dummy value to indicate success
        end
    else
        -- Classic / Cata: Use Talent Tabs
        for i = 1, 3 do
            local pointsSpent, name = GetTabPoints(i, isInspect)
            if name then validScan = true end
            totalPoints = totalPoints + pointsSpent
            if i > 1 then talentString = talentString .. "/" end
            talentString = talentString .. pointsSpent
        end
    end
    
    return talentString, validScan, totalPoints
end

-- Helper: SmartInspect
local lastInspectTime = 0
local currentInspectGUID = nil
local function SmartInspect(unit)
    if not unit or not UnitExists(unit) or not UnitIsConnected(unit) or not CanInspect(unit) then return end
    if InspectFrame and InspectFrame:IsShown() then return end
    
    if not GearScoreDB.autoInspect and not IsShiftKeyDown() then return end

    local now = GetTime()
    if (now - lastInspectTime > 1.5) then
        lastInspectTime = now
        
        -- Suppress UI errors (red text) during inspect to avoid spam
        local originalSound = GetCVar("Sound_EnableSFX")
        UIErrorsFrame:UnregisterEvent("UI_ERROR_MESSAGE")
        
        if ClearInspectPlayer then ClearInspectPlayer() end
        NotifyInspect(unit)
        currentInspectGUID = UnitGUID(unit)
        
        C_Timer.After(0.1, function() UIErrorsFrame:RegisterEvent("UI_ERROR_MESSAGE") end)
    else
         -- Retry logic
         local delay = 1.55 - (now - lastInspectTime)
         C_Timer.After(delay, function()
             if GameTooltip:IsVisible() then
                 local tooltipUnit = GetTooltipUnit(GameTooltip)
                 if tooltipUnit and UnitIsUnit(tooltipUnit, unit) then
                     SmartInspect(unit)
                 end
             end
         end)
    end
end

-- Helper: GetUnitGearInfo (The Core Logic)
local gearCache = {}
local function GetUnitGearInfo(unit)
    local totalILvl = 0
    local itemCount = 0
    local gearScore = 0
    local guid = UnitGUID(unit)
    
    -- Slots to check (Head to Trinket2, MainHand, OffHand, Ranged)
    local slots = {1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}
    local missingData = false
    local hasItemIDs = false
    
    for _, slot in ipairs(slots) do
        local itemID = GetInventoryItemID(unit, slot)
        if itemID then
            hasItemIDs = true
            local _, _, rarity, itemLevel = GetItemInfo(itemID)
            if itemLevel and itemLevel > 0 then
                -- Custom GearScore Calculation
                local multiplier = 1
                if rarity == 0 then multiplier = 0       -- Poor
                elseif rarity == 1 then multiplier = 1   -- Common
                elseif rarity == 2 then multiplier = 1.5 -- Uncommon
                elseif rarity == 3 then multiplier = 2   -- Rare
                elseif rarity == 4 then multiplier = 3.5 -- Epic
                elseif rarity == 5 then multiplier = 5   -- Legendary
                end
                
                -- Slot Weights
                local slotMult = 1
                if slot == 1 or slot == 5 or slot == 7 or slot == 16 or slot == 17 or slot == 18 then -- Big pieces
                    slotMult = 1.0
                elseif slot == 3 or slot == 6 or slot == 8 or slot == 10 or slot == 13 or slot == 14 then -- Medium
                    slotMult = 0.75
                else -- Small
                    slotMult = 0.5
                end
                
                gearScore = gearScore + (itemLevel * multiplier * slotMult)
                totalILvl = totalILvl + itemLevel
                itemCount = itemCount + 1
            else
                missingData = true
            end
        end
    end
    
    local avgILvl = 0
    if itemCount > 0 then
        avgILvl = math.floor(totalILvl / itemCount)
        local finalGS = math.floor(gearScore)
        
        if guid and not missingData then
             if not gearCache[guid] then gearCache[guid] = {} end
             gearCache[guid].gs = finalGS
             gearCache[guid].ilvl = avgILvl
             gearCache[guid].time = GetTime()
        end
        return finalGS, avgILvl, false, hasItemIDs
    end
    
    if guid and gearCache[guid] and gearCache[guid].gs then
        local cache = gearCache[guid]
        return cache.gs, cache.ilvl, true, hasItemIDs
    end
    
    return math.floor(gearScore), avgILvl, false, hasItemIDs
end

-- Event Handlers
function frame:INSPECT_READY(event, inspectGUID)
    if not inspectGUID then return end
    -- Only process if this matches our most recent inspection request
    if currentInspectGUID and inspectGUID ~= currentInspectGUID then return end
    
    local function AttemptTalentRead(attemptsLeft)
        if currentInspectGUID and inspectGUID ~= currentInspectGUID then return end -- Abort if target changed
        
        -- Try to find the unit for this GUID (needed for Retail GetInspectSpecialization)
        local unit = nil
        if UnitGUID("mouseover") == inspectGUID then unit = "mouseover"
        elseif UnitGUID("target") == inspectGUID then unit = "target"
        elseif UnitGUID("focus") == inspectGUID then unit = "focus"
        end

        -- In Classic, GetTabPoints(i, true) uses the last inspected unit internally, so unit arg is less strict,
        -- but for Retail we need a valid UnitID.
        
        if not unit and WOW_PROJECT_ID == WOW_PROJECT_MAINLINE then
             -- Can't read retail specs without a UnitID
             if attemptsLeft > 0 then
                 C_Timer.After(0.2, function() AttemptTalentRead(attemptsLeft - 1) end)
             end
             return
        end
        
        -- Use "mouseover" as fallback for Classic if unit not found (it usually works if inspect is active)
        if not unit then unit = "mouseover" end

        local talentString, validScan, totalPoints = GetUnitTalentString(unit, true)
        
        if validScan then
             -- If we have points or we are out of retries, save it.
             -- If 0 points and we have retries left, wait and retry.
             if totalPoints > 0 or attemptsLeft <= 0 then
                 if not gearCache[inspectGUID] then gearCache[inspectGUID] = {} end
                 gearCache[inspectGUID].talents = talentString
                 
                 -- Update tooltip
                 if GameTooltip:IsVisible() then
                    local unit = GetTooltipUnit(GameTooltip)
                    if unit then
                        local unitGUID = UnitGUID(unit)
                        if unitGUID and inspectGUID == unitGUID then
                             GameTooltip:SetUnit(unit)
                        end
                    end
                end
             else
                 -- Retry
                 C_Timer.After(0.2, function() AttemptTalentRead(attemptsLeft - 1) end)
             end
        else
             -- No valid tabs found? Retry if possible
             if attemptsLeft > 0 then
                 C_Timer.After(0.2, function() AttemptTalentRead(attemptsLeft - 1) end)
             else
                 -- Final attempt failed, but we might still have GS/iLvl
                 if GameTooltip:IsVisible() then
                     local unit = GetTooltipUnit(GameTooltip)
                     if unit then GameTooltip:SetUnit(unit) end
                 end
             end
        end
    end
    
    -- Start trying to read talents
    C_Timer.After(0.1, function() AttemptTalentRead(3) end)
end

function frame:GET_ITEM_INFO_RECEIVED(event, itemID, success)
    -- If we were waiting for item info, refresh tooltip
    if GameTooltip:IsVisible() then
         local unit = GetTooltipUnit(GameTooltip)
         if unit then GameTooltip:SetUnit(unit) end
    end
end

-- Tooltip Hook
GameTooltip:HookScript("OnTooltipSetUnit", function(self)
    local unit = GetTooltipUnit(self)
    if not unit then return end
    
    -- Auto Inspect Trigger
    if CanInspect(unit) then
        SmartInspect(unit)
    end
    
    local guid = UnitGUID(unit)
    if not guid then return end
    
    local gs, ilvl, isCached, hasItemIDs = GetUnitGearInfo(unit)
    
    -- If we have no itemIDs (distance too far?), don't show 0
    if not hasItemIDs and gs == 0 then return end
    
    local linesAdded = false
    
    if GearScoreDB.showGS then
        local msg = addon.L["GEARSCORE_LABEL"] .. ": |cffffffff" .. gs .. "|r"
        if isCached then msg = msg .. " (*)" end
        self:AddLine(msg, 1, 0.82, 0)
        linesAdded = true
    end
    
    if GearScoreDB.showiLvl then
        local msg = addon.L["ILVL_LABEL"] .. ": |cffffffff" .. ilvl .. "|r"
        self:AddLine(msg, 1, 0.82, 0)
        linesAdded = true
    end
    
    if GearScoreDB.showTalents then
        local talents = nil
        
        -- Try to get talents (local or cached)
        if UnitIsUnit(unit, "player") then
            talents = GetUnitTalentString(unit, false)
        else
            if gearCache[guid] and gearCache[guid].talents then
                talents = gearCache[guid].talents
            end
        end
        
        if talents then
            self:AddLine(addon.L["TALENTS_LABEL"] .. ": |cffffffff" .. talents .. "|r", 1, 0.82, 0)
            linesAdded = true
        else
             -- If we are inspecting, it might come soon
        end
    end
    
    if linesAdded then self:Show() end
end)

-- === Item Level Overlay ===

local function CreateItemLevelOverlay(button)
    if not button then return end
    if button.rlOverlay then return button.rlOverlay end
    
    local text = button:CreateFontString(nil, "OVERLAY")
    text:SetPoint("TOPLEFT", 2, -2)
    
    -- Improved contrast: Yellow with thick outline and shadow
    local fontPath = GameFontNormal:GetFont()
    text:SetFont(fontPath, 11, "OUTLINE") 
    text:SetTextColor(1, 1, 0) -- Yellow
    text:SetShadowOffset(1, -1)
    text:SetShadowColor(0, 0, 0, 1)
    
    button.rlOverlay = text
    return text
end

local function UpdateItemLevelOverlay(button, link)
    if not GearScoreDB.showOverlay then
        if button.rlOverlay then button.rlOverlay:SetText("") end
        return
    end
    
    if not link then
        if button.rlOverlay then button.rlOverlay:SetText("") end
        return
    end
    
    local _, _, _, itemLevel = GetItemInfo(link)
    if itemLevel and itemLevel > 1 then
        local text = CreateItemLevelOverlay(button)
        text:SetText(itemLevel)
    else
        if button.rlOverlay then button.rlOverlay:SetText("") end
    end
end

-- Hook Character Frame (Player)
if PaperDollItemSlotButton_Update then
    hooksecurefunc("PaperDollItemSlotButton_Update", function(self)
        if self:GetID() then
             local unit = "player"
             local itemLink = GetInventoryItemLink(unit, self:GetID())
             UpdateItemLevelOverlay(self, itemLink)
        end
    end)
end

-- Hook Bags (Classic & Retail)
local function HookBags()
    if ContainerFrame_Update then
        hooksecurefunc("ContainerFrame_Update", function(frame)
             local name = frame:GetName()
             for i = 1, frame.size do
                 local button = _G[name .. "Item" .. i]
                 if button then
                     local id = button:GetID()
                    local link
                    if C_Container and C_Container.GetContainerItemLink then
                        link = C_Container.GetContainerItemLink(frame:GetID(), id)
                    elseif GetContainerItemLink then
                        link = GetContainerItemLink(frame:GetID(), id)
                    end
                    UpdateItemLevelOverlay(button, link)
                 end
             end
        end)
    end
end
HookBags()