------------------------------------------------------ -- MetaHunt: Tooltips Module -- Standalone beast tooltip augmentation from MetaHunt data ------------------------------------------------------ local MTH_Tooltips = { name = "tooltips", enabled = true, version = "1.0.3", events = { "UPDATE_MOUSEOVER_UNIT", "UNIT_NAME_UPDATE", }, initialized = false, beastIndex = nil, vendorIndex = nil, debugState = { events = 0, pollTicks = 0, pollTriggers = 0, injectAttempts = 0, added = 0, lookupMiss = 0, skipNoUnit = 0, skipPlayerControlled = 0, skipTooltipApi = 0, skipNoName = 0, skipExistingLine = 0, lastUnitName = "", lastNormalized = "", lastOutcome = "", }, hookInstalled = false, injectFrame = nil, pollFrame = nil, pollElapsed = 0, lastMouseoverKey = "", } local function MTH_TT_GetOptionsStore() if MTH and MTH.GetModuleSavedVariables then local store = MTH:GetModuleSavedVariables("tooltips") if type(store) == "table" then if store.beastTooltips == nil then store.beastTooltips = true end if store.ammoVendorTooltips == nil then store.ammoVendorTooltips = true end if store.foodItemTooltips == nil then if type(FOM_Config) == "table" and FOM_Config.Tooltip ~= nil then store.foodItemTooltips = FOM_Config.Tooltip and true or false else store.foodItemTooltips = true end end if store.ownPetTooltips == nil then store.ownPetTooltips = false end return store end end return nil end local function MTH_TT_IsBeastTooltipsEnabled() local store = MTH_TT_GetOptionsStore() if type(store) == "table" then return store.beastTooltips and true or false end return true end local function MTH_TT_IsAmmoVendorTooltipsEnabled() local store = MTH_TT_GetOptionsStore() if type(store) == "table" then return store.ammoVendorTooltips and true or false end return true end local function MTH_TT_IsFoodItemTooltipsEnabled() local store = MTH_TT_GetOptionsStore() if type(store) == "table" then return store.foodItemTooltips and true or false end return true end local function MTH_TT_IsOwnPetTooltipsEnabled() local store = MTH_TT_GetOptionsStore() if type(store) == "table" then return store.ownPetTooltips and true or false end return false end local function MTH_TT_Loc(key, fallback) if MTH and MTH.GetLocalization then return MTH:GetLocalization(key, fallback) end return fallback end local function MTH_TT_Lower(value) if value == nil then return "" end return string.lower(tostring(value)) end local function MTH_TT_Trim(value) if value == nil then return "" end local text = tostring(value) text = string.gsub(text, "^%s+", "") text = string.gsub(text, "%s+$", "") return text end local function MTH_TT_NormalizeName(value) local text = MTH_TT_Trim(value) text = MTH_TT_Lower(text) text = string.gsub(text, "[^%w%s]", " ") text = string.gsub(text, "%s+", " ") text = MTH_TT_Trim(text) return text end local function MTH_TT_Log(line) return end local function MTH_TT_StateSet(outcome, unitName, normalized) local state = MTH_Tooltips and MTH_Tooltips.debugState if not state then return end state.lastOutcome = tostring(outcome or "") state.lastUnitName = tostring(unitName or "") state.lastNormalized = tostring(normalized or "") end local function MTH_TT_BuildIndex() local index = { byName = {} } if type(MTH_DS_Beasts) ~= "table" then MTH_TT_Log("build index skipped: MTH_DS_Beasts missing") return index end local count = 0 for _, row in pairs(MTH_DS_Beasts) do if type(row) == "table" then local beastName = MTH_TT_Trim(row.name) if beastName ~= "" then local key = MTH_TT_NormalizeName(beastName) if not index.byName[key] then index.byName[key] = row count = count + 1 end end end end MTH_TT_Log("build index complete: uniqueNames=" .. tostring(count)) return index end local function MTH_TT_FindBeastRow(name) if not MTH_Tooltips.beastIndex then MTH_Tooltips.beastIndex = MTH_TT_BuildIndex() end local lookup = MTH_TT_NormalizeName(name) if lookup == "" then return nil end return MTH_Tooltips.beastIndex.byName[lookup] end local function MTH_TT_BuildVendorIndex() local index = { byName = {} } if type(MTH_DS_AmmoItems) ~= "table" or type(MTH_DS_Vendors) ~= "table" then MTH_TT_Log("build vendor index skipped: ammo items or vendors missing") return index end for _, item in pairs(MTH_DS_AmmoItems) do if type(item) == "table" and type(item.vendors) == "table" then local subtype = MTH_TT_Lower(item.subtype) local hasArrows = subtype == "arrow" local hasBullets = subtype == "bullet" if hasArrows or hasBullets then for vendorId in pairs(item.vendors) do local id = tonumber(vendorId) or vendorId local vendor = MTH_DS_Vendors[id] if type(vendor) == "table" then local vendorName = MTH_TT_Trim(vendor.name) local key = MTH_TT_NormalizeName(vendorName) if key ~= "" then local entry = index.byName[key] if not entry then entry = { name = vendorName, arrows = false, bullets = false, } index.byName[key] = entry end if hasArrows then entry.arrows = true end if hasBullets then entry.bullets = true end end end end end end end return index end local function MTH_TT_FindVendorInfo(name) if not MTH_Tooltips.vendorIndex then MTH_Tooltips.vendorIndex = MTH_TT_BuildVendorIndex() end local lookup = MTH_TT_NormalizeName(name) if lookup == "" then return nil end return MTH_Tooltips.vendorIndex.byName[lookup] end local function MTH_TT_HasExistingTooltipLine() if not GameTooltip or not GameTooltip.NumLines then return false end for i = 1, GameTooltip:NumLines() do local fontString = getglobal("GameTooltipTextLeft" .. i) if fontString and fontString.GetText then local text = fontString:GetText() if text and (string.find(text, "Pet Abilities", 1, true) or string.find(text, "MetaHunt Abilities", 1, true)) then return true end end end return false end local function MTH_TT_HasExistingVendorTooltipLine() if not GameTooltip or not GameTooltip.NumLines then return false end for i = 1, GameTooltip:NumLines() do local fontString = getglobal("GameTooltipTextLeft" .. i) if fontString and fontString.GetText then local text = fontString:GetText() if text and ( string.find(text, "Sells Arrows and Bullets", 1, true) or string.find(text, "Sells Arrows", 1, true) or string.find(text, "Sells Bullets", 1, true) ) then return true end end end return false end local function MTH_TT_HasExistingFoodTooltipLine() if not GameTooltip or not GameTooltip.NumLines then return false end local markers = { MTH_TT_Loc("TOOLTIPS_FOOD_MARKER_UNKNOWN", "can eat this, but hasn't tried it yet."), MTH_TT_Loc("TOOLTIPS_FOOD_MARKER_UNDER", "doesn't like this anymore."), MTH_TT_Loc("TOOLTIPS_FOOD_MARKER_MIGHT", "might eat this."), MTH_TT_Loc("TOOLTIPS_FOOD_MARKER_WILL", "will eat this."), MTH_TT_Loc("TOOLTIPS_FOOD_MARKER_LIKE", "likes to eat this."), MTH_TT_Loc("TOOLTIPS_FOOD_MARKER_LOVE", "loves to eat this."), } for i = 1, GameTooltip:NumLines() do local fontString = getglobal("GameTooltipTextLeft" .. i) if fontString and fontString.GetText then local text = fontString:GetText() if text then for j = 1, table.getn(markers) do local marker = markers[j] if marker and marker ~= "" and string.find(text, marker, 1, true) then return true end end end end end return false end local function MTH_TT_ExtractItemIdFromLink(link) if type(link) ~= "string" then return nil end local _, _, itemIdText = string.find(link, "item:(%d+):") if itemIdText then return tonumber(itemIdText) end return nil end local function MTH_TT_GetQuestColor(name, defaultR, defaultG, defaultB) if type(QuestDifficultyColor) == "table" then local color = QuestDifficultyColor[name] if type(color) == "table" and color.r and color.g and color.b then return color.r, color.g, color.b end end return defaultR, defaultG, defaultB end local function MTH_TT_FormatElapsedStableStyle(timestamp) local value = tonumber(timestamp) if not value or value <= 0 then return nil end local now = tonumber(time()) or 0 local elapsed = now - value if elapsed < 0 then elapsed = 0 end local days = math.floor(elapsed / 86400) local remAfterDays = elapsed - (days * 86400) local hours = math.floor(remAfterDays / 3600) local remAfterHours = remAfterDays - (hours * 3600) local mins = math.floor(remAfterHours / 60) if days > 0 then if hours > 0 then return tostring(days) .. " days " .. tostring(hours) .. " hours" end return tostring(days) .. " days" end if hours > 0 then if mins > 0 then return tostring(hours) .. " hours " .. tostring(mins) .. " minutes" end return tostring(hours) .. " hours" end return tostring(mins) .. " minutes" end local function MTH_TT_GetTooltipUnitToken() if not GameTooltip or type(GameTooltip.GetUnit) ~= "function" then return nil end local ok, unitA, unitB = pcall(function() return GameTooltip:GetUnit() end) if not ok then return nil end if type(unitB) == "string" and unitB ~= "" then return unitB end if type(unitA) == "string" and unitA ~= "" then return unitA end return nil end local function MTH_TT_IsOwnerUnderPetFrame(owner) if not owner then return false end local petFrame = (_G and _G["PetFrame"]) or (type(getglobal) == "function" and getglobal("PetFrame")) local petFrameHappiness = (_G and _G["PetFrameHappiness"]) or (type(getglobal) == "function" and getglobal("PetFrameHappiness")) local cursor = owner local guard = 0 while cursor and guard < 20 do if cursor == petFrame or cursor == petFrameHappiness then return true end if type(cursor.GetParent) == "function" then cursor = cursor:GetParent() else break end guard = guard + 1 end return false end local function MTH_TT_IsMouseOverPetFrame() if type(MouseIsOver) ~= "function" then return false end local petFrame = (_G and _G["PetFrame"]) or (type(getglobal) == "function" and getglobal("PetFrame")) local petFrameHappiness = (_G and _G["PetFrameHappiness"]) or (type(getglobal) == "function" and getglobal("PetFrameHappiness")) if petFrame and MouseIsOver(petFrame) then return true end if petFrameHappiness and MouseIsOver(petFrameHappiness) then return true end return false end local function MTH_TT_IsTooltipOnOwnPet() if type(UnitExists) ~= "function" or type(UnitIsUnit) ~= "function" then return false end if not UnitExists("pet") then return false end local owner = nil local ownerName = "" if GameTooltip and type(GameTooltip.GetOwner) == "function" then owner = GameTooltip:GetOwner() if owner and owner.GetName then ownerName = tostring(owner:GetName() or "") end end if ownerName ~= "" then if string.find(ownerName, "PetActionButton", 1, true) or string.find(ownerName, "PetActionBar", 1, true) or string.find(ownerName, "BT4PetButton", 1, true) then return false end end local token = MTH_TT_GetTooltipUnitToken() if token and token ~= "" then local ok, same = pcall(UnitIsUnit, token, "pet") if ok and same then return true end end if UnitExists("mouseover") then local ok, same = pcall(UnitIsUnit, "mouseover", "pet") if ok and same then return true end end if owner then if MTH_TT_IsOwnerUnderPetFrame(owner) then return true end if ownerName ~= "" and string.find(ownerName, "PetFrame", 1, true) then return true end end if MTH_TT_IsMouseOverPetFrame() then return true end local hasMouseover = UnitExists("mouseover") and true or false if (not hasMouseover) and (token == nil or token == "") then local petName = (type(UnitName) == "function") and tostring(UnitName("pet") or "") or "" if petName ~= "" then local titleRegion = (type(getglobal) == "function" and getglobal("GameTooltipTextLeft1")) or (_G and _G["GameTooltipTextLeft1"]) if titleRegion and type(titleRegion.GetText) == "function" then local titleText = tostring(titleRegion:GetText() or "") if titleText ~= "" and MTH_TT_NormalizeName(titleText) == MTH_TT_NormalizeName(petName) then return true end end end end return false end local function MTH_TT_HasExistingOwnPetTooltipLines() if not GameTooltip or not GameTooltip.NumLines then return false end local function hasMarker(text) text = tostring(text or "") if text == "" then return false end if string.find(text, "Mood :", 1, true) or string.find(text, "Loyalty :", 1, true) or string.find(text, "XP :", 1, true) or string.find(text, "By my side for:", 1, true) or string.find(text, "Has eaten:", 1, true) or string.find(text, "times this session", 1, true) then return true end return false end for i = 1, GameTooltip:NumLines() do local left = getglobal("GameTooltipTextLeft" .. i) if left and left.GetText then if hasMarker(left:GetText()) then return true end end local right = getglobal("GameTooltipTextRight" .. i) if right and right.GetText then if hasMarker(right:GetText()) then return true end end end return false end local function MTH_TT_GetHeatColorByRatio(ratio) ratio = tonumber(ratio) or 0 if ratio < 0 then ratio = 0 end if ratio > 1 then ratio = 1 end local red = 1 - ratio local green = ratio local blue = 0.15 return red, green, blue end local function MTH_TT_GetLoyaltyHeatColor(level) local numeric = tonumber(level) if not numeric then return 0.90, 0.90, 0.90 end if numeric < 1 then numeric = 1 end if numeric > 6 then numeric = 6 end return MTH_TT_GetHeatColorByRatio((numeric - 1) / 5) end local function MTH_TT_FormatPercentCompact(percentValue) local numeric = tonumber(percentValue) if not numeric then return "?" end local rounded = math.floor((numeric * 10) + 0.5) / 10 local integerPart = math.floor(rounded) if math.abs(rounded - integerPart) < 0.001 then return tostring(integerPart) end return string.format("%.1f", rounded) end local function MTH_TT_ColorizeDigitsBlue(text) local source = tostring(text or "") if source == "" then return "" end return string.gsub(source, "(%d+)", "|cff73bfff%1|r") end local function MTH_TT_AddOwnPetTooltipHint() if not (MTH_Tooltips and MTH_Tooltips.enabled) then return end if not MTH_TT_IsOwnPetTooltipsEnabled() then return end if not GameTooltip or not GameTooltip.AddLine then return end if MTH_TT_HasExistingOwnPetTooltipLines() then return end if not MTH_TT_IsTooltipOnOwnPet() then return end if type(MTH_GetCurrentPetInfo) ~= "function" then return end local petInfo = MTH_GetCurrentPetInfo() if type(petInfo) ~= "table" then return end local happinessValue = tonumber(petInfo.happiness) local happinessText = "?" local happinessR, happinessG, happinessB = 0.90, 0.90, 0.90 if type(GetPetHappiness) == "function" then local h = GetPetHappiness() if h ~= nil then happinessValue = tonumber(h) or happinessValue end end if happinessValue == 1 then happinessText = "Unhappy" happinessR, happinessG, happinessB = 1.00, 0.25, 0.25 elseif happinessValue == 2 then happinessText = "Content" happinessR, happinessG, happinessB = 1.00, 0.90, 0.20 elseif happinessValue == 3 then happinessText = "Happy" happinessR, happinessG, happinessB = 0.25, 1.00, 0.25 end if type(GameTooltip.AddDoubleLine) == "function" then GameTooltip:AddDoubleLine("Mood :", tostring(happinessText), 0.90, 0.90, 0.90, happinessR, happinessG, happinessB) else GameTooltip:AddLine("Mood : " .. tostring(happinessText), happinessR, happinessG, happinessB) end local loyaltyDisplay = tostring(petInfo.loyaltyDisplay or "") if loyaltyDisplay == "" then local loyaltyText = tostring(petInfo.loyaltyText or "") if loyaltyText ~= "" then loyaltyDisplay = loyaltyText end end if loyaltyDisplay ~= "" then local loyaltyR, loyaltyG, loyaltyB = MTH_TT_GetLoyaltyHeatColor(petInfo.loyaltyLevel) if type(GameTooltip.AddDoubleLine) == "function" then GameTooltip:AddDoubleLine("Loyalty :", tostring(loyaltyDisplay), 0.90, 0.90, 0.90, loyaltyR, loyaltyG, loyaltyB) else GameTooltip:AddLine("Loyalty : " .. tostring(loyaltyDisplay), loyaltyR, loyaltyG, loyaltyB) end end local petLevel = tonumber(petInfo.level) local xpCur = tonumber(petInfo.xp) local xpTot = tonumber(petInfo.xpMax) if (petLevel == nil or petLevel < 60) and xpCur ~= nil and xpTot ~= nil and xpTot > 0 then local xpPercent = tonumber(petInfo.xpPercent) if xpPercent == nil then xpPercent = math.floor((xpCur / xpTot) * 1000 + 0.5) / 10 end local xpRatio = xpCur / xpTot local xpR, xpG, xpB = MTH_TT_GetHeatColorByRatio(xpRatio) local xpCurInt = math.floor(xpCur + 0.5) local xpTotInt = math.floor(xpTot + 0.5) local xpValue = MTH_TT_FormatPercentCompact(xpPercent) .. "% (" .. tostring(xpCurInt) .. "/" .. tostring(xpTotInt) .. ")" if type(GameTooltip.AddDoubleLine) == "function" then GameTooltip:AddDoubleLine("XP :", xpValue, 0.90, 0.90, 0.90, xpR, xpG, xpB) else GameTooltip:AddLine("XP : " .. xpValue, xpR, xpG, xpB) end end local withMeAt = tonumber(petInfo.withMeSinceAt) or nil local withMeText = MTH_TT_FormatElapsedStableStyle(withMeAt) if withMeText and withMeText ~= "" then local withMeValue = MTH_TT_ColorizeDigitsBlue(tostring(withMeText)) if type(GameTooltip.AddDoubleLine) == "function" then GameTooltip:AddDoubleLine("By my side for:", withMeValue, 0.90, 0.90, 0.90, 0.90, 0.90, 0.90) else GameTooltip:AddLine("By my side for: " .. withMeValue, 0.90, 0.90, 0.90) end end local sessionFeeds = 0 if type(MTH_FEED_GetSessionFeedCount) == "function" then sessionFeeds = tonumber(MTH_FEED_GetSessionFeedCount(petInfo.id)) or 0 end local feedValue = MTH_TT_ColorizeDigitsBlue(tostring(sessionFeeds) .. " times this session") if type(GameTooltip.AddDoubleLine) == "function" then GameTooltip:AddDoubleLine("Has eaten:", feedValue, 0.90, 0.90, 0.90, 0.90, 0.90, 0.90) else GameTooltip:AddLine("Has eaten: " .. feedValue, 0.90, 0.90, 0.90) end GameTooltip:Show() end local function MTH_TT_BuildFoodOpinionLine(itemLink) if type(itemLink) ~= "string" then return nil end local pet = type(MTH_FEED_GetCurrentPetContext) == "function" and MTH_FEED_GetCurrentPetContext() or nil if type(pet) ~= "table" or not pet.liveExists then return nil end local petName = MTH_TT_Trim(pet.name) if petName == "" then return nil end local itemId = MTH_TT_ExtractItemIdFromLink(itemLink) if not itemId then return nil end if type(FOM_IsInDiet) == "function" then local ok, inDiet = pcall(FOM_IsInDiet, itemId) if not ok or not inDiet then return nil end elseif type(MTH_FEED_GetFoodMeta) == "function" then local foodMeta = MTH_FEED_GetFoodMeta(itemId) if type(foodMeta) ~= "table" or not foodMeta.diet then return nil end end local realmPlayer = tostring(FOM_RealmPlayer or "") if realmPlayer == "" and type(GetCVar) == "function" and type(UnitName) == "function" then local realmName = tostring(GetCVar("realmName") or "") local playerName = tostring(UnitName("player") or "") if realmName ~= "" and playerName ~= "" then realmPlayer = realmName .. "." .. playerName end end local absoluteQuality = nil if realmPlayer ~= "" and type(FOM_FoodQuality) == "table" then local realmMap = FOM_FoodQuality[realmPlayer] if type(realmMap) == "table" then local petMap = realmMap[petName] if type(petMap) == "table" then absoluteQuality = petMap[itemId] end end end if absoluteQuality == nil then local text = string.format(MTH_TT_Loc("TOOLTIPS_FOOD_QUALITY_UNKNOWN", "%s can eat this, but hasn't tried it yet."), petName) local color = HIGHLIGHT_FONT_COLOR if color and color.r and color.g and color.b then return text, color.r, color.g, color.b end return text, 1.0, 0.82, 0.0 end local petLevel = tonumber(pet.level) or 0 if petLevel <= 0 then return nil end local currentQuality = tonumber(absoluteQuality) / petLevel if currentQuality < 0 then local r, g, b = MTH_TT_GetQuestColor("trivial", 0.5, 0.5, 0.5) return string.format(MTH_TT_Loc("TOOLTIPS_FOOD_QUALITY_UNDER", "%s doesn't like this anymore."), petName), r, g, b elseif currentQuality == 0 then local r, g, b = MTH_TT_GetQuestColor("trivial", 0.5, 0.5, 0.5) return string.format(MTH_TT_Loc("TOOLTIPS_FOOD_QUALITY_MIGHT", "%s might eat this."), petName), r, g, b elseif currentQuality <= 8 then local r, g, b = MTH_TT_GetQuestColor("standard", 1.0, 1.0, 0.0) return string.format(MTH_TT_Loc("TOOLTIPS_FOOD_QUALITY_WILL", "%s will eat this."), petName), r, g, b elseif currentQuality <= 17 then local r, g, b = MTH_TT_GetQuestColor("difficult", 1.0, 0.5, 0.0) return string.format(MTH_TT_Loc("TOOLTIPS_FOOD_QUALITY_LIKE", "%s likes to eat this."), petName), r, g, b elseif currentQuality <= 35 then local r, g, b = MTH_TT_GetQuestColor("verydifficult", 1.0, 0.1, 0.1) return string.format(MTH_TT_Loc("TOOLTIPS_FOOD_QUALITY_LOVE", "%s loves to eat this."), petName), r, g, b end return nil end local function MTH_TT_AddFoodTooltipHint() if not (MTH_Tooltips and MTH_Tooltips.enabled) then return end if not MTH_TT_IsFoodItemTooltipsEnabled() then return end if not GameTooltip or not GameTooltip.GetItem or not GameTooltip.AddLine then return end if MTH_TT_HasExistingFoodTooltipLine() then return end local _, itemLink = GameTooltip:GetItem() if not itemLink then return end local text, r, g, b = MTH_TT_BuildFoodOpinionLine(itemLink) if text and text ~= "" then GameTooltip:AddLine(text, r or 1.0, g or 1.0, b or 1.0) GameTooltip:Show() end end local function MTH_TT_HasExistingNotLearnedLine() if not GameTooltip or not GameTooltip.NumLines then return false end for i = 1, GameTooltip:NumLines() do local fontString = getglobal("GameTooltipTextLeft" .. i) if fontString and fontString.GetText then local text = fontString:GetText() if text and string.find(text, "Not learned yet, keep going !", 1, true) then return true end end end return false end local function MTH_TT_GetKnownSpellMap() if type(MTH_GetKnownPetSpellMap) == "function" then return MTH_GetKnownPetSpellMap() end if not MTH_CharSavedVariables or not MTH_CharSavedVariables.petTraining then return nil end return MTH_CharSavedVariables.petTraining.spellMap end local function MTH_TT_ParseAbilityToken(token) local text = MTH_TT_Trim(token) if text == "" then return "", "", nil end local _, _, abilityName, rankText = string.find(text, "^(.-)%s+(%d+)$") if abilityName and abilityName ~= "" then abilityName = MTH_TT_Trim(abilityName) return abilityName, MTH_TT_Lower(abilityName), tonumber(rankText) end return text, MTH_TT_Lower(text), nil end local function MTH_TT_HunterKnowsAbility(abilityLower, rankNumber) if abilityLower == "" then return false end local spellMap = MTH_TT_GetKnownSpellMap() if type(spellMap) ~= "table" then return false end for _, row in pairs(spellMap) do if row and row.name and row.isKnown ~= false then if MTH_TT_Lower(row.name) == abilityLower then if rankNumber then if tonumber(row.rankNumber) == tonumber(rankNumber) then return true end else return true end end end end return false end local function MTH_TT_FindAbilityCanonicalName(abilityName) local name = MTH_TT_Trim(abilityName) if name == "" then return nil end if not (MTH_DS_PetSpells and type(MTH_DS_PetSpells.byAbility) == "table") then return nil end if MTH_DS_PetSpells.byAbility[name] then return name end local key = MTH_TT_Lower(name) for abilityKey in pairs(MTH_DS_PetSpells.byAbility) do if MTH_TT_Lower(abilityKey) == key then return abilityKey end end return nil end local function MTH_TT_ParseRankFromText(value) local text = MTH_TT_Trim(value) if text == "" then return nil end local _, _, rankText = string.find(text, "[Rr]ank%s*(%d+)") if rankText then return tonumber(rankText) end local _, _, trailing = string.find(text, "(%d+)$") if trailing then return tonumber(trailing) end return nil end local function MTH_TT_CleanSpellName(value) local text = MTH_TT_Trim(value) if text == "" then return "" end text = string.gsub(text, "%s*%([Rr]ank%s*%d+%)", "") text = string.gsub(text, "%s+[Rr]ank%s*%d+$", "") text = string.gsub(text, "%s+%d+$", "") text = MTH_TT_Trim(text) return text end local function MTH_TT_GetTooltipSpellNameAndRank() local spellName = nil local rankNumber = nil if GameTooltip and type(GameTooltip.GetSpell) == "function" then local ok, a, b = pcall(function() return GameTooltip:GetSpell() end) if ok then if type(a) == "string" then spellName = a rankNumber = MTH_TT_ParseRankFromText(b) elseif type(b) == "string" then spellName = b rankNumber = MTH_TT_ParseRankFromText(a) end end end if not spellName or spellName == "" then local left1 = getglobal("GameTooltipTextLeft1") if left1 and left1.GetText then spellName = MTH_TT_Trim(left1:GetText()) end end if not rankNumber then local right1 = getglobal("GameTooltipTextRight1") if right1 and right1.GetText then rankNumber = MTH_TT_ParseRankFromText(right1:GetText()) end end if not rankNumber then local left2 = getglobal("GameTooltipTextLeft2") if left2 and left2.GetText then rankNumber = MTH_TT_ParseRankFromText(left2:GetText()) end end if not rankNumber then local right2 = getglobal("GameTooltipTextRight2") if right2 and right2.GetText then rankNumber = MTH_TT_ParseRankFromText(right2:GetText()) end end if (not rankNumber) and GameTooltip and GameTooltip.NumLines then for i = 2, GameTooltip:NumLines() do local left = getglobal("GameTooltipTextLeft" .. i) if left and left.GetText then local text = MTH_TT_Trim(left:GetText()) local parsed = MTH_TT_ParseRankFromText(text) if parsed and parsed > 0 then rankNumber = parsed break end end local right = getglobal("GameTooltipTextRight" .. i) if right and right.GetText then local parsedRight = MTH_TT_ParseRankFromText(right:GetText()) if parsedRight and parsedRight > 0 then rankNumber = parsedRight break end end end end return MTH_TT_CleanSpellName(spellName), rankNumber end local function MTH_TT_IsPetActionTooltipOwner() if not GameTooltip or type(GameTooltip.GetOwner) ~= "function" then return false end local owner = GameTooltip:GetOwner() if not owner then return false end local ownerName = owner.GetName and owner:GetName() or "" ownerName = tostring(ownerName or "") if ownerName ~= "" and ( string.find(ownerName, "PetActionButton", 1, true) or string.find(ownerName, "PetActionBar", 1, true) or string.find(ownerName, "BT4PetButton", 1, true) ) then return true end return false end local function MTH_TT_IsSpellOnPetActionBar(spellName) local wanted = MTH_TT_Lower(MTH_TT_CleanSpellName(spellName)) if wanted == "" then return false end if type(GetPetActionInfo) ~= "function" then return false end for slot = 1, 12 do local name = GetPetActionInfo(slot) if type(name) == "string" and name ~= "" then local normalized = MTH_TT_Lower(MTH_TT_CleanSpellName(name)) if normalized == wanted then return true end end end return false end local function MTH_TT_GetTooltipOwnerName() if not GameTooltip or type(GameTooltip.GetOwner) ~= "function" then return "" end local owner = GameTooltip:GetOwner() if not owner then return "" end if owner.GetName then local name = tostring(owner:GetName() or "") if name ~= "" then return name end end return "" end local function MTH_TT_AddPetActionNotLearnedHint() if not (MTH_Tooltips and MTH_Tooltips.enabled) then return end if not GameTooltip or not GameTooltip.AddLine then return end if MTH_TT_HasExistingNotLearnedLine() then return end local spellName, rankNumber = MTH_TT_GetTooltipSpellNameAndRank() if not spellName or spellName == "" then return end local ownerName = MTH_TT_GetTooltipOwnerName() local ownerMatch = MTH_TT_IsPetActionTooltipOwner() local barMatch = MTH_TT_IsSpellOnPetActionBar(spellName) if not ownerMatch and not barMatch then return end local canonical = MTH_TT_FindAbilityCanonicalName(spellName) if not canonical then return end local known = MTH_TT_HunterKnowsAbility(MTH_TT_Lower(canonical), rankNumber) if known then return end GameTooltip:AddLine("Not learned yet, keep going ! ", 1.0, 0.25, 0.25) GameTooltip:Show() end local function MTH_TT_ParseAbilities(abilitiesText) local text = MTH_TT_Trim(abilitiesText) local parsed = {} if text == "" or text == "None" then return parsed end local startPos = 1 local len = string.len(text) while startPos <= len do local commaPos = string.find(text, ",", startPos, true) local token if commaPos then token = string.sub(text, startPos, commaPos - 1) startPos = commaPos + 1 else token = string.sub(text, startPos) startPos = len + 1 end token = MTH_TT_Trim(token) if token ~= "" then local _, abilityLower, rankNumber = MTH_TT_ParseAbilityToken(token) local known = MTH_TT_HunterKnowsAbility(abilityLower, rankNumber) table.insert(parsed, { text = token, known = known }) end end return parsed end local function MTH_TT_AddTooltip(unit) local state = MTH_Tooltips.debugState state.injectAttempts = (state.injectAttempts or 0) + 1 if not UnitExists(unit) then state.skipNoUnit = (state.skipNoUnit or 0) + 1 MTH_TT_StateSet("skip-no-unit") return end if UnitPlayerControlled(unit) then state.skipPlayerControlled = (state.skipPlayerControlled or 0) + 1 MTH_TT_StateSet("skip-player-controlled") return end if not GameTooltip or not GameTooltip.AddLine then state.skipTooltipApi = (state.skipTooltipApi or 0) + 1 MTH_TT_StateSet("skip-tooltip-api") MTH_TT_Log("skip: GameTooltip API unavailable") return end local name = UnitName(unit) if not name or name == "" then state.skipNoName = (state.skipNoName or 0) + 1 MTH_TT_StateSet("skip-no-name") MTH_TT_Log("skip: mouseover has no unit name") return end local normalized = MTH_TT_NormalizeName(name) MTH_TT_StateSet("lookup", name, normalized) local beastTooltipsEnabled = MTH_TT_IsBeastTooltipsEnabled() local ammoVendorTooltipsEnabled = MTH_TT_IsAmmoVendorTooltipsEnabled() local vendorInfo = ammoVendorTooltipsEnabled and MTH_TT_FindVendorInfo(name) or nil local row = beastTooltipsEnabled and MTH_TT_FindBeastRow(name) or nil if not row and not vendorInfo then state.lookupMiss = (state.lookupMiss or 0) + 1 MTH_TT_StateSet("lookup-miss", name, normalized) MTH_TT_Log("lookup miss: unit='" .. tostring(name) .. "' normalized='" .. tostring(normalized) .. "'") return end local addedAnything = nil if vendorInfo and (vendorInfo.arrows or vendorInfo.bullets) and not MTH_TT_HasExistingVendorTooltipLine() then if vendorInfo.arrows and vendorInfo.bullets then GameTooltip:AddLine("Sells Arrows and Bullets", 0.45, 0.90, 1.00) elseif vendorInfo.arrows then GameTooltip:AddLine("Sells Arrows", 0.45, 0.90, 1.00) elseif vendorInfo.bullets then GameTooltip:AddLine("Sells Bullets", 0.45, 0.90, 1.00) end addedAnything = 1 MTH_TT_Log("vendor tooltip added: unit='" .. tostring(name) .. "'") end if row then local abilities = MTH_TT_ParseAbilities(row.abilities) if table.getn(abilities) == 0 then MTH_TT_StateSet("skip-no-abilities", name, normalized) MTH_TT_Log("skip: beast has no abilities ('" .. tostring(name) .. "')") elseif MTH_TT_HasExistingTooltipLine() then state.skipExistingLine = (state.skipExistingLine or 0) + 1 MTH_TT_StateSet("skip-existing-line", name, normalized) MTH_TT_Log("skip: tooltip line already present for '" .. tostring(name) .. "'") else GameTooltip:AddLine("Pet Abilities:", 1.0, 0.95, 0.45) for i = 1, table.getn(abilities) do local entry = abilities[i] if entry and entry.known then GameTooltip:AddLine(" " .. tostring(entry.text or ""), 0.2, 1.0, 0.2) else GameTooltip:AddLine(" " .. tostring(entry and entry.text or ""), 1.0, 0.25, 0.25) end end addedAnything = 1 MTH_TT_Log("beast tooltip added: unit='" .. tostring(name) .. "' entries='" .. tostring(table.getn(abilities)) .. "'") end end if addedAnything then GameTooltip:Show() state.added = (state.added or 0) + 1 MTH_TT_StateSet("added", name, normalized) end end local function MTH_TT_EnsureInjectFrame() if MTH_Tooltips.injectFrame then return MTH_Tooltips.injectFrame end local frame = CreateFrame("Frame", "MTH_TooltipsInjectFrame") frame.pending = false frame:SetScript("OnUpdate", function(self) self = self or this if not self or not self.pending then return end self.pending = false if MTH_Tooltips and MTH_Tooltips.enabled then MTH_TT_AddTooltip("mouseover") end end) MTH_Tooltips.injectFrame = frame return frame end local function MTH_TT_RequestInject() local frame = MTH_TT_EnsureInjectFrame() if frame then frame.pending = true end end local function MTH_TT_EnsurePollFrame() if MTH_Tooltips.pollFrame then return MTH_Tooltips.pollFrame end local frame = CreateFrame("Frame", "MTH_TooltipsPollFrame") frame:SetScript("OnUpdate", function(_, elapsed) if not (MTH_Tooltips and MTH_Tooltips.enabled) then return end if GameTooltip and GameTooltip.IsShown and GameTooltip:IsShown() then MTH_TT_AddOwnPetTooltipHint() if not MTH_TT_HasExistingNotLearnedLine() then MTH_TT_AddPetActionNotLearnedHint() end end elapsed = elapsed or arg1 MTH_Tooltips.pollElapsed = (MTH_Tooltips.pollElapsed or 0) + (elapsed or 0) if MTH_Tooltips.pollElapsed < 0.15 then return end MTH_Tooltips.pollElapsed = 0 local state = MTH_Tooltips.debugState state.pollTicks = (state.pollTicks or 0) + 1 if not UnitExists("mouseover") then MTH_Tooltips.lastMouseoverKey = "" return end local name = UnitName("mouseover") if not name or name == "" then return end local lvl = UnitLevel and UnitLevel("mouseover") or 0 local key = tostring(name) .. "#" .. tostring(lvl) if key ~= MTH_Tooltips.lastMouseoverKey then MTH_Tooltips.lastMouseoverKey = key state.pollTriggers = (state.pollTriggers or 0) + 1 MTH_TT_RequestInject() return end if not MTH_TT_HasExistingTooltipLine() then MTH_TT_RequestInject() end end) MTH_Tooltips.pollFrame = frame return frame end local function MTH_TT_InstallTooltipHook() if MTH_Tooltips.hookInstalled then return end if not GameTooltip then MTH_TT_Log("hook skipped: GameTooltip missing") return end if type(GameTooltip.HookScript) == "function" then local ok, err = pcall(function() GameTooltip:HookScript("OnTooltipSetUnit", function() if not (MTH_Tooltips and MTH_Tooltips.enabled) then return end MTH_TT_AddOwnPetTooltipHint() MTH_TT_RequestInject() end) GameTooltip:HookScript("OnTooltipSetItem", function() MTH_TT_AddFoodTooltipHint() end) GameTooltip:HookScript("OnTooltipSetSpell", function() MTH_TT_AddPetActionNotLearnedHint() end) end) if ok then MTH_Tooltips.hookInstalled = true MTH_TT_Log("hook installed: GameTooltip OnTooltipSetUnit+OnTooltipSetItem+OnTooltipSetSpell") else MTH_TT_Log("hook skipped: OnTooltipSetUnit unavailable (" .. tostring(err or "unknown") .. ")") end else MTH_TT_Log("hook skipped: HookScript API unavailable") end end function MTH_Tooltips:init() self.beastIndex = MTH_TT_BuildIndex() self.initialized = true MTH_TT_InstallTooltipHook() MTH_TT_EnsureInjectFrame() MTH_TT_EnsurePollFrame() MTH_TT_Log("module init complete") end function MTH_Tooltips:onEvent(evt, arg1) if evt ~= "UPDATE_MOUSEOVER_UNIT" and evt ~= "UNIT_NAME_UPDATE" then return end if not self.enabled then return end if evt == "UNIT_NAME_UPDATE" and arg1 ~= "mouseover" then return end self.debugState.events = (self.debugState.events or 0) + 1 MTH_TT_Log("event: UPDATE_MOUSEOVER_UNIT") if not self.initialized then self:init() end MTH_TT_RequestInject() end function MTH_Tooltips:setEnabled(enabled) self.enabled = enabled and true or false MTH_TT_Log("setEnabled: " .. tostring(self.enabled)) if self.enabled and not self.initialized then self:init() elseif self.enabled then MTH_TT_EnsurePollFrame() MTH_TT_EnsureInjectFrame() end end function MTH_Tooltips:SetBeastTooltipsEnabled(enabled) local store = MTH_TT_GetOptionsStore() if type(store) == "table" then store.beastTooltips = enabled and true or false end end function MTH_Tooltips:SetAmmoVendorTooltipsEnabled(enabled) local store = MTH_TT_GetOptionsStore() if type(store) == "table" then store.ammoVendorTooltips = enabled and true or false end self.vendorIndex = nil end function MTH_Tooltips:SetFoodItemTooltipsEnabled(enabled) local store = MTH_TT_GetOptionsStore() if type(store) == "table" then store.foodItemTooltips = enabled and true or false end end function MTH_Tooltips:SetOwnPetTooltipsEnabled(enabled) local store = MTH_TT_GetOptionsStore() if type(store) == "table" then store.ownPetTooltips = enabled and true or false end end function MTH_Tooltips:GetBeastTooltipsEnabled() return MTH_TT_IsBeastTooltipsEnabled() end function MTH_Tooltips:GetAmmoVendorTooltipsEnabled() return MTH_TT_IsAmmoVendorTooltipsEnabled() end function MTH_Tooltips:GetFoodItemTooltipsEnabled() return MTH_TT_IsFoodItemTooltipsEnabled() end function MTH_Tooltips:GetOwnPetTooltipsEnabled() return MTH_TT_IsOwnPetTooltipsEnabled() end function MTH_Tooltips:cleanup() self.enabled = false end function MTH_Tooltips:GetDebugSnapshot() return self.debugState end MTH:RegisterModule("tooltips", MTH_Tooltips)