Files
MetaHunt/api/hunterbook-tab-beastlore.lua
2026-03-23 22:25:10 +01:00

483 lines
15 KiB
Lua

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