if type(MTH_HUNTERBOOK_TABS) ~= "table" then MTH_HUNTERBOOK_TABS = {} end MTH_HUNTERBOOK_TABS.pets = { headerLabel = "Beasts", columnLabels = { "ID", "Lvl", "Fam", "Name", "Abilities", "Zone", "R", "E", "U", "AS" }, columnLayout = { { x = 8, width = 28, align = "LEFT" }, { x = 38, width = 30, align = "LEFT" }, { x = 68, width = 18, align = "LEFT" }, { x = 88, width = 130, align = "LEFT" }, { x = 220, width = 148, align = "LEFT" }, { x = 370, width = 80, align = "LEFT" }, { x = 452, width = 14, align = "CENTER" }, { x = 468, width = 14, align = "CENTER" }, { x = 484, width = 14, align = "CENTER" }, { x = 500, width = 54, align = "LEFT" }, }, } function MTH_BOOK_GetBeastAbilitiesSummary(beast) if not beast or not beast.abilities or beast.abilities == "" or beast.abilities == "None" then return "-" end return tostring(beast.abilities) end function MTH_BOOK_GetBeastZoneSummary(beast) if not beast or not beast.coords or table.getn(beast.coords) == 0 then return "-" end -- Count coords per WMA zone ID to find the dominant zone. local coordCount = {} for i = 1, table.getn(beast.coords) do local c = beast.coords[i] if c and c[3] then local zid = c[3] coordCount[zid] = (coordCount[zid] or 0) + 1 end end local zids = {} for zid, _ in pairs(coordCount) do table.insert(zids, zid) end -- Sort by frequency descending so the zone with most spawns is listed first. table.sort(zids, function(a, b) return (coordCount[a] or 0) > (coordCount[b] or 0) end) -- Resolve to zone names, deduplicating by name (multiple WMA IDs can resolve to the same zone). local seenNames = {} local zoneNames = {} for i = 1, table.getn(zids) do local name = MTH_BOOK_GetZoneName(zids[i]) if name and not seenNames[name] then seenNames[name] = true table.insert(zoneNames, name) end end if table.getn(zoneNames) == 0 then return "-" end if table.getn(zoneNames) == 1 then return zoneNames[1] end return zoneNames[1] .. " +" .. tostring(table.getn(zoneNames) - 1) end function MTH_BOOK_SplitAbilities(text) local out = {} if not text or text == "" or text == "None" then return out end local startPos = 1 local textLen = string.len(text) while startPos <= textLen 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 = textLen + 1 end token = string.gsub(token, "^%s+", "") token = string.gsub(token, "%s+$", "") if token ~= "" then table.insert(out, token) end end return out end local _subzoneChildCache = nil local function MTH_BOOK_EnsureSubzoneCache() if _subzoneChildCache then return end _subzoneChildCache = {} if not MTH_DS_Zones then return end for zid, row in pairs(MTH_DS_Zones) do local parent = row.parent local x1 = row.x1 local y1 = row.y1 local w = row.width local h = row.height if parent and x1 and y1 and w and h then local names = row.names local name = names and (names.enUS or names[GetLocale and GetLocale() or "enUS"] or names.deDE) if name then if not _subzoneChildCache[parent] then _subzoneChildCache[parent] = {} end table.insert(_subzoneChildCache[parent], { x1, y1, x1 + w, y1 + h, name }) end end end end function MTH_BOOK_GetBeastSubZoneSummary(beast) if not beast or not beast.coords or table.getn(beast.coords) == 0 then return nil end MTH_BOOK_EnsureSubzoneCache() if not _subzoneChildCache then return nil end local counts = {} for i = 1, table.getn(beast.coords) do local c = beast.coords[i] if c then local cx, cy, wmaId = c[1], c[2], c[3] if cx and cy and wmaId then local pfqId = wmaId if type(MTH_MAP_WMA_TO_PFQ) == "table" then pfqId = MTH_MAP_WMA_TO_PFQ[wmaId] or wmaId end local children = _subzoneChildCache[pfqId] if children then for _, sub in ipairs(children) do if cx >= sub[1] and cx <= sub[3] and cy >= sub[2] and cy <= sub[4] then counts[sub[5]] = (counts[sub[5]] or 0) + 1 break end end end end end end local bestName, bestCount = nil, 0 for name, count in pairs(counts) do if count > bestCount then bestName = name bestCount = count end end return bestName end function MTH_BOOK_HasUsableAbilities(beast) if not beast then return false end local abilities = MTH_BOOK_SplitAbilities(beast.abilities) if table.getn(abilities) == 0 then return false end for i = 1, table.getn(abilities) do local token = MTH_BOOK_SafeLower(abilities[i]) if token ~= "" and token ~= "none" and token ~= "unknown" then return true end end return false end function MTH_BOOK_HasKnownLevel(beast) if not beast then return false end if MTH_BOOK_IsUnknownText(beast.lvl) then return false end return MTH_BOOK_ParseLevel(beast.lvl) ~= nil end function MTH_BOOK_HasKnownZone(beast) if not beast or not beast.coords or table.getn(beast.coords) == 0 then return false end for i = 1, table.getn(beast.coords) do local c = beast.coords[i] if c and c[3] then local zoneName = MTH_BOOK_SafeLower(MTH_BOOK_GetZoneName(c[3])) if zoneName ~= "" and zoneName ~= "unknown" and string.sub(zoneName, 1, 5) ~= "zone " then return true end end end return false end function MTH_BOOK_ParseAbilityToken(token) local text = tostring(token or "") text = string.gsub(text, "^%s+", "") text = string.gsub(text, "%s+$", "") if text == "" then return "", "", nil end local _, _, ability, rank = string.find(text, "^(.-)%s*(%d+)$") if ability and ability ~= "" then ability = string.gsub(ability, "^%s+", "") ability = string.gsub(ability, "%s+$", "") if ability ~= "" then return MTH_BOOK_SafeLower(ability), ability, tonumber(rank) end end return MTH_BOOK_SafeLower(text), text, nil end function MTH_BOOK_BeastHasAbilityRank(beast, abilityValue, rankValue) if not beast then return false end local wantedAbility = MTH_BOOK_SafeLower(abilityValue) if wantedAbility == "" or wantedAbility == "all" then return true end local wantedRank = tonumber(rankValue) local abilities = MTH_BOOK_SplitAbilities(beast.abilities) for i = 1, table.getn(abilities) do local abilityLower, _, rank = MTH_BOOK_ParseAbilityToken(abilities[i]) if abilityLower == wantedAbility then if not wantedRank then return true end if rank and rank == wantedRank then return true end end end return false end function MTH_BOOK_ParseBeastTraits(beast) local traits = { rare = beast and beast.rare and true or false, elite = beast and (beast.elite == true or beast.isElite == true) or false, unique = beast and (beast.unique == true or beast.isUnique == true) or false, } if not beast then return traits end local rank = MTH_BOOK_SafeLower(beast.rank) if rank ~= "" then if string.find(rank, "rare", 1, true) then traits.rare = true end if string.find(rank, "elite", 1, true) then traits.elite = true end if string.find(rank, "unique", 1, true) then traits.unique = true end end local cls = MTH_BOOK_SafeLower(beast.classification) if cls ~= "" then if string.find(cls, "rare", 1, true) then traits.rare = true end if string.find(cls, "elite", 1, true) then traits.elite = true end if string.find(cls, "unique", 1, true) then traits.unique = true end end local name = MTH_BOOK_SafeLower(beast.name) if name ~= "" then local scanPos = 1 while true do local startPos, endPos = string.find(name, "%b()", scanPos) if not startPos then break end local token = string.sub(name, startPos, endPos) scanPos = endPos + 1 local inner = string.sub(token, 2, -2) inner = string.gsub(inner, "^%s+", "") inner = string.gsub(inner, "%s+$", "") local normalized = string.gsub(inner, "[%s%-_/]", "") if inner == "rare" or normalized == "r" then traits.rare = true end if inner == "elite" or normalized == "e" then traits.elite = true end if inner == "unique" or normalized == "u" then traits.unique = true end if string.find(inner, "rare", 1, true) then traits.rare = true end if string.find(inner, "elite", 1, true) then traits.elite = true end if string.find(inner, "unique", 1, true) then traits.unique = true end if normalized ~= "" and string.find(normalized, "[^reu]", 1) == nil then if string.find(normalized, "r", 1, true) then traits.rare = true end if string.find(normalized, "e", 1, true) then traits.elite = true end if string.find(normalized, "u", 1, true) then traits.unique = true end end end end return traits end function MTH_BOOK_BeastMatches(beastId, beast) if not beast then return false end local forcedBeastId = MTH_BOOK_STATE and tonumber(MTH_BOOK_STATE.forcedBeastId) or nil if forcedBeastId and tonumber(beastId) ~= forcedBeastId then return false end local beastLevel = MTH_BOOK_ParseLevel(beast.lvl) local traits = MTH_BOOK_ParseBeastTraits(beast) local familyLower = MTH_BOOK_SafeLower(beast.family or "") if MTH_BOOK_STATE.minLevel and (not beastLevel or beastLevel < MTH_BOOK_STATE.minLevel) then return false end if MTH_BOOK_STATE.maxLevel and (not beastLevel or beastLevel > MTH_BOOK_STATE.maxLevel) then return false end if MTH_BOOK_STATE.flag1 and not traits.rare then return false end if MTH_BOOK_STATE.flag2 and not traits.elite then return false end if MTH_BOOK_STATE.flag3 and not traits.unique then return false end if not MTH_BOOK_BeastHasAbilityRank(beast, MTH_BOOK_STATE.petAbility, MTH_BOOK_STATE.petRank) then return false end if MTH_BOOK_STATE.petHideNoAbilities and not MTH_BOOK_HasUsableAbilities(beast) then return false end if MTH_BOOK_STATE.petHideUnknown then if familyLower == "unknown" then return false end if not MTH_BOOK_HasKnownLevel(beast) then return false end if not MTH_BOOK_HasKnownZone(beast) then return false end end if MTH_BOOK_STATE.quick ~= "all" then if familyLower ~= MTH_BOOK_STATE.quick then return false end end if MTH_BOOK_STATE.petInZoneOnly then -- Use the zone ID cached once per BuildResults call (avoids N calls to -- GetCurrentZoneId which is expensive). If zone ID is nil (new/unknown -- zone like Moonwhisper Coast), no DB entry can match — exclude all. local currentZoneId = MTH_BOOK_STATE._cachedZoneId if not currentZoneId or not MTH_BOOK_BeastHasZoneId(beast, currentZoneId) then return false end end if MTH_BOOK_STATE.search ~= "" then local localizedName = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(beastId, beast.name)) or beast.name local name = MTH_BOOK_SafeLower(localizedName) local family = MTH_BOOK_SafeLower(beast.family) local idText = tostring(beastId) if string.find(name, MTH_BOOK_STATE.search, 1, true) == nil and string.find(family, MTH_BOOK_STATE.search, 1, true) == nil and string.find(idText, MTH_BOOK_STATE.search, 1, true) == nil then return false end end if MTH_BOOK_STATE.petZoneFilter and MTH_BOOK_STATE.petZoneFilter ~= "all" then if not MTH_BOOK_BeastHasZoneId(beast, MTH_BOOK_STATE.petZoneFilter) then return false end end return true end function MTH_BOOK_BeastSort(a, b) local beasts = MTH_DS_Beasts local ba = beasts and beasts[a] local bb = beasts and beasts[b] if not ba or not bb then return tostring(a) < tostring(b) end local la = MTH_BOOK_ParseLevel(ba.lvl) or 0 local lb = MTH_BOOK_ParseLevel(bb.lvl) or 0 if la ~= lb then return la < lb end local na = (MTH and MTH.GetLocalizedBeastName) and MTH:GetLocalizedBeastName(a, ba.name) or (ba.name or "") local nb = (MTH and MTH.GetLocalizedBeastName) and MTH:GetLocalizedBeastName(b, bb.name) or (bb.name or "") if na ~= nb then return na < nb end return a < b end local function MTH_BOOKTAB_BeastLoreTrim(value) local text = tostring(value or "") text = string.gsub(text, "^%s+", "") text = string.gsub(text, "%s+$", "") return text end local function MTH_BOOKTAB_BeastLoreFindAbilityBundle(abilityName) if not (MTH_DS_PetSpells and type(MTH_DS_PetSpells.byAbility) == "table") then return nil end local direct = MTH_DS_PetSpells.byAbility[abilityName] if type(direct) == "table" then return direct end local wanted = MTH_BOOK_SafeLower(abilityName) for key, bundle in pairs(MTH_DS_PetSpells.byAbility) do if MTH_BOOK_SafeLower(key) == wanted then return bundle end end return nil end function MTH_BOOKTAB_BuildBeastAbilityEntries(beast) local entries = {} if type(beast) ~= "table" then return entries end local tokens = MTH_BOOK_SplitAbilities(beast.abilities) local seen = {} for i = 1, table.getn(tokens) do local rawToken = MTH_BOOKTAB_BeastLoreTrim(tokens[i]) if rawToken ~= "" then local tokenKey = MTH_BOOK_SafeLower(rawToken) if tokenKey ~= "" and not seen[tokenKey] then seen[tokenKey] = true local _, abilityLabel, requestedRank = MTH_BOOK_ParseAbilityToken(rawToken) local bundle = MTH_BOOKTAB_BeastLoreFindAbilityBundle(abilityLabel) local canonical = abilityLabel if type(bundle) == "table" and type(bundle.ability) == "string" and bundle.ability ~= "" then canonical = bundle.ability end local spellRows = (type(bundle) == "table" and type(bundle.spells) == "table") and bundle.spells or {} local preferredSpell = nil local firstBeastSpell = nil local rankMin = nil local rankMax = nil local rankMap = {} for s = 1, table.getn(spellRows) do local spell = spellRows[s] if type(spell) == "table" and MTH_BOOK_SafeLower(spell.learnMethod or "beast") ~= "trainer" then if not firstBeastSpell then firstBeastSpell = spell end local rank = tonumber(spell.rankNumber) if rank and rank > 0 then rankMap[rank] = true if not rankMin or rank < rankMin then rankMin = rank end if not rankMax or rank > rankMax then rankMax = rank end if requestedRank and requestedRank == rank then preferredSpell = spell end end end end local rankCount = 0 for _ in pairs(rankMap) do rankCount = rankCount + 1 end if not preferredSpell then preferredSpell = firstBeastSpell end local displayName = canonical if requestedRank then displayName = canonical .. " " .. tostring(requestedRank) end table.insert(entries, { name = displayName, tooltipName = canonical, requestedRank = requestedRank, icon = preferredSpell and preferredSpell.icon or nil, description = preferredSpell and preferredSpell.description or "", rankMin = rankMin, rankMax = rankMax, rankCount = rankCount, }) end end end return entries end function MTH_BOOKTAB_ShowBeastAbilityTooltip(anchor, abilityEntry) if not anchor or type(abilityEntry) ~= "table" then return end local tooltip = MTH_BOOK_GetSpellTooltip and MTH_BOOK_GetSpellTooltip() or GameTooltip if not tooltip then return end tooltip:SetOwner(anchor, "ANCHOR_RIGHT") tooltip:ClearLines() if MTH_BOOK_SetSpellTooltipIcon then MTH_BOOK_SetSpellTooltipIcon(tooltip, abilityEntry.icon) end local title = tostring(abilityEntry.tooltipName or abilityEntry.name or "Unknown") if abilityEntry.requestedRank then title = title .. " " .. tostring(abilityEntry.requestedRank) end tooltip:AddLine(title, 1.00, 0.82, 0.00) if abilityEntry.description and abilityEntry.description ~= "" then tooltip:AddLine(" ") tooltip:AddLine(tostring(abilityEntry.description), 0.90, 0.90, 0.90, true) end tooltip:Show() end