Files
MetaHunt/api/hunterbook-tab-npcfinder.lua
T
DuvelCorp acac6ace6f NPC Finder: fix zone-ID mislabel and show all zones per NPC
- Translate the coord's WMA id to pfQuest before the MTH_DS_Zones parent
  lookup, so e.g. Feralas (WMA 121) no longer collides with pfQ 121
  (Raven Hill) and prints its parent 'Duskwood'
- Zone column now lists every distinct zone an NPC occupies, smallest-map
  first, so border/capital NPCs read 'Darnassus, Teldrassil' etc. (sort
  key matches)
2026-09-14 18:38:10 +02:00

240 lines
7.8 KiB
Lua

if type(MTH_HUNTERBOOK_TABS) ~= "table" then MTH_HUNTERBOOK_TABS = {} end
MTH_HUNTERBOOK_TABS.npcs = {
headerLabel = "NPC Finder",
columnLabels = { "ID", "Name", "React", "Function", "Zone" },
columnLayout = {
{ x = 8, width = 52, align = "LEFT" },
{ x = 60, width = 140, align = "LEFT" },
{ x = 200, width = 40, align = "LEFT" },
{ x = 242, width = 160, align = "LEFT" },
{ x = 404, width = 148, align = "LEFT" },
},
}
function MTH_BOOKTAB_GetNPCReactBucket(fac)
local f = tostring(fac or "")
if f == "A" then return "A" end
if f == "H" then return "H" end
return "AH"
end
MTH_BOOK_GetNPCReactBucket = MTH_BOOKTAB_GetNPCReactBucket
function MTH_BOOKTAB_GetNPCFunctionLabel(key)
local map = {
vendor = "Ammo Vendor",
repair = "Repair",
stablemaster = "Stable Master",
huntertrainer = "Hunter Trainer",
pettrainer = "Pet Trainer",
}
return map[key] or (string.upper(string.sub(key, 1, 1)) .. string.sub(key, 2))
end
MTH_BOOK_GetNPCFunctionLabel = MTH_BOOKTAB_GetNPCFunctionLabel
function MTH_BOOKTAB_GetNPCFunctions(vendor)
local out = {}
local seen = {}
local meta = vendor and vendor.meta
if meta then
for key, value in pairs(meta) do
if type(key) == "string" and value and tostring(value) ~= "" then
local normalizedKey = MTH_BOOK_SafeLower(key)
if normalizedKey ~= "" and not seen[normalizedKey] then
seen[normalizedKey] = true
table.insert(out, normalizedKey)
end
end
end
end
table.sort(out, function(a, b)
return MTH_BOOK_SafeLower(MTH_BOOKTAB_GetNPCFunctionLabel(a)) < MTH_BOOK_SafeLower(MTH_BOOKTAB_GetNPCFunctionLabel(b))
end)
return out
end
MTH_BOOK_GetNPCFunctions = MTH_BOOKTAB_GetNPCFunctions
function MTH_BOOKTAB_GetNPCFunctionSummary(vendor)
local keys = MTH_BOOKTAB_GetNPCFunctions(vendor)
if table.getn(keys) == 0 then return "-", {} end
local labels = {}
for i = 1, table.getn(keys) do
table.insert(labels, MTH_BOOKTAB_GetNPCFunctionLabel(keys[i]))
end
return table.concat(labels, ", "), keys
end
MTH_BOOK_GetNPCFunctionSummary = MTH_BOOKTAB_GetNPCFunctionSummary
local function MTH_BOOKTAB_GetZoneAndSubzone(zoneId)
local zid = tonumber(zoneId)
if not zid then return "-", "-" end
local zoneName = MTH_BOOK_GetZoneName(zid)
-- Coord zone IDs are WMA; MTH_DS_Zones is keyed by pfQuest area IDs. Translate
-- first, or WMA 121 (Feralas) collides with pfQ 121 (Raven Hill -> parent Duskwood).
local pfqId = zid
if type(MTH_MAP_WMA_TO_PFQ) == "table" then
pfqId = MTH_MAP_WMA_TO_PFQ[zid] or zid
end
local row = MTH_DS_Zones and (MTH_DS_Zones[pfqId] or MTH_DS_Zones[tostring(pfqId)])
if row and row.parent and tonumber(row.parent) and tonumber(row.parent) ~= pfqId then
local parentName = MTH_BOOK_GetZoneName(row.parent)
if parentName and string.sub(parentName, 1, 5) ~= "Zone " then
return parentName, zoneName
end
end
return zoneName, "-"
end
function MTH_BOOKTAB_GetNPCZoneSummary(vendor)
if not vendor or not vendor.coords or table.getn(vendor.coords) == 0 then
return "-", "-", {}
end
local zones = {}
local zoneSet = {}
local firstZone = "-"
local firstSubzone = "-"
for i = 1, table.getn(vendor.coords) do
local c = vendor.coords[i]
if c and c[3] then
local zoneName, subzoneName = MTH_BOOKTAB_GetZoneAndSubzone(c[3])
if firstZone == "-" then
firstZone = zoneName
firstSubzone = subzoneName
end
if zoneName ~= "-" and not zoneSet[zoneName] then
zoneSet[zoneName] = true
table.insert(zones, zoneName)
end
end
end
return firstZone, firstSubzone, zones
end
MTH_BOOK_GetNPCZoneSummary = MTH_BOOKTAB_GetNPCZoneSummary
-- Zone column text: every distinct zone the NPC sits in, most-specific (smallest
-- map) first, so multi-location NPCs read e.g. "Darnassus, Teldrassil".
function MTH_BOOKTAB_GetNPCZoneDisplay(vendor)
if not vendor or not vendor.coords or table.getn(vendor.coords) == 0 then
return "-"
end
local seen = {}
local list = {}
for i = 1, table.getn(vendor.coords) do
local c = vendor.coords[i]
if c and c[3] then
local zoneName = MTH_BOOK_GetZoneName(c[3])
if zoneName and zoneName ~= "-" and not seen[zoneName] then
seen[zoneName] = true
local area = nil
local size = MTH_DS_MinimapSizes and (MTH_DS_MinimapSizes[tonumber(c[3])] or MTH_DS_MinimapSizes[c[3]])
if type(size) == "table" and tonumber(size[1]) and tonumber(size[2]) then
area = tonumber(size[1]) * tonumber(size[2])
end
table.insert(list, { name = zoneName, area = area, order = i })
end
end
end
local count = table.getn(list)
if count == 0 then return "-" end
-- Smallest map first; zones with no known size keep data order, at the end.
table.sort(list, function(x, y)
if x.area and y.area then
if x.area ~= y.area then return x.area < y.area end
return x.order < y.order
end
if x.area then return true end
if y.area then return false end
return x.order < y.order
end)
local names = {}
for i = 1, count do table.insert(names, list[i].name) end
return table.concat(names, ", ")
end
MTH_BOOK_GetNPCZoneDisplay = MTH_BOOKTAB_GetNPCZoneDisplay
function MTH_BOOK_NPCHasZoneId(vendor, zoneId)
local zid = tonumber(zoneId)
if not zid or not vendor or not vendor.coords then
return false
end
for i = 1, table.getn(vendor.coords) do
local c = vendor.coords[i]
if c and tonumber(c[3]) == zid then
return true
end
end
return false
end
function MTH_BOOK_NPCMatches(npcId, vendor)
if not vendor then return false end
local react = MTH_BOOK_GetNPCReactBucket(vendor.fac)
if react == "A" and not MTH_BOOK_STATE.flag1 then return false end
if react == "H" and not MTH_BOOK_STATE.flag2 then return false end
if react == "AH" and not MTH_BOOK_STATE.flag3 then return false end
if MTH_BOOK_STATE.npcHideNoZone then
local firstZone = MTH_BOOK_GetNPCZoneSummary(vendor)
local zoneText = MTH_BOOK_SafeLower(firstZone)
if firstZone == "-" or zoneText == "" or zoneText == "unknown" or string.sub(zoneText, 1, 5) == "zone " then
return false
end
end
if MTH_BOOK_STATE.npcFunction and MTH_BOOK_STATE.npcFunction ~= "all" then
local hasFunction = false
local functions = MTH_BOOK_GetNPCFunctions(vendor)
for i = 1, table.getn(functions) do
if functions[i] == MTH_BOOK_STATE.npcFunction then
hasFunction = true
break
end
end
if not hasFunction then return false end
end
if MTH_BOOK_STATE.npcZone and MTH_BOOK_STATE.npcZone ~= "all" then
local _, _, zones = MTH_BOOK_GetNPCZoneSummary(vendor)
local hasZone = false
for i = 1, table.getn(zones) do
if zones[i] == MTH_BOOK_STATE.npcZone then
hasZone = true
break
end
end
if not hasZone then return false end
end
if MTH_BOOK_STATE.npcInZoneOnly then
local currentZoneId = MTH_BOOK_GetCurrentZoneId()
if currentZoneId and not MTH_BOOK_NPCHasZoneId(vendor, currentZoneId) then
return false
end
end
if MTH_BOOK_STATE.search ~= "" then
local functionSummary = MTH_BOOK_GetNPCFunctionSummary(vendor)
local firstZone, firstSubzone = MTH_BOOK_GetNPCZoneSummary(vendor)
local displayName = (MTH and MTH.GetLocalizedNPCNameById and MTH:GetLocalizedNPCNameById(npcId, vendor.name)) or vendor.name
local hay = MTH_BOOK_SafeLower(tostring(displayName or "") .. " " .. tostring(functionSummary or "") .. " " .. tostring(firstZone or "") .. " " .. tostring(firstSubzone or "") .. " " .. tostring(npcId))
if string.find(hay, MTH_BOOK_STATE.search, 1, true) == nil then
return false
end
end
return true
end
function MTH_BOOK_NPCSort(a, b)
local vendors = MTH_DS_Vendors
local va = vendors and vendors[a]
local vb = vendors and vendors[b]
if not va or not vb then return a < b end
local vaName = (MTH and MTH.GetLocalizedNPCNameById and MTH:GetLocalizedNPCNameById(a, va.name)) or va.name
local vbName = (MTH and MTH.GetLocalizedNPCNameById and MTH:GetLocalizedNPCNameById(b, vb.name)) or vb.name
local na = MTH_BOOK_SafeLower(vaName)
local nb = MTH_BOOK_SafeLower(vbName)
if na ~= nb then return na < nb end
return a < b
end