v1.4.0 Twow 1.18.1

This commit is contained in:
DuvelCorp
2026-03-23 22:25:10 +01:00
parent 23bb69d094
commit 8479607a13
78 changed files with 66731 additions and 68666 deletions
+543
View File
@@ -0,0 +1,543 @@
------------------------------------------------------
-- MetaHunt: Beast Lore Scanner
-- TWoW 1.18.1 — "Nightmares of Ursol"
------------------------------------------------------
-- Fires when the player channels Beast Lore on a target.
-- Captures all available data and stores it in
-- MTH_SavedVariables.beastLoreScan.entries (account-wide).
--
-- Dedup rule: skips recording if the same creature name
-- already exists in this zone within 30 yards of current
-- player position (uses ds-minimap-sizes for yard scale).
-- A chat message is always printed either way.
------------------------------------------------------
local SPELL_BEAST_LORE = "Beast Lore"
local DEDUP_YARDS = 30
local FALLBACK_DEDUP_PCT = 0.015 -- ~1.5% of map width if zone size unknown
-- ── Tooltip probe ────────────────────────────────────────────
local MTH_BLS_Probe = nil
local function MTH_BLS_EnsureProbe()
if not MTH_BLS_Probe then
MTH_BLS_Probe = CreateFrame("GameTooltip", "MTH_BLSProbe", UIParent, "GameTooltipTemplate")
MTH_BLS_Probe:SetOwner(UIParent, "ANCHOR_NONE")
end
return MTH_BLS_Probe
end
-- Read left/right text lines from the probe (Lua 5.0 compatible: getglobal)
local function MTH_BLS_GetProbeLines()
local lines = {}
for i = 1, 24 do
local L = getglobal("MTH_BLSProbeTextLeft" .. i)
local R = getglobal("MTH_BLSProbeTextRight" .. i)
local lText = L and L:GetText() or nil
local rText = R and R:GetText() or nil
if (not lText or lText == "") and (not rText or rText == "") then
break
end
table.insert(lines, { left = lText or "", right = rText or "" })
end
return lines
end
-- Parse Beast Lore tooltip lines into structured fields.
-- Uses string.find with captures (Lua 5.0, no string.match).
local function MTH_BLS_ParseTooltip(lines)
local data = {
tameable = nil, -- true / false / nil (unknown)
diet = nil,
abilities = nil,
armor = nil,
health = nil,
minDmg = nil,
maxDmg = nil,
}
for _, line in ipairs(lines) do
local L = line.left or ""
local R = line.right or ""
-- Tameability ──────────────────────────────────────────
if string.find(L, "Cannot Be Tamed", 1, true) or string.find(R, "Cannot Be Tamed", 1, true) then
data.tameable = false
elseif string.find(L, "Can Be Tamed", 1, true) or string.find(R, "Can Be Tamed", 1, true)
or string.find(L, "Tameable", 1, true) or string.find(R, "Tameable", 1, true) then
data.tameable = true
end
-- Diet ─────────────────────────────────────────────────
if not data.diet then
local _, _, v = string.find(L, "^Diet:%s*(.*)")
if not v then _, _, v = string.find(R, "^Diet:%s*(.*)") end
if v and v ~= "" then data.diet = v end
end
-- Tamed Abilities ──────────────────────────────────────
if not data.abilities then
local _, _, v = string.find(L, "^Tamed Abilities:%s+(.*)")
if not v then _, _, v = string.find(R, "^Tamed Abilities:%s+(.*)") end
if v and v ~= "" then data.abilities = v end
end
-- Damage range ("107 - 133 Damage") ────────────────────
if not data.minDmg then
local _, _, lo, hi = string.find(L, "(%d+) %- (%d+) Damage")
if not lo then _, _, lo, hi = string.find(R, "(%d+) %- (%d+) Damage") end
if lo and hi then
data.minDmg = tonumber(lo)
data.maxDmg = tonumber(hi)
end
end
-- Armor ────────────────────────────────────────────────
if not data.armor then
local _, _, v = string.find(L, "Armor:%s*(%d+)")
if not v then _, _, v = string.find(R, "Armor:%s*(%d+)") end
if v then data.armor = tonumber(v) end
end
-- Health ───────────────────────────────────────────────
if not data.health then
local _, _, v = string.find(L, "Health:%s*(%d+)")
if not v then _, _, v = string.find(R, "Health:%s*(%d+)") end
if v then data.health = tonumber(v) end
end
end
return data
end
-- ── Location helpers ─────────────────────────────────────────
-- pfQuest AreaTable ID → WMA ID bridge (GetCurrentMapAreaID returns pfQ IDs)
local MTH_PFQ_TO_WMA = {
[1]=27,[3]=17,[4]=19,[8]=38,[10]=34,[11]=40,[12]=30,[14]=4,[15]=141,[16]=181,
[17]=11,[25]=519,[28]=22,[33]=37,[36]=15,[38]=35,[40]=39,[41]=32,[44]=36,
[45]=16,[46]=29,[47]=26,[51]=28,[85]=20,[130]=21,[139]=23,[141]=41,[148]=42,
[209]=650,[215]=9,[267]=24,[331]=43,[357]=121,[361]=182,[400]=61,[405]=101,
[406]=81,[408]=505,[409]=504,[440]=161,[490]=201,[491]=641,[493]=241,
[616]=501,[618]=281,[717]=611,[718]=516,[719]=609,[721]=518,[722]=639,
[796]=520,[876]=500,[1176]=605,[1337]=521,[1377]=261,[1477]=607,[1497]=382,
[1519]=301,[1537]=341,[1581]=514,[1583]=629,[1584]=623,[1637]=321,[1638]=362,
[1657]=381,[1769]=700,[1941]=523,[1977]=619,[2017]=652,[2040]=509,[2057]=648,
[2100]=517,[2159]=627,[2257]=659,[2366]=663,[2437]=603,[2557]=689,[2597]=401,
[2677]=635,[2717]=617,[3277]=443,[3358]=461,[3428]=654,[3429]=625,[3456]=667,
[3457]=682,[3478]=522,[4012]=502,[5023]=503,[5024]=511,[5053]=680,[5077]=670,
[5086]=674,[5087]=676,[5097]=678,[5103]=672,[5121]=507,[5130]=601,[5135]=644,
[5136]=643,[5138]=637,[5147]=655,[5148]=668,[5153]=645,[5163]=646,[5179]=510,
[5204]=662,[5208]=665,[5225]=513,[5536]=512,[5557]=683,[5561]=685,[5581]=686,
[5601]=691,[5602]=684,[5628]=693,[5640]=701,[5641]=698,[5642]=699,[5722]=702,
[5723]=657,[5731]=705,[5734]=704,
}
local function MTH_BLS_GetZoneId()
if type(SetMapToCurrentZone) == "function" then
pcall(SetMapToCurrentZone)
end
if type(GetCurrentMapAreaID) == "function" then
local ok, id = pcall(GetCurrentMapAreaID)
if ok and tonumber(id) and tonumber(id) > 0 then
local pfq = tonumber(id)
return MTH_PFQ_TO_WMA[pfq] or pfq
end
end
return 0
end
local function MTH_BLS_GetPlayerCoords()
if type(SetMapToCurrentZone) == "function" then
pcall(SetMapToCurrentZone)
end
if type(GetPlayerMapPosition) ~= "function" then return nil, nil end
local ok, x, y = pcall(GetPlayerMapPosition, "player")
if ok and x and y and (x ~= 0 or y ~= 0) then return x, y end
return nil, nil
end
-- Returns true if a beast with this name is already in the static MTH_DS_Beasts database.
-- We skip SavedVariables recording for beasts already catalogued in the DB.
local function MTH_BLS_IsInStaticDB(name)
if type(MTH_DS_Beasts) ~= "table" or not name or name == "" then return false end
for _, row in pairs(MTH_DS_Beasts) do
if type(row) == "table" and row.name == name then
return true
end
end
return false
end
-- ── Storage ──────────────────────────────────────────────────
local function MTH_BLS_EnsureStore()
if type(MTH_SavedVariables) ~= "table" then
MTH_SavedVariables = {}
end
if type(MTH_SavedVariables.beastLoreScan) ~= "table" then
MTH_SavedVariables.beastLoreScan = {}
end
if type(MTH_SavedVariables.beastLoreScan.entries) ~= "table" then
MTH_SavedVariables.beastLoreScan.entries = {}
end
return MTH_SavedVariables.beastLoreScan
end
-- ── Dedup ────────────────────────────────────────────────────
-- Returns the existing entry for a same-named beast in the same zone,
-- plus whether any of its recorded coords is within DEDUP_YARDS of (x, y).
local function MTH_BLS_FindEntry(name, zoneId, x, y)
local store = MTH_BLS_EnsureStore()
-- Normalize: treat 0 as unknown (entries are stored with nil for unknown zones)
if not zoneId or zoneId == 0 then zoneId = nil end
local mapW, mapH = nil, nil
if type(MTH_DS_MinimapSizes) == "table" and zoneId then
local sz = MTH_DS_MinimapSizes[zoneId]
if type(sz) == "table" then
mapW = tonumber(sz[1])
mapH = tonumber(sz[2])
end
end
local function IsNearby(ex, ey)
if not ex or not ey or not x or not y then return false end
local dx = ex - x
local dy = ey - y
if mapW and mapH then
return math.sqrt((dx * mapW)^2 + (dy * mapH)^2) < DEDUP_YARDS
else
-- No map size data: use a generous fallback (~5% of map width)
return math.sqrt(dx * dx + dy * dy) < 0.05
end
end
local entries = store.entries
for i = 1, table.getn(entries) do
local e = entries[i]
-- Match by name and zone. When either side has no zone, match on name alone
-- (coords will still be checked below; cross-zone false positives are
-- unlikely given players don't share coordinate spaces across zones).
local zoneMatch = (e.zoneId == zoneId) or (not e.zoneId) or (not zoneId)
if type(e) == "table"
and e.name == name
and zoneMatch
then
-- Check all recorded coords for this beast.
if type(e.coords) == "table" then
for j = 1, table.getn(e.coords) do
local c = e.coords[j]
if type(c) == "table" and IsNearby(c.x, c.y) then
return e, true -- entry found, nearby coord found
end
end
elseif IsNearby(e.x, e.y) then
-- Legacy single-coord entry
return e, true
end
return e, false -- entry found, but no coord nearby
end
end
return nil, false -- no entry for this beast in this zone
end
-- ── Main scan ────────────────────────────────────────────────
-- Per-session cooldown: prevents re-recording the same beast in the
-- same zone within SCAN_COOLDOWN seconds, as a safety net on top of
-- the coordinate-based dedup.
local MTH_BLS_ScanCooldowns = {}
local SCAN_COOLDOWN = 120 -- 2 minutes
local function MTH_BLS_Scan()
-- Basic target guards
if type(UnitExists) == "function" and not UnitExists("target") then return end
if type(UnitIsPlayer) == "function" and UnitIsPlayer("target") then return end
local name = (type(UnitName) == "function") and UnitName("target") or nil
if not name or name == "" then return end
-- ── Unit data ───────────────────────────────────────────
local level = (type(UnitLevel) == "function") and UnitLevel("target") or nil
local family = (type(UnitCreatureFamily) == "function") and UnitCreatureFamily("target") or nil
local healthMax = (type(UnitHealthMax) == "function") and UnitHealthMax("target") or nil
local minDmg, maxDmg, mainSpeed = nil, nil, nil
if type(UnitDamage) == "function" then
local ok, a, b, _, _, spd = pcall(UnitDamage, "target")
if ok then minDmg, maxDmg, mainSpeed = a, b, spd end
end
if not mainSpeed and type(UnitAttackSpeed) == "function" then
local ok, spd = pcall(UnitAttackSpeed, "target")
if ok then mainSpeed = spd end
end
local guid = nil
if type(UnitGUID) == "function" then
local ok, g = pcall(UnitGUID, "target")
if ok and g then guid = g end
end
-- ── Location ────────────────────────────────────────────
local zoneId = MTH_BLS_GetZoneId()
local x, y = MTH_BLS_GetPlayerCoords()
local zone = (type(GetZoneText) == "function") and GetZoneText() or ""
local subzone = (type(GetSubZoneText) == "function") and GetSubZoneText() or ""
-- Session cooldown: hard block on re-scanning the same beast in same zone
local cooldownKey = tostring(name) .. "|" .. tostring(zone)
local lastScan = MTH_BLS_ScanCooldowns[cooldownKey]
if lastScan and (GetTime() - lastScan) < SCAN_COOLDOWN then
MTH:Log("|cff888888Beast Lore: " .. tostring(name) .. " scanned recently — skipped|r")
return
end
-- ── Tooltip probe ───────────────────────────────────────
local probe = MTH_BLS_EnsureProbe()
probe:ClearLines()
local probeOk = pcall(function() probe:SetUnit("target") end)
local parsed = { tameable = nil, diet = nil, abilities = nil, armor = nil, health = nil }
if probeOk then
local lines = MTH_BLS_GetProbeLines()
parsed = MTH_BLS_ParseTooltip(lines)
end
-- Tooltip values override unit API (Beast Lore reveals more accurate data)
if parsed.minDmg then minDmg = parsed.minDmg end
if parsed.maxDmg then maxDmg = parsed.maxDmg end
if parsed.health then healthMax = parsed.health end
-- Fallback tameability: family present = tameable
if parsed.tameable == nil then
parsed.tameable = (family and family ~= "") and true or false
end
-- ── Dedup / upsert ──────────────────────────────────────
local function R3(v)
if type(v) ~= "number" then return nil end
return math.floor(v * 1000 + 0.5) / 1000
end
local function R2(v)
if type(v) ~= "number" then return nil end
return math.floor(v * 100 + 0.5) / 100
end
local rx, ry = R3(x), R3(y)
-- Mark scanned now (before storing) so any rapid re-trigger is blocked
MTH_BLS_ScanCooldowns[cooldownKey] = GetTime()
local existing, nearbyFound = MTH_BLS_FindEntry(name, zoneId, x, y)
if nearbyFound then
-- Already recorded at this location — do nothing.
MTH:Log("|cff888888Beast Lore: " .. tostring(name)
.. " already recorded nearby — skipped|r")
return
end
local store = MTH_BLS_EnsureStore()
if existing then
-- Known beast, new location — add coord to its list.
if type(existing.coords) ~= "table" then
-- Migrate legacy single coord into coords list.
existing.coords = {}
if existing.x or existing.y then
table.insert(existing.coords, { x = existing.x, y = existing.y, timestamp = existing.timestamp })
end
existing.x = nil
existing.y = nil
end
table.insert(existing.coords, { x = rx, y = ry, timestamp = (type(time) == "function") and time() or nil })
existing.lastUpdated = (type(time) == "function") and time() or nil
-- Update abilities/data if newly revealed by this cast.
if parsed.abilities and parsed.abilities ~= "" then existing.abilities = parsed.abilities end
local coordStr = (rx and ry)
and string.format(" (%.1f, %.1f)", rx * 100, ry * 100)
or ""
local abilitiesStr = (existing.abilities and existing.abilities ~= "")
and (" |cff88ddff[" .. existing.abilities .. "]|r")
or ""
MTH:Log("Beast Lore: |cffffaa00" .. tostring(name) .. "|r lv" .. (existing.level and tostring(existing.level) or "?")
.. " — new coord added" .. coordStr
.. abilitiesStr
.. " |cff888888[#" .. tostring(table.getn(store.entries)) .. " +" .. tostring(table.getn(existing.coords)) .. " loc]|r")
return
end
-- Skip recording if this beast is already in the static database.
if MTH_BLS_IsInStaticDB(name) then
MTH:Log("|cff888888Beast Lore: " .. tostring(name) .. " is already in the MetaHunt database — skipped|r")
return
end
-- New beast entirely.
local entry = {
name = name,
guid = guid,
level = level,
family = family,
tameable = parsed.tameable,
minDmg = minDmg and math.floor(minDmg + 0.5) or nil,
maxDmg = maxDmg and math.floor(maxDmg + 0.5) or nil,
attackSpeed = mainSpeed and R2(mainSpeed) or nil,
armor = parsed.armor or nil,
health = healthMax or nil,
diet = parsed.diet or nil,
abilities = parsed.abilities or nil,
coords = { { x = rx, y = ry, timestamp = (type(time) == "function") and time() or nil } },
zoneId = (zoneId and zoneId > 0) and zoneId or nil,
zone = (zone and zone ~= "") and zone or nil,
subzone = (subzone and subzone ~= "") and subzone or nil,
timestamp = (type(time) == "function") and time() or nil,
}
table.insert(store.entries, entry)
-- ── Chat confirmation ────────────────────────────────────
local tameStr = parsed.tameable
and "|cff33ff55Tameable|r"
or "|cffff4444Cannot be Tamed|r"
local famStr = (family and family ~= "")
and ("|cffffcc00" .. family .. "|r")
or "|cff888888unknown family|r"
local lvlStr = level and tostring(level) or "?"
local coordStr = (x and y)
and string.format(" (%.1f, %.1f)", x * 100, y * 100)
or ""
local zoneStr = (zone and zone ~= "") and (", " .. zone .. coordStr) or coordStr
local idx = table.getn(store.entries)
local abilitiesStr = ""
if parsed.abilities and parsed.abilities ~= "" then
abilitiesStr = " |cff88ddff[" .. parsed.abilities .. "]|r"
end
MTH:Log("|cff00ff88YOU FOUND AND RECORDED A NEW BEAST!|r |cffffaa00" .. tostring(name) .. "|r lv" .. lvlStr
.. " — " .. tameStr .. " • " .. famStr
.. zoneStr
.. abilitiesStr
.. " |cff888888[#" .. tostring(idx) .. "]|r")
end
-- ── Detection: event-gated tooltip poll ─────────────────────
-- SPELLCAST_CHANNEL_* events do not reliably fire in TurtleWoW 1.18.1.
-- Strategy: spellcast events open a short window (MTH_BLS_WindowEnd).
-- OnUpdate only runs during that window, checking the target tooltip
-- for Beast Lore-specific lines ("Diet:", "Tamed Abilities:").
-- If no event fires at all, the window stays closed and nothing runs.
local MTH_BLS_Frame = CreateFrame("Frame", "MTH_BeastLoreScanFrame")
local MTH_BLS_PollAccum = 0
local MTH_BLS_WindowEnd = 0 -- GetTime() deadline; 0 = window closed
local MTH_BLS_PollLast = nil -- target name already scanned this window
local WINDOW_DURATION = 6 -- seconds to probe after Beast Lore cast
local function MTH_BLS_OpenWindow()
MTH_BLS_WindowEnd = GetTime() + WINDOW_DURATION
MTH_BLS_PollLast = nil
end
-- [DISABLED] Beast Lore scan activation — not needed while full beast DB is shipped.
-- Re-enable by removing the `if false then` / `end` wrapper below.
if false then
-- Register all known spellcast event variants.
MTH_BLS_Frame:RegisterEvent("SPELLCAST_CHANNEL_START")
MTH_BLS_Frame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
MTH_BLS_Frame:RegisterEvent("SPELLCAST_START")
MTH_BLS_Frame:RegisterEvent("SPELLCAST_STOP")
MTH_BLS_Frame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
MTH_BLS_Frame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
MTH_BLS_Frame:RegisterEvent("UNIT_SPELLCAST_START")
MTH_BLS_Frame:RegisterEvent("UNIT_SPELLCAST_STOP")
MTH_BLS_Frame:RegisterEvent("PLAYER_TARGET_CHANGED")
MTH_BLS_Frame:SetScript("OnEvent", function()
local evt = event or ""
if evt == "PLAYER_TARGET_CHANGED" then
-- New target: reset so re-targeting after a cast works.
MTH_BLS_PollLast = nil
return
end
-- Determine spell name from event args.
local isUnitEvt = string.sub(evt, 1, 15) == "UNIT_SPELLCAST_"
local spellName
if isUnitEvt then
if (arg1 or "") ~= "player" then return end
spellName = arg2 or ""
else
spellName = arg1 or ""
end
if string.find(spellName, SPELL_BEAST_LORE, 1, true) then
MTH_BLS_OpenWindow()
end
end)
MTH_BLS_Frame:SetScript("OnUpdate", function()
MTH_BLS_PollAccum = MTH_BLS_PollAccum + (arg1 or 0)
if MTH_BLS_PollAccum < 2.0 then return end
MTH_BLS_PollAccum = 0
if not (type(UnitExists) == "function" and UnitExists("target")) then return end
if type(UnitIsPlayer) == "function" and UnitIsPlayer("target") then return end
local name = (type(UnitName) == "function") and UnitName("target") or nil
if not name or name == "" then return end
if name == MTH_BLS_PollLast then return end
-- Probe the target tooltip for Beast Lore-specific lines.
local probe = MTH_BLS_EnsureProbe()
probe:ClearLines()
local probeOk = pcall(function() probe:SetUnit("target") end)
if not probeOk then return end
local hasData = false
for i = 1, 20 do
local L = getglobal("MTH_BLSProbeTextLeft" .. i)
if not L then break end
local txt = (L.GetText and L:GetText()) or ""
if string.find(txt, "Diet:", 1, true)
or string.find(txt, "Tamed Abilities:", 1, true)
or string.find(txt, "Cannot Be Tamed", 1, true)
then
hasData = true
break
end
end
if not hasData then return end
-- Beast Lore data confirmed — record and close window.
MTH_BLS_PollLast = name
MTH_BLS_WindowEnd = 0
local ok, err = pcall(MTH_BLS_Scan)
if not ok and MTH and MTH.Print then
MTH:Print("[BeastLore Scan] error: " .. tostring(err), "error")
end
end)
end -- [DISABLED] if false
-- ── Public accessors ─────────────────────────────────────────
-- Returns the first saved entry whose name matches (case-insensitive),
-- or nil if none found.
function MTH_BLS_FindSavedBeastByName(name)
local store = MTH_BLS_EnsureStore()
if not store or type(store.entries) ~= "table" then return nil end
local wantLower = string.lower(name or "")
if wantLower == "" then return nil end
for i = 1, table.getn(store.entries) do
local e = store.entries[i]
if type(e) == "table" and string.lower(e.name or "") == wantLower then
return e
end
end
return nil
end
+16 -4
View File
@@ -1,5 +1,5 @@
MTH_CONST = MTH_CONST or {}
MTH_CONST.version = MTH_CONST.version or "1.3.0"
MTH_CONST.version = MTH_CONST.version or "1.4.0"
MTH_CONST.WEAPON_TYPES = {
BOWS = "Bows",
@@ -24,6 +24,7 @@ MTH_CONST.AMMO = {
ICETHREADED = "Ice Threaded Bullet",
THORIUM = "Thorium Shells",
ROCKSHARD = "Rockshard Pellets",
CRAFTEDTHORIUM = "Enchanted Thorium Shells",
CANNONBALL = "Miniature Cannon Balls",
},
ARROWS = {
@@ -32,16 +33,19 @@ MTH_CONST.AMMO = {
RAZOR = "Razor Arrow",
FEATHER = "Feathered Arrow",
PRECISION = "Precision Arrow",
BRIGHT = "Bright Wood Arrows",
JAGGED = "Jagged Arrow",
SHADE = "Shade Wood Arrows",
ICETHREADED = "Ice Threaded Arrow",
SMOOTH = "Smooth Ironfeather Arrows",
THORIUM = "Thorium Headed Arrow",
STAR = "Starfeather Arrows",
DOOMSHOT = "Doomshot",
},
JUNKSHOTS = {
SERPENT = "Serpent Sting",
SCORPID = "Scorpid Sting",
VIPER = "Viper Sting",
ARCANE = "Arcane Shot",
CONCUSSIVE = "Concussive Shot",
DISTRACTING = "Distracting Shot",
BAITED = "Baited Shot",
@@ -69,6 +73,7 @@ BULLETS_ICETHREADED = MTH_CONST.AMMO.BULLETS.ICETHREADED
BULLETS_THORIUM = MTH_CONST.AMMO.BULLETS.THORIUM
BULLETS_ROCKSHARD = MTH_CONST.AMMO.BULLETS.ROCKSHARD
BULLETS_CANNONBALL = MTH_CONST.AMMO.BULLETS.CANNONBALL
BULLETS_CRAFTEDTHORIUM = MTH_CONST.AMMO.BULLETS.CRAFTEDTHORIUM
ARROWS_ROUGH = MTH_CONST.AMMO.ARROWS.ROUGH
ARROWS_SHARP = MTH_CONST.AMMO.ARROWS.SHARP
@@ -77,13 +82,16 @@ ARROWS_FEATHER = MTH_CONST.AMMO.ARROWS.FEATHER
ARROWS_PRECISION = MTH_CONST.AMMO.ARROWS.PRECISION
ARROWS_JAGGED = MTH_CONST.AMMO.ARROWS.JAGGED
ARROWS_ICETHREADED = MTH_CONST.AMMO.ARROWS.ICETHREADED
ARROWS_BRIGHT = MTH_CONST.AMMO.ARROWS.BRIGHT
ARROWS_SHADE = MTH_CONST.AMMO.ARROWS.SHADE
ARROWS_SMOOTH = MTH_CONST.AMMO.ARROWS.SMOOTH
ARROWS_THORIUM = MTH_CONST.AMMO.ARROWS.THORIUM
ARROWS_STAR = MTH_CONST.AMMO.ARROWS.STAR
ARROWS_DOOMSHOT = MTH_CONST.AMMO.ARROWS.DOOMSHOT
JUNKSHOT_SERPENT = MTH_CONST.AMMO.JUNKSHOTS.SERPENT
JUNKSHOT_SCORPID = MTH_CONST.AMMO.JUNKSHOTS.SCORPID
JUNKSHOT_VIPER = MTH_CONST.AMMO.JUNKSHOTS.VIPER
JUNKSHOT_ARCANE = MTH_CONST.AMMO.JUNKSHOTS.ARCANE
JUNKSHOT_CONCUSSIVE = MTH_CONST.AMMO.JUNKSHOTS.CONCUSSIVE
JUNKSHOT_DISTRACTING = MTH_CONST.AMMO.JUNKSHOTS.DISTRACTING
JUNKSHOT_BAITED = MTH_CONST.AMMO.JUNKSHOTS.BAITED
@@ -105,6 +113,7 @@ MTH_AMMO_BULLETS = {
BULLETS_ICETHREADED,
BULLETS_THORIUM,
BULLETS_ROCKSHARD,
BULLETS_CRAFTEDTHORIUM,
BULLETS_CANNONBALL,
}
@@ -114,9 +123,13 @@ MTH_AMMO_ARROWS = {
ARROWS_RAZOR,
ARROWS_FEATHER,
ARROWS_PRECISION,
ARROWS_BRIGHT,
ARROWS_JAGGED,
ARROWS_SHADE,
ARROWS_ICETHREADED,
ARROWS_SMOOTH,
ARROWS_THORIUM,
ARROWS_STAR,
ARROWS_DOOMSHOT,
}
@@ -124,7 +137,6 @@ MTH_AMMO_JUNKSHOTS = {
JUNKSHOT_SERPENT,
JUNKSHOT_SCORPID,
JUNKSHOT_VIPER,
JUNKSHOT_ARCANE,
JUNKSHOT_CONCUSSIVE,
JUNKSHOT_DISTRACTING,
JUNKSHOT_BAITED,
+7 -32
View File
@@ -1,5 +1,5 @@
MTH = MTH or {
version = "1.3.0",
version = "1.4.0",
name = "MetaHunt",
modules = {},
config = {},
@@ -266,15 +266,6 @@ if type(MTH_CharSavedVariables.modules) ~= "table" then
MTH_CharSavedVariables.modules = {}
end
local function MTH_ModuleStateDebug(message)
local text = "[MODULE STATE] " .. tostring(message or "")
if MTH and MTH.debug and MTH.Log then
MTH:Log(text, "debug")
elseif MTH and MTH.debug and MTH.Print then
MTH:Print(text, "debug")
end
end
local function MTH_IsPetUiFrameName(frameName)
local name = tostring(frameName or "")
if name == "" then
@@ -423,7 +414,6 @@ end
local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
if type(name) ~= "string" or name == "" then
MTH_ModuleStateDebug("read <invalid-name> fallback-default=" .. tostring(defaultEnabled and true or false))
return defaultEnabled and true or false
end
@@ -461,7 +451,6 @@ local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
local resolved = hardDefault and true or false
MTH_CharSavedVariables.moduleStates[resolvedName] = resolved
MTH_CharSavedVariables.modules[resolvedName].enabled = resolved
MTH_ModuleStateDebug("read " .. tostring(name) .. " seeded char default=" .. tostring(resolved))
return resolved
end
end
@@ -472,7 +461,6 @@ local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
local key = candidates[i]
if MTH_CharSavedVariables.moduleStates[key] ~= nil then
local resolved = MTH_CharSavedVariables.moduleStates[key] and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from char.moduleStates." .. tostring(key) .. "=" .. tostring(resolved))
MTH_CharSavedVariables.moduleStates[resolvedName] = resolved
return resolved
end
@@ -484,7 +472,6 @@ local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
local key = candidates[i]
if MTH_SavedVariables.moduleStates[key] ~= nil then
local resolved = MTH_SavedVariables.moduleStates[key] and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from moduleStates." .. tostring(key) .. "=" .. tostring(resolved))
MTH_CharSavedVariables.moduleStates[resolvedName] = resolved
if type(MTH_CharSavedVariables.modules[resolvedName]) ~= "table" then
MTH_CharSavedVariables.modules[resolvedName] = {}
@@ -500,7 +487,6 @@ local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
local key = candidates[i]
if type(MTH_SavedVariables.modules[key]) == "table" and MTH_SavedVariables.modules[key].enabled ~= nil then
local resolved = MTH_SavedVariables.modules[key].enabled and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from modules." .. tostring(key) .. ".enabled=" .. tostring(resolved))
MTH_CharSavedVariables.moduleStates[resolvedName] = resolved
if type(MTH_CharSavedVariables.modules[resolvedName]) ~= "table" then
MTH_CharSavedVariables.modules[resolvedName] = {}
@@ -515,7 +501,6 @@ local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
local key = candidates[i]
if type(MTH_SavedVariables[key]) == "table" and MTH_SavedVariables[key].enabled ~= nil then
local resolved = MTH_SavedVariables[key].enabled and true or false
MTH_ModuleStateDebug("read " .. tostring(name) .. " from legacy-top-level." .. tostring(key) .. ".enabled=" .. tostring(resolved))
MTH_CharSavedVariables.moduleStates[resolvedName] = resolved
if type(MTH_CharSavedVariables.modules[resolvedName]) ~= "table" then
MTH_CharSavedVariables.modules[resolvedName] = {}
@@ -525,7 +510,6 @@ local function MTH_GetPersistedModuleEnabled(name, defaultEnabled)
end
end
MTH_ModuleStateDebug("read " .. tostring(name) .. " no-persisted-value fallback-default=" .. tostring(defaultEnabled and true or false))
return defaultEnabled and true or false
end
@@ -555,7 +539,6 @@ local function MTH_SetPersistedModuleEnabled(name, enabled)
enabled = enabled and true or false
MTH_CharSavedVariables.moduleStates[name] = enabled
MTH_CharSavedVariables.modules[name].enabled = enabled
MTH_ModuleStateDebug("write " .. tostring(name) .. " enabled=" .. tostring(enabled))
end
function MTH:InitEventRouter()
@@ -669,7 +652,6 @@ function MTH:RegisterModule(name, module)
end
if self.modules[name] then
if self.debug then print("|cFFFFAA00MetaHunt|r: Module '" .. name .. "' already registered") end
return false
end
@@ -684,8 +666,6 @@ function MTH:RegisterModule(name, module)
local initialEnabled = MTH_GetPersistedModuleEnabled(name, defaultEnabled)
module.enabled = initialEnabled and true or false
MTH_ModuleStateDebug("register " .. tostring(name) .. " default=" .. tostring(defaultEnabled) .. " initial=" .. tostring(module.enabled))
self.modules[name] = module
self:RegisterModuleEvents(name, module.events)
@@ -709,7 +689,6 @@ function MTH:RegisterModule(name, module)
self:_SetModuleEventSubscriptions(name, true)
end
if self.debug then print("|cFF00AA00MetaHunt|r: Module '" .. name .. "' registered") end
return true
end
@@ -774,7 +753,6 @@ function MTH:SetModuleEnabled(name, enabled)
if self.SetConfig then
self:SetConfig(name, "enabled", enabled)
end
MTH_ModuleStateDebug("request " .. tostring(name) .. " no-op runtime=" .. tostring(module.enabled) .. " persisted-synced")
return true
end
@@ -796,7 +774,6 @@ function MTH:SetModuleEnabled(name, enabled)
if self.SetConfig then
self:SetConfig(name, "enabled", enabled)
end
MTH_ModuleStateDebug("request " .. tostring(name) .. " applied runtime=" .. tostring(module.enabled))
return true
end
@@ -1035,6 +1012,12 @@ function MTH:Log(msg, severity)
return chatText
end
function MTH:DebugPrint(msg)
if self.debug then
self:Print("[DEBUG] " .. tostring(msg), "debug")
end
end
function MTH:Print(msg, severity)
return self:Log(msg, severity)
end
@@ -1083,12 +1066,6 @@ function MTH:SetMessageEnabled(key, enabled)
return true
end
function MTH:DebugPrint(msg)
if self.debug then
self:Print("[DEBUG] " .. tostring(msg), "debug")
end
end
function MTH_Log(message, severity)
if MTH and MTH.Log then
return MTH:Log(message, severity)
@@ -1270,12 +1247,10 @@ function MTH:ApplyPersistedModuleStates(source)
end
local persistedEnabled = MTH_GetPersistedModuleEnabled(name, defaultEnabled)
MTH_ModuleStateDebug("bootstrap " .. tostring(source or "") .. " " .. tostring(name) .. " persisted=" .. tostring(persistedEnabled) .. " runtime=" .. tostring(module.enabled and true or false))
if module.enabled ~= persistedEnabled then
local ok, err = self:SetModuleEnabled(name, persistedEnabled)
if not ok then
MTH_ModuleStateDebug("bootstrap apply failed " .. tostring(name) .. ": " .. tostring(err))
end
else
MTH_SetPersistedModuleEnabled(name, persistedEnabled)
-2
View File
@@ -623,13 +623,11 @@ function MTH_PT_ApplyScan(triggerReason)
local msg = "New pet data recorded (" .. tostring(added) .. " new rank(s)): " .. detailsText .. ". Known totals for this character: " .. tostring(totalBaselines) .. " ability type(s), " .. tostring(totalRanks) .. " rank(s)."
if MTH_PT_IsScanMessageEnabled() then
MTH:Print(msg)
MTH:Print("INFO: " .. msg, "debug")
end
else
local msg = "New pet data recorded (" .. tostring(added) .. " new rank(s)). Known totals for this character: " .. tostring(totalBaselines) .. " ability type(s), " .. tostring(totalRanks) .. " rank(s)."
if MTH_PT_IsScanMessageEnabled() then
MTH:Print(msg)
MTH:Print("INFO: " .. msg, "debug")
end
end
end
+172
View File
@@ -0,0 +1,172 @@
------------------------------------------------------
-- MetaHunt: Hunter Talent & Spellbook Scanner
-- Runs once on login and re-runs when spells/talents change.
--
-- Exposes:
-- MTH_HT_GetTalentRank(talentName) → currentRank (0 if unlearned)
-- MTH_HT_HasTalent(talentName) → true if rank >= 1
-- MTH_HT_GetSpellInfo(spellName) → { slot, rank, rankNum } or nil
-- MTH_HT_RescanTalents() → force re-scan of all talent tabs
-- MTH_HT_RescanSpellbook() → force re-scan of spellbook
--
-- Results cached in:
-- MTH_HT_Talents["TalentName"] = { tab, idx, rank, maxRank }
-- MTH_HT_Spells["SpellName"] = { slot, rank, rankNum }
-- (spell entry is the *highest rank* known for that name)
------------------------------------------------------
MTH_HT_Talents = {}
MTH_HT_Spells = {}
-- ---------------------------------------------------------------------------
-- Talent scanner
-- Uses GetNumTalentTabs / GetNumTalents / GetTalentInfo (all vanilla 1.12)
-- ---------------------------------------------------------------------------
function MTH_HT_RescanTalents()
MTH_HT_Talents = {}
local numTabs = GetNumTalentTabs and GetNumTalentTabs() or 0
for tabIdx = 1, numTabs do
local numTalents = GetNumTalents and GetNumTalents(tabIdx) or 0
for talIdx = 1, numTalents do
local name, _, _, _, currentRank, maxRank = GetTalentInfo(tabIdx, talIdx)
if name and name ~= "" then
local existing = MTH_HT_Talents[name]
-- If the same name appears in multiple tabs (unlikely), keep highest rank.
if not existing or (tonumber(currentRank) or 0) > (existing.rank or 0) then
MTH_HT_Talents[name] = {
tab = tabIdx,
idx = talIdx,
rank = tonumber(currentRank) or 0,
maxRank = tonumber(maxRank) or 0,
}
end
end
end
end
end
-- ---------------------------------------------------------------------------
-- Spellbook scanner
-- Iterates all player spell tabs and caches every known spell by name.
-- Stores the highest rank found (so MTH_HT_Spells["Aimed Shot"].slot is
-- always the actionable rank the player actually has).
-- ---------------------------------------------------------------------------
local function MTH_HT_ParseRankNum(rankStr)
if not rankStr or rankStr == "" then return 0 end
local _, _, n = string.find(tostring(rankStr), "(%d+)")
return tonumber(n) or 0
end
function MTH_HT_RescanSpellbook()
MTH_HT_Spells = {}
local numTabs = GetNumSpellTabs and GetNumSpellTabs() or 0
for tabIdx = 1, numTabs do
local _, _, offset, numSpells = GetSpellTabInfo(tabIdx)
for i = 1, numSpells do
local slot = offset + i
local name, rank = GetSpellName(slot, BOOKTYPE_SPELL)
if name and name ~= "" then
local rankNum = MTH_HT_ParseRankNum(rank)
local existing = MTH_HT_Spells[name]
if not existing or rankNum > (existing.rankNum or 0) then
MTH_HT_Spells[name] = {
slot = slot,
rank = rank or "",
rankNum = rankNum,
}
end
end
end
end
end
-- ---------------------------------------------------------------------------
-- Public accessors
-- ---------------------------------------------------------------------------
function MTH_HT_GetTalentRank(talentName)
local entry = MTH_HT_Talents[tostring(talentName or "")]
return entry and (entry.rank or 0) or 0
end
function MTH_HT_HasTalent(talentName)
return MTH_HT_GetTalentRank(talentName) >= 1
end
function MTH_HT_GetSpellInfo(spellName)
return MTH_HT_Spells[tostring(spellName or "")]
end
-- ---------------------------------------------------------------------------
-- Event-driven scanning
-- ---------------------------------------------------------------------------
local MTH_HT_Frame = CreateFrame("Frame", "MTH_HunterTalentScanFrame")
local MTH_HT_NeedTalentRescan = false
local MTH_HT_NeedSpellRescan = false
local MTH_HT_RescanDelay = 0.6 -- seconds after event before scanning
local MTH_HT_RescanElapsed = 0
MTH_HT_Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
MTH_HT_Frame:RegisterEvent("LEARNED_SPELL_IN_TAB")
MTH_HT_Frame:RegisterEvent("SPELLS_CHANGED")
MTH_HT_Frame:RegisterEvent("CHARACTER_POINTS_CHANGED")
MTH_HT_Frame:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
-- Immediate scan on login
MTH_HT_RescanTalents()
MTH_HT_RescanSpellbook()
if type(MTH_HT_OnScanComplete) == "function" then
MTH_HT_OnScanComplete()
end
-- Also schedule a deferred re-scan: GetTalentInfo can return stale
-- data for a brief window after PLAYER_ENTERING_WORLD on login.
MTH_HT_NeedTalentRescan = true
MTH_HT_NeedSpellRescan = true
MTH_HT_RescanElapsed = 0
this:Show() -- enable OnUpdate for the deferred pass
return
end
-- For other events, schedule a deferred rescan so we don't fire
-- multiple times if several events arrive in the same frame.
if event == "CHARACTER_POINTS_CHANGED" then
MTH_HT_NeedTalentRescan = true
end
if event == "LEARNED_SPELL_IN_TAB" or event == "SPELLS_CHANGED" then
MTH_HT_NeedSpellRescan = true
end
MTH_HT_RescanElapsed = 0
this:Show()
end)
MTH_HT_Frame:SetScript("OnUpdate", function()
MTH_HT_RescanElapsed = (MTH_HT_RescanElapsed or 0) + (arg1 or 0)
if MTH_HT_RescanElapsed < MTH_HT_RescanDelay then return end
-- Batch-flush pending scans
if MTH_HT_NeedTalentRescan then
MTH_HT_RescanTalents()
MTH_HT_NeedTalentRescan = false
end
if MTH_HT_NeedSpellRescan then
MTH_HT_RescanSpellbook()
MTH_HT_NeedSpellRescan = false
end
-- Notify dependents
if type(MTH_HT_OnScanComplete) == "function" then
MTH_HT_OnScanComplete()
end
this:Hide()
MTH_HT_RescanElapsed = 0
end)
MTH_HT_Frame:Hide()
+19 -11
View File
@@ -429,9 +429,7 @@ local function MTH_PETS_LogConsistency(line)
return
end
local function MTH_PETS_LogTame(line)
return
end
local function MTH_PETS_LogTame(_) end
local function MTH_PETS_FormatRowConsistency(row)
if type(row) ~= "table" then
@@ -856,11 +854,21 @@ local function MTH_PETS_ParseCreatureIdFromGuid(guid)
return nil
end
-- TBC-style string GUID: "Creature-realm-map-inst-?-ENTRY-spawn"
local _, _, creatureIdText = string.find(guidText, "^Creature%-%d+%-%d+%-%d+%-%d+%-(%d+)%-%d+$")
if creatureIdText then
return tonumber(creatureIdText)
end
-- Vanilla/TurtleWoW hex GUID: "0xF130EEEEEECCCCCC" — entry is bits 24-47 (hex chars 7-12)
if string.sub(guidText, 1, 2) == "0x" and string.len(guidText) >= 14 then
local entryHex = string.sub(guidText, 7, 12)
local parsed = tonumber(entryHex, 16)
if parsed and parsed > 0 then
return parsed
end
end
return nil
end
@@ -2553,7 +2561,6 @@ function MTH_PETS_RunSavedVarCleanup(pets)
pets._betaFreshStoreApplied = nil
if purged > 0 and MTH and MTH.Print then
MTH:Print("Cleaned up " .. tostring(purged) .. " stale pet-update records from SavedVariables.", "debug")
end
end
@@ -3400,7 +3407,6 @@ end
local function MTH_ST_DebugDumpLine(line)
if MTH and MTH.Print then
MTH:Print(tostring(line or ""), "debug")
end
end
@@ -3543,7 +3549,6 @@ function MTH_CommandPetsState()
.. " petTrainingLastScan=" .. tostring(petTraining and petTraining.lastScan or 0)
.. " hasCompletedPetScan=" .. tostring(petTraining and petTraining.hasCompletedPetScan or nil)
MTH:Print(summary)
MTH:Print("[PETSSTATE] " .. summary, "debug")
end
function MTH_CommandPetsReset()
@@ -3628,7 +3633,6 @@ function MTH_CommandPetsReset()
.. " preservedHistory=" .. tostring(historyCount)
.. " nextId=" .. tostring(store.nextId)
MTH:Print(line)
MTH:Print("[PETSRESET] " .. line, "debug")
return true
end
@@ -3784,9 +3788,8 @@ function MTH_ST_HandleSpellcastEvent(evt, eventArg1, eventArg2)
if evt == "SPELLCAST_START" or evt == "SPELLCAST_CHANNEL_START"
or evt == "UNIT_SPELLCAST_START" or evt == "UNIT_SPELLCAST_CHANNEL_START" then
if not MTH_PETS_IsTameBeastSpellName(castSpellName) then
return true
end
-- For vanilla SPELLCAST_* events arg1 is a duration (ms), not spell name.
-- Always forward to RecordTameAttempt — it has all the fallback/heuristic logic.
MTH_PETS_RecordTameAttempt(evt, castSpellName)
return true
end
@@ -4156,6 +4159,12 @@ function MTH_ST_InitService()
MTH_PetLifecycleEventFrame:RegisterEvent("UNIT_PET")
MTH_PetLifecycleEventFrame:RegisterEvent("PET_BAR_UPDATE")
MTH_PetLifecycleEventFrame:RegisterEvent("UNIT_HAPPINESS")
MTH_PetLifecycleEventFrame:RegisterEvent("SPELLCAST_CHANNEL_START")
MTH_PetLifecycleEventFrame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
MTH_PetLifecycleEventFrame:RegisterEvent("SPELLCAST_START")
MTH_PetLifecycleEventFrame:RegisterEvent("SPELLCAST_STOP")
MTH_PetLifecycleEventFrame:RegisterEvent("SPELLCAST_FAILED")
MTH_PetLifecycleEventFrame:RegisterEvent("SPELLCAST_INTERRUPTED")
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_COMBAT_XP_GAIN")
MTH_PetLifecycleEventFrame:RegisterEvent("PLAYER_XP_UPDATE")
MTH_PetLifecycleEventFrame:RegisterEvent("CHAT_MSG_SYSTEM")
@@ -4172,7 +4181,6 @@ function MTH_ST_InitService()
MTH_PETS_InstallCoreRenameHook()
MTH_PETS_EmitLiveState("service:init", true)
if MTH_PETS_TRACE_CONSISTENCY and MTH and MTH.Print then
MTH:Print("[PETCONSIST] trace enabled", "debug")
end
end
+114 -50
View File
@@ -301,46 +301,6 @@ if type(MTH_CommandPetSpellScan) ~= "function" then
end
end
------------------------------------------------------
-- /mth diag — Runtime Diagnostics
------------------------------------------------------
function MTH_CommandDiag()
MTH:Print("======= MetaHunt Diagnostics =======", "debug")
-- 1. Lua version
local luaVer = _VERSION or "unknown"
MTH:Print("[DIAG] _VERSION = " .. tostring(luaVer), "debug")
MTH:Print("[DIAG] string.match = " .. tostring(type(string.match) == "function"), "debug")
MTH:Print("[DIAG] string.gfind = " .. tostring(type(string.gfind) == "function"), "debug")
-- 2. Active modules
local moduleCount = 0
local enabledModules = {}
if MTH.modules then
for name, mod in pairs(MTH.modules) do
moduleCount = moduleCount + 1
if mod.enabled then
table.insert(enabledModules, name)
end
end
end
MTH:Print("[DIAG] Registered modules: " .. tostring(moduleCount), "debug")
MTH:Print("[DIAG] Enabled: " .. table.concat(enabledModules, ", "), "debug")
-- 3. Event router state
local routerFrame = (MTH._eventRouter and MTH._eventRouter.frame) or nil
local routerEvents = (MTH._eventRouter and MTH._eventRouter.eventRefs) or {}
local eventCount = 0
for _ in pairs(routerEvents) do eventCount = eventCount + 1 end
MTH:Print("[DIAG] EventRouter frame: " .. tostring(routerFrame ~= nil) .. ", events: " .. tostring(eventCount), "debug")
-- 4. Debug frame initialized?
local dfInit = (MTH_DebugFrame and MTH_DebugFrame.initialized) and true or false
MTH:Print("[DIAG] DebugFrame initialized: " .. tostring(dfInit), "debug")
MTH:Print("======= End Diagnostics =======", "debug")
end
function SlashCmdList.MTH(msg, editbox)
if MTH and MTH.ApplyClassGate and MTH:ApplyClassGate("slash") then
return
@@ -352,24 +312,128 @@ function SlashCmdList.MTH(msg, editbox)
local lowerMsg = string.lower(msg)
local _, _, lowerCmd, lowerArg = string.find(lowerMsg, "^(%S+)%s*(.-)%s*$")
if msg == "" then
MTH:Print("Available: /mth options, /mth book, /mth diag, /mth err")
elseif lowerMsg == "options" then
MTH_CommandOptions()
elseif lowerMsg == "book" or lowerMsg == "hunterbook" then
MTH_CommandBook()
elseif lowerMsg == "peers" or lowerMsg == "who" then
MTH_CommandPeers()
elseif lowerMsg == "diag" then
MTH_CommandDiag()
MTH:Print("Available: /mth options, /mth book")
elseif lowerMsg == "err" or lowerMsg == "errors" or lowerMsg == "debug" then
if MTH_DebugFrame and MTH_DebugFrame.Toggle then
MTH_DebugFrame:Toggle()
else
MTH:Print("Debug frame is not available.")
end
elseif lowerMsg == "options" then
MTH_CommandOptions()
elseif lowerMsg == "book" or lowerMsg == "hunterbook" then
MTH_CommandBook()
elseif lowerMsg == "peers" or lowerMsg == "who" then
MTH_CommandPeers()
elseif lowerMsg == "zoneinfo" then
-- Dumps all available zone data for the current zone, for adding to ds-zones / ds-minimap-sizes
pcall(SetMapToCurrentZone)
local zoneName = (type(GetRealZoneText) == "function" and GetRealZoneText()) or ""
local subZone = (type(GetSubZoneText) == "function" and GetSubZoneText()) or ""
local zoneText = (type(GetZoneText) == "function" and GetZoneText()) or ""
local areaId = nil
if type(GetCurrentMapAreaID) == "function" then
local ok, v = pcall(GetCurrentMapAreaID)
if ok then areaId = v end
end
local cx, cy = nil, nil
if type(GetPlayerMapPosition) == "function" then
local ok, x, y = pcall(GetPlayerMapPosition, "player")
if ok then cx, cy = x, y end
end
local contId = nil
if type(GetCurrentMapContinent) == "function" then
local ok, v = pcall(GetCurrentMapContinent)
if ok then contId = v end
end
MTH:Print("|cffffff00=== Zone Info ===|r")
MTH:Print("GetRealZoneText: |cffffaa00" .. tostring(zoneName) .. "|r")
MTH:Print("GetZoneText: |cffffaa00" .. tostring(zoneText) .. "|r")
MTH:Print("GetSubZoneText: |cffffaa00" .. tostring(subZone) .. "|r")
MTH:Print("AreaID (GetCurrentMapAreaID): |cff88ffff" .. tostring(areaId) .. "|r")
MTH:Print("ContinentID: |cff88ffff" .. tostring(contId) .. "|r")
if cx and cy then
MTH:Print(string.format("Player map pos: |cff88ffff%.4f, %.4f|r (display: %.1f, %.1f)", cx, cy, cx*100, cy*100))
end
-- Also check if zone is in our tables
local inZones = MTH_DS_Zones and MTH_DS_Zones[tonumber(areaId)] and "YES" or "not in MTH_DS_Zones"
local inSizes = MTH_DS_MinimapSizes and MTH_DS_MinimapSizes[tonumber(areaId)] and "YES" or "not in ds-minimap-sizes"
MTH:Print("MTH_DS_Zones entry: " .. inZones)
MTH:Print("MTH_DS_MinimapSizes entry: " .. inSizes)
elseif lowerMsg == "npcid" then
-- Print the creature entry ID of the current target, or arm a listener for next NPC interaction
local function MTH_ParseCreatureEntryFromAnyGuid(guid)
if type(guid) ~= "string" or guid == "" then return nil end
-- Format 1: TBC-style "Creature-realm-map-inst-?-ENTRY-spawn"
local _, _, id1 = string.find(guid, "^Creature%-%d+%-%d+%-%d+%-%d+%-(%d+)%-%d+$")
if id1 then return tonumber(id1) end
-- Format 2: vanilla hex "0xF130EEEEEECCCCCC" — entry is bits 24-47 = hex chars 7-12
if string.sub(guid, 1, 2) == "0x" and string.len(guid) >= 14 then
local entryHex = string.sub(guid, 7, 12) -- 24 bits = up to 16M
local parsed = tonumber(entryHex, 16)
if parsed and parsed > 0 then return parsed end
end
return nil
end
local function MTH_PrintNpcId(unitToken)
unitToken = unitToken or "target"
local name = (type(UnitName) == "function") and UnitName(unitToken) or "?"
local level = (type(UnitLevel) == "function") and UnitLevel(unitToken) or "?"
-- Try UnitGUID first, then GetUnitGUID (TurtleWoW nameplate API)
local guid = nil
if type(UnitGUID) == "function" then
local ok, g = pcall(UnitGUID, unitToken)
if ok and g and g ~= "" then guid = g end
end
if (not guid or guid == "") and type(GetUnitGUID) == "function" then
local ok, g = pcall(GetUnitGUID, unitToken)
if ok and g and g ~= "" then guid = g end
end
local creatureId = MTH_ParseCreatureEntryFromAnyGuid(guid)
if creatureId then
MTH:Print(string.format("|cffffff00NPC:|r %s |cffffff00ID:|r |cff88ffff%s|r lvl: %s", tostring(name), tostring(creatureId), tostring(level)))
else
MTH:Print(string.format("|cffffff00NPC:|r %s lvl: %s |cffff8800GUID: %s|r", tostring(name), tostring(level), tostring(guid or "nil")))
MTH:Print("|cffaaaaaa(Could not parse creature ID from GUID)|r")
end
end
-- Try current target first
local hasTarget = (type(UnitExists) == "function") and UnitExists("target")
if hasTarget then
MTH_PrintNpcId("target")
end
-- Arm a one-shot frame for MERCHANT_SHOW / TRAINER_LIST_UPDATE / GOSSIP_SHOW
if not _G["MTH_NpcIdListenerFrame"] then
local f = CreateFrame("Frame", "MTH_NpcIdListenerFrame")
f:RegisterEvent("MERCHANT_SHOW")
f:RegisterEvent("TRAINER_LIST_UPDATE")
f:RegisterEvent("GOSSIP_SHOW")
f:SetScript("OnEvent", function()
MTH_PrintNpcId("target")
this:UnregisterEvent("MERCHANT_SHOW")
this:UnregisterEvent("TRAINER_LIST_UPDATE")
this:UnregisterEvent("GOSSIP_SHOW")
_G["MTH_NpcIdListenerFrame"] = nil
end)
if not hasTarget then
MTH:Print("|cffaaffaaArmed.|r Open any vendor, trainer, or stable master to capture their ID.")
end
end
elseif lowerCmd == "bls" then
if lowerArg == "reset" then
if type(MTH_SavedVariables) == "table" then
MTH_SavedVariables.beastLoreScan = { entries = {} }
end
MTH:Print("|cffff4444Beast Lore scan data wiped.|r All recorded beasts cleared.")
else
MTH:Print("Beast Lore Scan: |cffffff00/mth bls reset|r — wipe all recorded beast data")
end
else
MTH:Print("Unknown command: " .. tostring(msg))
MTH:Print("Available: /mth options, /mth book, /mth diag, /mth err")
MTH:Print("Available: /mth options, /mth book, /mth npcid, /mth bls reset")
end
end
+4 -30
View File
@@ -6,18 +6,6 @@
-- Uses MTH_DS_BeastSkins[familyName] generated by .tools/generators/gen_beast_skins.py
-- Skin rendering: PlayerModel:SetUnit(CreatureDisplayInfo.ID)
local function mv_log(msg)
if MTH_DebugFrame and type(MTH_DebugFrame.AddInfo) == "function" then
MTH_DebugFrame:AddInfo("[MV] " .. tostring(msg))
end
end
local function mv_err(msg)
if MTH_DebugFrame and type(MTH_DebugFrame.AddError) == "function" then
MTH_DebugFrame:AddError("[MV] " .. tostring(msg))
end
end
local MTH_MV = {
inited = false,
family = nil,
@@ -45,10 +33,8 @@ local MTH_MV = {
local function MTH_MV_ApplyModel(model, skinId)
if not model or not skinId then
mv_err("ApplyModel: nil model or skinId model="..tostring(model).." id="..tostring(skinId))
return
end
mv_log("ApplyModel id="..tostring(skinId))
model:SetUnit(skinId)
model:SetPosition(0, 0, 0)
model:SetModelScale(1)
@@ -106,14 +92,11 @@ MTH_MV_BuildSkinBeastList = function(skinId)
end
local function MTH_MV_Init()
if MTH_MV.inited then mv_log("Init: already done") return end
mv_log("Init: starting")
if MTH_MV.inited then return end
local backdrop = getglobal("MTH_BOOK_DetailBackdrop")
if not backdrop then
mv_err("Init: MTH_BOOK_DetailBackdrop not found")
return
end
mv_log("Init: backdrop found, creating previewFrame")
MTH_MV.inited = true
-- ----------------------------------------------------------------
@@ -136,7 +119,6 @@ local function MTH_MV_Init()
MTH_MV.previewFrame:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.8)
MTH_MV.previewFrame:Hide()
mv_log("Init: previewFrame created, creating PlayerModel")
-- PlayerModel fills the inner area
MTH_MV.previewModel = CreateFrame("PlayerModel", nil, MTH_MV.previewFrame)
MTH_MV.previewModel:SetPoint("TOPLEFT", MTH_MV.previewFrame, "TOPLEFT", 3, -3)
@@ -158,7 +140,6 @@ local function MTH_MV_Init()
GameTooltip:Hide()
end)
mv_log("Init: previewModel created, creating popup")
-- ----------------------------------------------------------------
-- Popup frame (wider: left side = model viewer, right = beast list)
-- ----------------------------------------------------------------
@@ -343,7 +324,6 @@ local function MTH_MV_Init()
end
MTH_MV.popup = popup
mv_log("Init: complete")
end
-- ---------------------------------------------------------------------------
@@ -351,12 +331,10 @@ end
-- ---------------------------------------------------------------------------
function MTH_BOOK_ModelViewer_SetFamily(family, skinId)
mv_log("SetFamily: "..tostring(family).." skinId="..tostring(skinId))
MTH_MV_Init()
if not MTH_MV.inited then mv_err("SetFamily: init failed") return end
if not MTH_MV.inited then return end
local skins = MTH_DS_BeastSkins and type(MTH_DS_BeastSkins) == "table" and MTH_DS_BeastSkins[family]
if not skins or table.getn(skins) == 0 then
mv_log("SetFamily: no skins for "..tostring(family).." DS type="..type(MTH_DS_BeastSkins or false))
MTH_BOOK_ModelViewer_Clear()
return
end
@@ -380,7 +358,6 @@ function MTH_BOOK_ModelViewer_SetFamily(family, skinId)
MTH_BOOK_ModelViewer_Clear()
return
end
mv_log("SetFamily: "..table.getn(filteredSkins).." tameable skins (filtered from "..table.getn(skins)..")")
MTH_MV.family = family
MTH_MV.skins = filteredSkins
@@ -392,7 +369,6 @@ function MTH_BOOK_ModelViewer_SetFamily(family, skinId)
for i = 1, table.getn(filteredSkins) do
if filteredSkins[i].id == skinId then
MTH_MV.index = i
mv_log("SetFamily: skinId "..skinId.." matched skin index "..i)
break
end
end
@@ -409,7 +385,6 @@ function MTH_BOOK_ModelViewer_SetFamily(family, skinId)
end
function MTH_BOOK_ModelViewer_Clear()
mv_log("Clear")
if MTH_MV.previewFrame then MTH_MV.previewFrame:Hide() end
if not MTH_MV.keepPopup then
if MTH_MV.popup and MTH_MV.popup:IsShown() then MTH_MV.popup:Hide() end
@@ -420,10 +395,9 @@ function MTH_BOOK_ModelViewer_Clear()
end
function MTH_BOOK_ModelViewer_OpenPopup()
mv_log("OpenPopup skins="..table.getn(MTH_MV.skins))
MTH_MV_Init()
if not MTH_MV.popup then mv_err("OpenPopup: no popup frame") return end
if table.getn(MTH_MV.skins) == 0 then mv_log("OpenPopup: no skins") return end
if not MTH_MV.popup then return end
if table.getn(MTH_MV.skins) == 0 then return end
MTH_MV.popup:Show()
MTH_MV.popup:Raise()
MTH_MV_DisplayCurrent()
+98 -8
View File
@@ -29,13 +29,31 @@ function MTH_BOOK_GetBeastZoneSummary(beast)
return "-"
end
local seen = {}
local zoneNames = {}
-- 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] and not seen[c[3]] then
seen[c[3]] = true
table.insert(zoneNames, MTH_BOOK_GetZoneName(c[3]))
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
@@ -68,6 +86,69 @@ function MTH_BOOK_SplitAbilities(text)
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)
@@ -232,8 +313,11 @@ function MTH_BOOK_BeastMatches(beastId, beast)
end
if MTH_BOOK_STATE.petInZoneOnly then
local currentZoneId = MTH_BOOK_GetCurrentZoneId()
if currentZoneId and not MTH_BOOK_BeastHasZoneId(beast, currentZoneId) 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
@@ -251,6 +335,12 @@ function MTH_BOOK_BeastMatches(beastId, beast)
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
@@ -258,7 +348,7 @@ 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 a < b end
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
+345 -50
View File
@@ -69,6 +69,8 @@ local MTH_BOOK_STATE = {
petOnlyMyLevel = true,
itemOnlyMyLevel = false,
petInZoneOnly = false,
petContinent = "all",
petZoneFilter = "all",
itemSubtype = "all",
npcFunction = "all",
npcZone = "all",
@@ -274,7 +276,7 @@ local MTH_BOOK_CONTENT_LAYOUT_FAMILIES = {
listX = 16,
listY = -74,
listW = 728,
listH = 376,
listH = 456,
detailX = 748,
detailY = -74,
detailW = 1,
@@ -364,7 +366,7 @@ local function MTH_BOOK_UpdateCheckboxLayoutByMode()
getglobal("MTH_BOOK_HideNoAbilities"),
getglobal("MTH_BOOK_HideUnknown"),
}
local xOffsets = { 18, 86, 154, 232, 372 }
local xOffsets = { 18, 86, 154, 232, 345 }
local yOffset = -140
if MTH_BOOK_STATE.mode == "npcs" then
yOffset = -116
@@ -754,12 +756,15 @@ local function MTH_BOOK_ShowAllFilteredBeastsOnMap()
end
local nodes = {}
local firstZoneId = nil
for i = 1, table.getn(MTH_BOOK_STATE.results or {}) do
local beastId = tonumber(MTH_BOOK_STATE.results[i])
local beast = beastId and MTH_DS_Beasts and MTH_DS_Beasts[beastId]
if beast and beast.coords then
local beastDisplayName = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(beastId, beast.name)) or beast.name or "Unknown"
local cr, cg, cb = 0.95, 0.82, 0.10
if MTH_Map and MTH_Map.GetFamilyColor then
cr, cg, cb = MTH_Map:GetFamilyColor(beast.family)
end
for j = 1, table.getn(beast.coords) do
local c = beast.coords[j]
if c and c[1] and c[2] and c[3] then
@@ -767,9 +772,6 @@ local function MTH_BOOK_ShowAllFilteredBeastsOnMap()
local y = tonumber(c[2])
local zoneId = tonumber(c[3])
if x and y and zoneId then
if not firstZoneId then
firstZoneId = zoneId
end
local details = string.format("Family: %s\nLevel: %s\nAbilities: %s", beast.family or "?", beast.lvl or "?", beast.abilities or "None")
table.insert(nodes, {
zoneId = zoneId,
@@ -778,7 +780,7 @@ local function MTH_BOOK_ShowAllFilteredBeastsOnMap()
title = string.format("%s (%d)", beastDisplayName, beastId),
detail = details,
beastId = beastId,
color = { 0.95, 0.82, 0.10 },
color = { cr, cg, cb },
})
end
end
@@ -794,8 +796,11 @@ local function MTH_BOOK_ShowAllFilteredBeastsOnMap()
if not MTH_Map:SetSource("focus") then
return
end
if firstZoneId and MTH_Map.OpenWorldMapForZone then
MTH_Map:OpenWorldMapForZone(firstZoneId)
-- Navigate to the player's current (filter) zone, not the first coord's zone ID
-- which may point to a different zone due to multi-zone beast spawn data.
local navZoneId = MTH_BOOK_STATE._cachedZoneId
if navZoneId and MTH_Map.OpenWorldMapForZone then
MTH_Map:OpenWorldMapForZone(navZoneId)
end
MTH_Map:UpdateWorldMap()
MTH_Map:UpdateMinimap()
@@ -1076,11 +1081,17 @@ end
MTH_BOOK_GetZoneName = function(zoneId)
local normalizedId = tonumber(zoneId) or zoneId
if not normalizedId then return "Zone " .. tostring(zoneId or "?") end
if MTH_DS_ZoneNamesFallback and MTH_DS_ZoneNamesFallback[normalizedId] then
return MTH_DS_ZoneNamesFallback[normalizedId]
-- Beast coords use WMA IDs. Always translate WMA→pfQ first so that e.g.
-- WMA 4 → pfQ 14 (Durotar) instead of hitting pfQ 4 (Blasted Lands) by accident.
local pfqId = normalizedId
if type(MTH_MAP_WMA_TO_PFQ) == "table" then
pfqId = MTH_MAP_WMA_TO_PFQ[normalizedId] or normalizedId
end
if not MTH_DS_Zones then return "Zone " .. tostring(zoneId or "?") end
local row = MTH_DS_Zones[normalizedId] or MTH_DS_Zones[tostring(normalizedId)]
if MTH_DS_ZoneNamesFallback and MTH_DS_ZoneNamesFallback[pfqId] then
return MTH_DS_ZoneNamesFallback[pfqId]
end
if not MTH_DS_Zones then return "Zone " .. tostring(normalizedId) end
local row = MTH_DS_Zones[pfqId] or MTH_DS_Zones[tostring(pfqId)]
if not row then return "Zone " .. tostring(normalizedId) end
local names = row.names
if not names then return "Zone " .. tostring(normalizedId) end
@@ -1109,7 +1120,12 @@ function MTH_BOOK_GetCurrentZoneId()
if MTH_Map and type(MTH_Map.GetMapIDByName) == "function" then
local mapId = MTH_Map:GetMapIDByName(zoneName)
if mapId then
return tonumber(mapId) or mapId
-- Translate pfQ → WMA so zone IDs match coord tuples in MTH_DS_Beasts.
local pfqId = tonumber(mapId)
if pfqId and type(MTH_MAP_PFQ_TO_WMA) == "table" then
return MTH_MAP_PFQ_TO_WMA[pfqId] or pfqId
end
return pfqId or mapId
end
end
@@ -1123,7 +1139,11 @@ function MTH_BOOK_GetCurrentZoneId()
if row and row.names then
for _, value in pairs(row.names) do
if MTH_BOOK_NormalizeZoneLookupName(value) == normalized then
return tonumber(zoneId) or zoneId
local pfqId = tonumber(zoneId) or zoneId
if type(pfqId) == "number" and type(MTH_MAP_PFQ_TO_WMA) == "table" then
return MTH_MAP_PFQ_TO_WMA[pfqId] or pfqId
end
return pfqId
end
end
end
@@ -1432,12 +1452,25 @@ local function MTH_BOOK_BuildResults()
local results = {}
if MTH_BOOK_STATE.mode == "pets" then
if not MTH_DS_Beasts then return results end
for beastId, beast in pairs(MTH_DS_Beasts) do
if MTH_BOOK_BeastMatches(beastId, beast) then
table.insert(results, beastId)
-- Cache zone info once for this build pass (avoids calling GetCurrentZoneId
-- once per beast, which is expensive and causes freezes on large datasets).
if MTH_BOOK_STATE.petInZoneOnly then
MTH_BOOK_STATE._cachedZoneId = MTH_BOOK_GetCurrentZoneId()
MTH_BOOK_STATE._cachedZoneName = (type(GetRealZoneText) == "function" and GetRealZoneText())
or (type(GetZoneText) == "function" and GetZoneText()) or nil
else
MTH_BOOK_STATE._cachedZoneId = nil
MTH_BOOK_STATE._cachedZoneName = nil
end
if MTH_DS_Beasts then
for beastId, beast in pairs(MTH_DS_Beasts) do
if MTH_BOOK_BeastMatches(beastId, beast) then
table.insert(results, beastId)
end
end
end
table.sort(results, MTH_BOOK_BeastSort)
MTH_BOOK_ApplyActiveSort(results)
return results
@@ -2054,6 +2087,77 @@ local function MTH_BOOK_UpdatePetLearnSourceDropdownText()
MTH_BOOK_DropdownSetText(sourceDropdown, MTH_BOOK_GetPetLearnSourceLabel(MTH_BOOK_STATE.petLearnSource))
end
local MTH_BOOK_BeastContinentOptions = {
{ value = "all", label = "" },
{ value = 1, label = "Kalimdor" },
{ value = 0, label = "E. Kingdoms" },
}
local MTH_BOOK_BeastZoneOptions = {
{ value = "all", label = "All Zones" },
}
local function MTH_BOOK_BuildBeastZoneOptions(continentMapId)
local seen = {}
local rows = {}
if MTH_DS_Beasts and MTH_DS_WMAZones then
for _, beast in pairs(MTH_DS_Beasts) do
if beast and beast.coords then
for i = 1, table.getn(beast.coords) do
local c = beast.coords[i]
if c and c[3] and not seen[c[3]] then
local wmaEntry = MTH_DS_WMAZones[c[3]]
if wmaEntry and wmaEntry.mapId == continentMapId then
seen[c[3]] = true
local zoneName = MTH_BOOK_GetZoneName and MTH_BOOK_GetZoneName(c[3])
if zoneName and string.sub(zoneName, 1, 5) ~= "Zone " then
table.insert(rows, { value = c[3], label = zoneName })
end
end
end
end
end
end
end
table.sort(rows, function(a, b)
return MTH_BOOK_SafeLower(a.label) < MTH_BOOK_SafeLower(b.label)
end)
MTH_BOOK_BeastZoneOptions = { { value = "all", label = "All Zones" } }
for i = 1, table.getn(rows) do
table.insert(MTH_BOOK_BeastZoneOptions, rows[i])
end
end
local function MTH_BOOK_GetBeastContinentLabel(value)
for i = 1, table.getn(MTH_BOOK_BeastContinentOptions) do
if MTH_BOOK_BeastContinentOptions[i].value == value then
return MTH_BOOK_BeastContinentOptions[i].label
end
end
return ""
end
local function MTH_BOOK_GetBeastZoneLabel(value)
for i = 1, table.getn(MTH_BOOK_BeastZoneOptions) do
if MTH_BOOK_BeastZoneOptions[i].value == value then
return MTH_BOOK_BeastZoneOptions[i].label
end
end
return "All Zones"
end
local function MTH_BOOK_UpdateBeastContinentDropdownText()
local dd = getglobal("MTH_BOOK_BeastContinentDropdown")
if not dd then return end
MTH_BOOK_DropdownSetText(dd, MTH_BOOK_GetBeastContinentLabel(MTH_BOOK_STATE.petContinent))
end
local function MTH_BOOK_UpdateBeastZoneDropdownText()
local dd = getglobal("MTH_BOOK_BeastZoneDropdown")
if not dd then return end
MTH_BOOK_DropdownSetText(dd, MTH_BOOK_GetBeastZoneLabel(MTH_BOOK_STATE.petZoneFilter))
end
local function MTH_BOOK_GetItemSubtypeLabelByValue(value)
local options = MTH_BOOK_GetItemSubtypeOptions()
for i = 1, table.getn(options) do
@@ -2089,7 +2193,7 @@ local function MTH_BOOK_UpdateQuickFilterControls()
if MTH_BOOK_IsItemMode() then
MTH_BOOK_STATE.pageSize = 18
elseif MTH_BOOK_STATE.mode == "families" then
MTH_BOOK_STATE.pageSize = 16
MTH_BOOK_STATE.pageSize = MTH_BOOK_MAX_ROWS
elseif MTH_BOOK_STATE.mode == "stable" then
MTH_BOOK_STATE.pageSize = 5
elseif MTH_BOOK_STATE.mode == "pethistory" then
@@ -2102,6 +2206,9 @@ local function MTH_BOOK_UpdateQuickFilterControls()
local abilityDropdown = getglobal("MTH_BOOK_AbilityDropdown")
local rankDropdown = getglobal("MTH_BOOK_RankDropdown")
local petLearnSourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown")
local beastContinentDropdown = getglobal("MTH_BOOK_BeastContinentDropdown")
local beastZoneDropdown = getglobal("MTH_BOOK_BeastZoneDropdown")
local beastZoneLabel = getglobal("MTH_BOOK_BeastZoneLabel")
local itemSubtypeDropdown = getglobal("MTH_BOOK_ItemSubtypeDropdown")
local npcFunctionDropdown = getglobal("MTH_BOOK_NPCFunctionDropdown")
local npcZoneDropdown = getglobal("MTH_BOOK_NPCZoneDropdown")
@@ -2222,6 +2329,19 @@ local function MTH_BOOK_UpdateQuickFilterControls()
end
end
if petLearnSourceDropdown then petLearnSourceDropdown:Hide() end
if beastZoneLabel then beastZoneLabel:Show() end
if beastContinentDropdown then
beastContinentDropdown:Show()
MTH_BOOK_UpdateBeastContinentDropdownText()
end
if beastZoneDropdown then
if MTH_BOOK_STATE.petContinent ~= "all" then
beastZoneDropdown:Show()
MTH_BOOK_UpdateBeastZoneDropdownText()
else
beastZoneDropdown:Hide()
end
end
if itemSubtypeDropdown then itemSubtypeDropdown:Hide() end
if npcFunctionDropdown then npcFunctionDropdown:Hide() end
if npcZoneDropdown then npcZoneDropdown:Hide() end
@@ -2257,6 +2377,9 @@ local function MTH_BOOK_UpdateQuickFilterControls()
petLearnSourceDropdown:Show()
MTH_BOOK_UpdatePetLearnSourceDropdownText()
end
if beastZoneLabel then beastZoneLabel:Hide() end
if beastContinentDropdown then beastContinentDropdown:Hide() end
if beastZoneDropdown then beastZoneDropdown:Hide() end
if itemSubtypeDropdown then itemSubtypeDropdown:Hide() end
if petOnlyMyLevel then
petOnlyMyLevel:ClearAllPoints()
@@ -2313,6 +2436,9 @@ local function MTH_BOOK_UpdateQuickFilterControls()
if abilityDropdown then abilityDropdown:Hide() end
if rankDropdown then rankDropdown:Hide() end
if petLearnSourceDropdown then petLearnSourceDropdown:Hide() end
if beastZoneLabel then beastZoneLabel:Hide() end
if beastContinentDropdown then beastContinentDropdown:Hide() end
if beastZoneDropdown then beastZoneDropdown:Hide() end
if itemSubtypeDropdown then itemSubtypeDropdown:Hide() end
if npcFunctionDropdown then npcFunctionDropdown:Show() end
if npcZoneDropdown then npcZoneDropdown:Hide() end
@@ -2367,6 +2493,9 @@ local function MTH_BOOK_UpdateQuickFilterControls()
if abilityDropdown then abilityDropdown:Hide() end
if rankDropdown then rankDropdown:Hide() end
if petLearnSourceDropdown then petLearnSourceDropdown:Hide() end
if beastZoneLabel then beastZoneLabel:Hide() end
if beastContinentDropdown then beastContinentDropdown:Hide() end
if beastZoneDropdown then beastZoneDropdown:Hide() end
if itemSubtypeDropdown then
itemSubtypeDropdown:ClearAllPoints()
itemSubtypeDropdown:SetPoint("TOPLEFT", itemSubtypeDropdown:GetParent(), "TOPLEFT", 238, -80)
@@ -2504,6 +2633,15 @@ local function MTH_BOOK_UpdateQuickFilterControls()
if MTH_BOOK_STATE.mode ~= "pets" and petInZoneOnly then
petInZoneOnly:Hide()
end
if MTH_BOOK_STATE.mode ~= "pets" and beastContinentDropdown then
beastContinentDropdown:Hide()
end
if MTH_BOOK_STATE.mode ~= "pets" and beastZoneDropdown then
beastZoneDropdown:Hide()
end
if MTH_BOOK_STATE.mode ~= "pets" and beastZoneLabel then
beastZoneLabel:Hide()
end
if MTH_BOOK_STATE.mode ~= "npcs" and npcInZoneOnly then
npcInZoneOnly:Hide()
end
@@ -2609,6 +2747,7 @@ local function MTH_BOOK_InitRankDropdown()
MTH_BOOK_UpdateRankDropdownText()
end
local function MTH_BOOK_InitPetLearnSourceDropdown()
local sourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown")
if not sourceDropdown then return end
@@ -2703,6 +2842,56 @@ local function MTH_BOOK_AddStableStyleKV(lines, label, value)
table.insert(lines, "|cff9a9a9a" .. tostring(label or "") .. ":|r |cffffffff" .. tostring(value or "-") .. "|r")
end
local function MTH_BOOK_GetOrCreateDetailTitleLabel()
local lbl = getglobal("MTH_BOOK_DetailTitleLabel")
if lbl then return lbl end
local parent = getglobal("MTH_BOOK_DetailBackdrop")
if not parent then return nil end
lbl = parent:CreateFontString("MTH_BOOK_DetailTitleLabel", "OVERLAY")
lbl:SetFontObject(GameFontNormal)
lbl:SetJustifyH("LEFT")
lbl:SetJustifyV("TOP")
lbl:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, -8)
lbl:SetWidth(120)
lbl:Hide()
return lbl
end
local function MTH_BOOK_ShowDetailTitle(text)
local lbl = MTH_BOOK_GetOrCreateDetailTitleLabel()
if lbl then
lbl:SetText(tostring(text or ""))
lbl:Show()
end
local editBox = getglobal("MTH_BOOK_DetailBackdropDetailText")
local parent = getglobal("MTH_BOOK_DetailBackdrop")
if editBox and parent then
editBox:ClearAllPoints()
editBox:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, -28)
end
end
local function MTH_BOOK_HideDetailTitle()
local lbl = getglobal("MTH_BOOK_DetailTitleLabel")
if lbl then lbl:Hide() end
local editBox = getglobal("MTH_BOOK_DetailBackdropDetailText")
local parent = getglobal("MTH_BOOK_DetailBackdrop")
if editBox and parent then
editBox:ClearAllPoints()
editBox:SetPoint("TOPLEFT", parent, "TOPLEFT", 6, -6)
end
end
local function MTH_BOOK_FormatBeastReact(reactA, reactH)
local function reactColor(r)
local rl = r and string.lower(r) or ""
if rl == "friendly" then return "|cFF44DD44" end
if rl == "neutral" then return "|cFFFFCC00" end
return "|cFFDD4444"
end
return reactColor(reactA) .. "A|r " .. reactColor(reactH) .. "H|r"
end
local function MTH_BOOK_FormatDateTimeValue(value)
local ts = tonumber(value)
if not ts or ts <= 0 then
@@ -2943,7 +3132,7 @@ end
function MTH_BOOK_SetDetailText(detail, text)
if not detail then return end
local wrapped = tostring(text or "")
if MTH_BOOK_STATE.mode ~= "pethistory" then
if MTH_BOOK_STATE.mode ~= "pethistory" and MTH_BOOK_STATE.mode ~= "pets" and MTH_BOOK_STATE.mode ~= "abilities" then
wrapped = MTH_BOOK_WrapDetailText(wrapped)
end
detail._mthLocking = true
@@ -2996,6 +3185,7 @@ end
function MTH_BOOK_UpdateDetail()
local detail = getglobal("MTH_BOOK_DetailBackdropDetailText")
if not detail then return end
MTH_BOOK_HideDetailTitle()
-- Hide model skin preview when not in a beast-list mode
if MTH_BOOK_STATE.mode ~= "pets" and MTH_BOOK_STATE.mode ~= "abilities" then
if MTH_BOOK_ModelViewer_Clear then MTH_BOOK_ModelViewer_Clear() end
@@ -3074,28 +3264,50 @@ function MTH_BOOK_UpdateDetail()
return
end
local beastId = selected.beastId
local beast = MTH_DS_Beasts and MTH_DS_Beasts[beastId]
if not beast then
MTH_BOOK_SetDetailText(detail, "|cFFFFD100Inspector|r\n\nSelect an entry from the list to view identity, stats, sources and locations.")
MTH_BOOK_UpdateOpenMapButton()
return
end
local lines = {}
local beastDisplayName = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(beastId, beast.name)) or beast.name
table.insert(lines, "|cFFFFFF00" .. (beastDisplayName or "Unknown") .. "|r")
MTH_BOOK_AddDetailSection(lines, "Identity")
MTH_BOOK_AddDetailKV(lines, "ID", beastId)
MTH_BOOK_AddDetailKV(lines, "Family", beast.family or "?")
MTH_BOOK_ShowDetailTitle("|cFFFFFF00" .. (beastDisplayName or "Unknown") .. "|r")
MTH_BOOK_AddDetailSection(lines, "Stats")
MTH_BOOK_AddDetailKV(lines, "Level", beast.lvl or "?")
MTH_BOOK_AddDetailKV(lines, "Attack Speed", beast.attackSpeed or "?")
local lines = {}
-- Identity block
MTH_BOOK_AddStableStyleKV(lines, "ID", beastId)
MTH_BOOK_AddStableStyleKV(lines, "Family", beast.family or "?")
local rankLabel = beast.rank
if rankLabel == "normal" or rankLabel == nil or rankLabel == "" then rankLabel = nil end
if rankLabel then MTH_BOOK_AddStableStyleKV(lines, "Rank", rankLabel) end
local spawnCount = (beast.coords and table.getn(beast.coords)) or 0
MTH_BOOK_AddStableStyleKV(lines, "Spawns", spawnCount > 0 and tostring(spawnCount) or "?")
if beast.reactA or beast.reactH then
table.insert(lines, "|cff9a9a9aReaction:|r " .. MTH_BOOK_FormatBeastReact(beast.reactA, beast.reactH))
end
-- Stats block
MTH_BOOK_AddStableStyleKV(lines, "Level", beast.lvl or "?")
MTH_BOOK_AddStableStyleKV(lines, "Atk Speed", beast.attackSpeed or "?")
local respawnWindow = MTH_BOOK_FormatRespawnWindow(beast)
if respawnWindow then
MTH_BOOK_AddDetailKV(lines, "Respawn", respawnWindow)
if respawnWindow then MTH_BOOK_AddStableStyleKV(lines, "Respawn", respawnWindow) end
if beast.health then MTH_BOOK_AddStableStyleKV(lines, "Health", beast.health) end
if beast.armor then MTH_BOOK_AddStableStyleKV(lines, "Armor", beast.armor) end
if beast.dmgMin and beast.dmgMax then
MTH_BOOK_AddStableStyleKV(lines, "Damage", string.format("%.1f - %.1f", tonumber(beast.dmgMin) or 0, tonumber(beast.dmgMax) or 0))
end
if beast.abilities and beast.abilities ~= "" and beast.abilities ~= "None" then
MTH_BOOK_AddStableStyleKV(lines, "Abilities", beast.abilities)
end
local beastZone = MTH_BOOK_GetBeastZoneSummary and MTH_BOOK_GetBeastZoneSummary(beast)
if beastZone and beastZone ~= "-" then
MTH_BOOK_AddStableStyleKV(lines, "Zone", beastZone)
end
local subzone = MTH_BOOK_GetBeastSubZoneSummary and MTH_BOOK_GetBeastSubZoneSummary(beast)
if subzone then
MTH_BOOK_AddStableStyleKV(lines, "Subzone", subzone)
end
MTH_BOOK_AddDetailKV(lines, "Rare", beast.rare and "Yes" or "No")
MTH_BOOK_AddDetailKV(lines, "Abilities", beast.abilities or "None")
if MTH_BOOK_ModelViewer_SetFamily then MTH_BOOK_ModelViewer_SetFamily(beast.family, beast.skinId) end
MTH_BOOK_SetDetailText(detail, table.concat(lines, "\n"))
@@ -3116,22 +3328,36 @@ function MTH_BOOK_UpdateDetail()
MTH_BOOK_UpdateOpenMapButton()
return
end
MTH_BOOK_ShowDetailTitle("|cFFFFFF00" .. tostring(entry.abilityToken) .. "|r")
local lines = {}
table.insert(lines, "|cFFFFFF00" .. tostring(entry.abilityToken) .. "|r")
local beastDisplayName = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(entry.beastId, beast.name)) or beast.name
MTH_BOOK_AddStableStyleKV(lines, "Beast", tostring(beastDisplayName or "Unknown") .. " (#" .. tostring(entry.beastId) .. ")")
MTH_BOOK_AddStableStyleKV(lines, "Family", beast.family or "?")
local rankLabel = beast.rank
if rankLabel == "normal" or rankLabel == nil or rankLabel == "" then rankLabel = nil end
if rankLabel then MTH_BOOK_AddStableStyleKV(lines, "Rank", rankLabel) end
MTH_BOOK_AddDetailSection(lines, "Identity")
MTH_BOOK_AddDetailKV(lines, "Beast", tostring(beastDisplayName or "Unknown") .. " (ID " .. tostring(entry.beastId) .. ")")
MTH_BOOK_AddDetailKV(lines, "Family", beast.family or "?")
MTH_BOOK_AddDetailSection(lines, "Stats")
MTH_BOOK_AddDetailKV(lines, "Level", beast.lvl or "?")
MTH_BOOK_AddDetailKV(lines, "Attack Speed", beast.attackSpeed or "?")
MTH_BOOK_AddStableStyleKV(lines, "Level", beast.lvl or "?")
MTH_BOOK_AddStableStyleKV(lines, "Atk Speed", beast.attackSpeed or "?")
local respawnWindow = MTH_BOOK_FormatRespawnWindow(beast)
if respawnWindow then
MTH_BOOK_AddDetailKV(lines, "Respawn", respawnWindow)
if respawnWindow then MTH_BOOK_AddStableStyleKV(lines, "Respawn", respawnWindow) end
if beast.health then MTH_BOOK_AddStableStyleKV(lines, "Health", beast.health) end
if beast.armor then MTH_BOOK_AddStableStyleKV(lines, "Armor", beast.armor) end
if beast.dmgMin and beast.dmgMax then
MTH_BOOK_AddStableStyleKV(lines, "Damage", string.format("%.1f - %.1f", tonumber(beast.dmgMin) or 0, tonumber(beast.dmgMax) or 0))
end
if beast.reactA or beast.reactH then
table.insert(lines, "|cff9a9a9aReaction:|r " .. MTH_BOOK_FormatBeastReact(beast.reactA, beast.reactH))
end
local beastZone = MTH_BOOK_GetBeastZoneSummary and MTH_BOOK_GetBeastZoneSummary(beast)
if beastZone and beastZone ~= "-" then
MTH_BOOK_AddStableStyleKV(lines, "Zone", beastZone)
end
local subzone = MTH_BOOK_GetBeastSubZoneSummary and MTH_BOOK_GetBeastSubZoneSummary(beast)
if subzone then
MTH_BOOK_AddStableStyleKV(lines, "Subzone", subzone)
end
MTH_BOOK_AddDetailKV(lines, "Rare", beast.rare and "Yes" or "No")
if MTH_BOOK_ModelViewer_SetFamily then MTH_BOOK_ModelViewer_SetFamily(beast.family, beast.skinId) end
MTH_BOOK_SetDetailText(detail, table.concat(lines, "\n"))
@@ -3659,6 +3885,7 @@ end
local function MTH_BOOK_GetRowValues(entry)
if MTH_BOOK_STATE.mode == "pets" then
-- MetaHunt DB entry
local beast = MTH_DS_Beasts and MTH_DS_Beasts[entry]
if not beast then return { "", "", "", "", "", "", "", "", "", "" } end
local beastDisplayName = (MTH and MTH.GetLocalizedBeastName and MTH:GetLocalizedBeastName(entry, beast.name)) or beast.name
@@ -4507,6 +4734,8 @@ local function MTH_BOOK_GetFilterWidgets()
npcFunctionDropdown = getglobal("MTH_BOOK_NPCFunctionDropdown"),
npcZoneDropdown = getglobal("MTH_BOOK_NPCZoneDropdown"),
petLearnSourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown"),
beastContinentDropdown = getglobal("MTH_BOOK_BeastContinentDropdown"),
beastZoneDropdown = getglobal("MTH_BOOK_BeastZoneDropdown"),
itemSubtypeDropdown = getglobal("MTH_BOOK_ItemSubtypeDropdown"),
petOnlyMyLevel = getglobal("MTH_BOOK_PetOnlyMyLevel"),
petInZoneOnly = getglobal("MTH_BOOK_PetInZoneOnly"),
@@ -4528,6 +4757,8 @@ local function MTH_BOOK_ResetStateForMode(mode)
MTH_BOOK_STATE.petOnlyMyLevel = false
MTH_BOOK_STATE.itemOnlyMyLevel = false
MTH_BOOK_STATE.petInZoneOnly = false
MTH_BOOK_STATE.petContinent = "all"
MTH_BOOK_STATE.petZoneFilter = "all"
MTH_BOOK_STATE.itemSubtype = "all"
MTH_BOOK_STATE.npcFunction = "all"
MTH_BOOK_STATE.npcZone = "all"
@@ -4591,6 +4822,8 @@ local function MTH_BOOK_ApplyStateToWidgets(widgets)
if widgets.petInZoneOnly then
widgets.petInZoneOnly:SetChecked(MTH_BOOK_STATE.mode == "pets" and MTH_BOOK_STATE.petInZoneOnly and 1 or nil)
end
if widgets.beastContinentDropdown then MTH_BOOK_UpdateBeastContinentDropdownText() end
if widgets.beastZoneDropdown then MTH_BOOK_UpdateBeastZoneDropdownText() end
if widgets.npcInZoneOnly then
widgets.npcInZoneOnly:SetChecked(MTH_BOOK_STATE.mode == "npcs" and MTH_BOOK_STATE.npcInZoneOnly and 1 or nil)
end
@@ -4840,6 +5073,8 @@ local function MTH_BOOK_ResetAllFiltersAndRefresh()
if widgets.abilityDropdown then MTH_BOOK_UpdateAbilityDropdownText() end
if widgets.rankDropdown then MTH_BOOK_UpdateRankDropdownText() end
if widgets.petLearnSourceDropdown then MTH_BOOK_UpdatePetLearnSourceDropdownText() end
if widgets.beastContinentDropdown then MTH_BOOK_UpdateBeastContinentDropdownText() end
if widgets.beastZoneDropdown then MTH_BOOK_UpdateBeastZoneDropdownText() end
if widgets.itemSubtypeDropdown then MTH_BOOK_UpdateItemSubtypeDropdownText() end
if widgets.npcFunctionDropdown or widgets.npcZoneDropdown then MTH_BOOK_UpdateNPCDropdownTexts() end
@@ -4847,6 +5082,71 @@ local function MTH_BOOK_ResetAllFiltersAndRefresh()
MTH_BOOK_RefreshFilter()
end
local function MTH_BOOK_WireBeastDropdowns()
local continentDD = getglobal("MTH_BOOK_BeastContinentDropdown")
if continentDD then
UIDropDownMenu_Initialize(continentDD, function()
for i = 1, table.getn(MTH_BOOK_BeastContinentOptions) do
local option = MTH_BOOK_BeastContinentOptions[i]
local info = MTH_BOOK_DropdownCreateInfo()
info.text = (option.label ~= "") and option.label or "(any)"
info.func = function()
MTH_BOOK_STATE.petContinent = option.value
MTH_BOOK_STATE.petZoneFilter = "all"
MTH_BOOK_STATE.page = 1
MTH_BOOK_STATE.selectedEntry = nil
MTH_BOOK_DropdownSetText(continentDD, info.text)
if option.value ~= "all" then
MTH_BOOK_BuildBeastZoneOptions(option.value)
local zoneDD = getglobal("MTH_BOOK_BeastZoneDropdown")
if zoneDD then
UIDropDownMenu_Initialize(zoneDD, MTH_BOOK_STATE._beastZoneInitFunc)
zoneDD:Show()
MTH_BOOK_UpdateBeastZoneDropdownText()
end
else
local zoneDD = getglobal("MTH_BOOK_BeastZoneDropdown")
if zoneDD then zoneDD:Hide() end
end
MTH_BOOK_RefreshFilter()
end
info.checked = (MTH_BOOK_STATE.petContinent == option.value)
UIDropDownMenu_AddButton(info)
end
end)
UIDropDownMenu_SetWidth(80, continentDD)
UIDropDownMenu_JustifyText("LEFT", continentDD)
MTH_BOOK_UpdateBeastContinentDropdownText()
end
local zoneInitFunc = function()
local zoneDD = getglobal("MTH_BOOK_BeastZoneDropdown")
for i = 1, table.getn(MTH_BOOK_BeastZoneOptions) do
local option = MTH_BOOK_BeastZoneOptions[i]
local info = MTH_BOOK_DropdownCreateInfo()
info.text = option.label
info.func = function()
MTH_BOOK_STATE.petZoneFilter = option.value
MTH_BOOK_STATE.page = 1
MTH_BOOK_STATE.selectedEntry = nil
if zoneDD then MTH_BOOK_DropdownSetText(zoneDD, option.label) end
MTH_BOOK_RefreshFilter()
end
info.checked = (MTH_BOOK_STATE.petZoneFilter == option.value)
UIDropDownMenu_AddButton(info)
end
end
MTH_BOOK_STATE._beastZoneInitFunc = zoneInitFunc
local zoneDD = getglobal("MTH_BOOK_BeastZoneDropdown")
if zoneDD then
UIDropDownMenu_Initialize(zoneDD, zoneInitFunc)
UIDropDownMenu_SetWidth(120, zoneDD)
UIDropDownMenu_JustifyText("LEFT", zoneDD)
MTH_BOOK_UpdateBeastZoneDropdownText()
end
end
local function MTH_BOOK_WireUI(frame)
if frame and frame.SetBackdropColor then
frame:SetBackdropColor(0.05, 0.05, 0.05, 1.00)
@@ -4868,12 +5168,6 @@ local function MTH_BOOK_WireUI(frame)
local requireVendor = getglobal("MTH_BOOK_RequireVendor")
local requireDrop = getglobal("MTH_BOOK_RequireDrop")
local requireObject = getglobal("MTH_BOOK_RequireObject")
local abilityDropdown = getglobal("MTH_BOOK_AbilityDropdown")
local rankDropdown = getglobal("MTH_BOOK_RankDropdown")
local petLearnSourceDropdown = getglobal("MTH_BOOK_PetLearnSourceDropdown")
local itemSubtypeDropdown = getglobal("MTH_BOOK_ItemSubtypeDropdown")
local npcFunctionDropdown = getglobal("MTH_BOOK_NPCFunctionDropdown")
local npcZoneDropdown = getglobal("MTH_BOOK_NPCZoneDropdown")
local petBookScanButton = getglobal("MTH_BOOK_PetBookScanButton")
local hideNoAbilities = getglobal("MTH_BOOK_HideNoAbilities")
local hideUnknown = getglobal("MTH_BOOK_HideUnknown")
@@ -4934,6 +5228,7 @@ local function MTH_BOOK_WireUI(frame)
MTH_BOOK_InitFamilyDropdown()
MTH_BOOK_InitAbilityDropdown()
MTH_BOOK_InitRankDropdown()
MTH_BOOK_WireBeastDropdowns()
MTH_BOOK_InitPetLearnSourceDropdown()
MTH_BOOK_InitItemSubtypeDropdown()
MTH_BOOK_InitNPCFunctionDropdown()
+21 -2
View File
@@ -57,6 +57,14 @@
</Anchors>
</FontString>
<FontString name="$parentBeastZoneLabel" inherits="GameFontNormalSmall" text="Zone:" hidden="true">
<Anchors>
<Anchor point="TOPLEFT">
<Offset><AbsDimension x="464" y="-142"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
@@ -265,7 +273,7 @@
</Frame>
<CheckButton name="$parentPetInZoneOnly" inherits="UICheckButtonTemplate" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="488" y="-112"/></Offset></Anchor>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="526" y="-106"/></Offset></Anchor>
</Anchors>
</CheckButton>
<Button name="$parentShowAllOnMapButton" inherits="UIPanelButtonTemplate" text="Show all on map" hidden="true">
@@ -298,8 +306,19 @@
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="232" y="-140"/></Offset></Anchor></Anchors>
</CheckButton>
<CheckButton name="$parentHideUnknown" inherits="UICheckButtonTemplate">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="372" y="-140"/></Offset></Anchor></Anchors>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="290" y="-140"/></Offset></Anchor></Anchors>
</CheckButton>
<Frame name="$parentBeastContinentDropdown" inherits="UIDropDownMenuTemplate" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="482" y="-136"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="$parentBeastZoneDropdown" inherits="UIDropDownMenuTemplate" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="592" y="-136"/></Offset></Anchor>
</Anchors>
</Frame>
<Frame name="$parentListSliderBackdrop">
<Size><AbsDimension x="22" y="322"/></Size>
+46 -22
View File
@@ -12,6 +12,7 @@ MTH_Map = {
mapZoneCache = {},
focusNodes = {},
pendingZoneOpenId = nil,
pendingZoneOpenPfqId = nil,
pendingZoneOpenSourceId = nil,
pendingZoneOpenAttempts = 0,
pendingZoneOpenNextAttemptAt = 0,
@@ -60,11 +61,6 @@ local function MTH_Map_Mod(a, b)
return 0
end
local function MTH_Map_Log(msg)
if MTH and MTH.debug and MTH.Print then
MTH:Print("[MAP] " .. tostring(msg), "debug")
end
end
local function MTH_Map_NormalizeName(name)
if not name then return "" end
@@ -130,12 +126,18 @@ local function MTH_Map_GetZoneNameById(zoneId)
local normalizedId = tonumber(zoneId) or zoneId
if not normalizedId then return nil end
if MTH_DS_ZoneNamesFallback and MTH_DS_ZoneNamesFallback[normalizedId] then
return MTH_DS_ZoneNamesFallback[normalizedId]
-- Translate WMA→pfQ first so WMA IDs don't accidentally hit wrong pfQ entries.
local pfqId = normalizedId
if type(MTH_MAP_WMA_TO_PFQ) == "table" then
pfqId = MTH_MAP_WMA_TO_PFQ[normalizedId] or normalizedId
end
if MTH_DS_ZoneNamesFallback and MTH_DS_ZoneNamesFallback[pfqId] then
return MTH_DS_ZoneNamesFallback[pfqId]
end
if not MTH_DS_Zones then return nil end
local row = MTH_DS_Zones[normalizedId] or MTH_DS_Zones[tostring(normalizedId)]
local row = MTH_DS_Zones[pfqId] or MTH_DS_Zones[tostring(pfqId)]
if not row or not row.names then return nil end
return row.names.enUS or row.names[GetLocale()] or row.names.deDE or row.names.frFR
end
@@ -327,7 +329,6 @@ function MTH_Map:BuildZoneLookup()
for _ in pairs(self.zoneNameNormToId) do
count = count + 1
end
MTH_Map_Log("Zone lookup rebuilt: " .. tostring(count) .. " entries")
end
function MTH_Map:GetMapIDByName(name)
@@ -418,6 +419,15 @@ function MTH_Map:ResolveZoneHierarchy(zoneId)
return resolved
end
-- Translate a pfQ AreaTable ID to a WMA ID (for zone matching against coord tuples).
-- Falls back to the pfQ ID itself if no mapping exists (sub-zones, custom zones).
local function MTH_Map_PfqToWma(pfqId)
if type(MTH_MAP_PFQ_TO_WMA) == "table" then
return MTH_MAP_PFQ_TO_WMA[pfqId] or pfqId
end
return pfqId
end
function MTH_Map:GetCurrentMapID()
local continent = GetCurrentMapContinent and GetCurrentMapContinent() or nil
local zone = GetCurrentMapZone and GetCurrentMapZone() or nil
@@ -434,6 +444,7 @@ function MTH_Map:GetCurrentMapID()
if mapId then
local resolved = self:ResolveZoneHierarchy(mapId)
local normalized = resolved.zoneId or mapId
normalized = MTH_Map_PfqToWma(normalized)
self.lastMapContext = string.format("continent=%s zone=%s mapZone='%s' real=nil mapId=%s normalized=%s", tostring(continent), tostring(zone), tostring(zoneName), tostring(mapId), tostring(normalized))
return normalized
end
@@ -449,6 +460,7 @@ function MTH_Map:GetCurrentMapID()
mapId = self:GetMapIDByName(real)
local resolved = self:ResolveZoneHierarchy(mapId)
local normalized = resolved.zoneId or mapId
normalized = MTH_Map_PfqToWma(normalized)
self.lastMapContext = string.format("continent=%s zone=%s mapZone='%s' real='%s' mapId=%s normalized=%s", tostring(continent), tostring(zone), tostring(zoneName), tostring(real), tostring(mapId), tostring(normalized))
return normalized
end
@@ -467,6 +479,7 @@ end
local function MTH_Map_ClearPendingZoneOpen()
MTH_Map.pendingZoneOpenId = nil
MTH_Map.pendingZoneOpenPfqId = nil
MTH_Map.pendingZoneOpenSourceId = nil
MTH_Map.pendingZoneOpenAttempts = 0
MTH_Map.pendingZoneOpenNextAttemptAt = 0
@@ -532,8 +545,12 @@ end
function MTH_Map:OpenWorldMapForZone(zoneId)
local zid = tonumber(zoneId)
if not zid then return false end
local resolved = self:ResolveZoneHierarchy(zid)
local targetZoneId = tonumber(resolved.zoneId or zid) or zid
-- Translate WMA ID → pfQ so game map APIs (pfMap, SetMapZoom) work correctly.
local pfqZid = (type(MTH_MAP_WMA_TO_PFQ) == "table" and MTH_MAP_WMA_TO_PFQ[zid]) or zid
local resolved = self:ResolveZoneHierarchy(pfqZid)
local targetZoneId = tonumber(resolved.zoneId or pfqZid) or pfqZid
-- WMA ID of this zone, for comparing against GetCurrentMapID() which returns WMA IDs.
local targetWmaId = (type(MTH_MAP_PFQ_TO_WMA) == "table" and MTH_MAP_PFQ_TO_WMA[targetZoneId]) or zid
if ToggleWorldMap then
if not WorldMapFrame or not WorldMapFrame:IsShown() then
@@ -546,34 +563,39 @@ function MTH_Map:OpenWorldMapForZone(zoneId)
if pfMap and pfMap.SetMapByID then
pfMap:SetMapByID(targetZoneId)
local currentAfterPF = self:GetCurrentMapID()
if tonumber(currentAfterPF) == tonumber(targetZoneId) then
if tonumber(currentAfterPF) == tonumber(targetWmaId) then
MTH_Map_ClearPendingZoneOpen()
return true
end
end
local zoomApplied = MTH_Map_ApplyWorldMapZone(targetZoneId, zid)
local zoomApplied = MTH_Map_ApplyWorldMapZone(zid, zid)
if zoomApplied then
local currentAfterZoom = self:GetCurrentMapID()
if tonumber(currentAfterZoom) == tonumber(targetZoneId) then
if tonumber(currentAfterZoom) == tonumber(targetWmaId) then
MTH_Map_ClearPendingZoneOpen()
return true
end
end
local nowMapId = self:GetCurrentMapID()
if tonumber(nowMapId) == tonumber(targetZoneId) then
if tonumber(nowMapId) == tonumber(targetWmaId) then
MTH_Map_ClearPendingZoneOpen()
return true
end
self.pendingZoneOpenId = targetZoneId
self.pendingZoneOpenSourceId = zid
self.pendingZoneOpenId = targetWmaId
self.pendingZoneOpenPfqId = targetZoneId
self.pendingZoneOpenSourceId = pfqZid
self.pendingZoneOpenAttempts = 8
self.pendingZoneOpenNextAttemptAt = (GetTime and GetTime() or 0) + 0.25
return true
end
function MTH_Map:GetFamilyColor(family)
return MTH_Map_GetFamilyColor(family)
end
function MTH_Map:FocusBeast(beastId, beast)
local id = tonumber(beastId)
if not id then return false end
@@ -890,7 +912,6 @@ function MTH_Map:RebuildNodes()
end
end
MTH_Map_Log("Rebuilt nodes for source '" .. tostring(self.activeSource) .. "'")
end
function MTH_Map:UpdateWorldMap()
@@ -1019,6 +1040,11 @@ function MTH_Map:UpdateMinimap()
local minimapSizes = MTH_DS_MinimapSizes or (pfDB and pfDB["minimap"])
local mapSize = minimapSizes and minimapSizes[mapId]
if not mapSize and type(MTH_MAP_WMA_TO_PFQ) == "table" then
-- Fallback: try the pfQ key (for sub-zones not yet mapped to WMA)
local pfqId = MTH_MAP_WMA_TO_PFQ[mapId]
mapSize = pfqId and minimapSizes and minimapSizes[pfqId]
end
if not mapSize then
self.lastMiniReason = self.verboseMiniReasons and "missing-minimap-size" or "missing-size"
self._lastMiniState = nil
@@ -1227,7 +1253,6 @@ function MTH_Map:RegisterDefaultProviders()
end
if unresolvedCount > 0 then
MTH_Map_Log("drops source: " .. tostring(unresolvedCount) .. " drop-creature links have no loaded coordinates (sample IDs: " .. table.concat(unresolvedSamples, ", ") .. ")")
end
return nodes
@@ -1274,9 +1299,9 @@ function MTH_Map:Init()
if WorldMapFrame and WorldMapFrame:IsShown() then
if pfMap and pfMap.SetMapByID then
pfMap:SetMapByID(MTH_Map.pendingZoneOpenId)
pfMap:SetMapByID(MTH_Map.pendingZoneOpenPfqId or MTH_Map.pendingZoneOpenId)
end
MTH_Map_ApplyWorldMapZone(MTH_Map.pendingZoneOpenId, MTH_Map.pendingZoneOpenSourceId)
MTH_Map_ApplyWorldMapZone(MTH_Map.pendingZoneOpenId or MTH_Map.pendingZoneOpenPfqId, MTH_Map.pendingZoneOpenId or MTH_Map.pendingZoneOpenPfqId)
local nowMapId = MTH_Map:GetCurrentMapID()
if tonumber(nowMapId) == tonumber(MTH_Map.pendingZoneOpenId) then
MTH_Map_ClearPendingZoneOpen()
@@ -1318,5 +1343,4 @@ function MTH_Map:Init()
end)
end
MTH_Map_Log("Map system initialized")
end
+133
View File
@@ -0,0 +1,133 @@
------------------------------------------------------
-- MetaHunt: Experimental Ammunition Options Panel
-- TWoW 1.18.1 — "Nightmares of Ursol"
------------------------------------------------------
local function MTH_EA_OPT_Ready()
return type(MTH_GetFrame) == "function"
and type(MTH_CreateCheckbox) == "function"
and type(MTH_CreateSlider) == "function"
end
local function MTH_EA_OPT_GetCfg()
if type(MTH_SavedVariables) ~= "table" then return {} end
if type(MTH_SavedVariables.modules) ~= "table" then return {} end
return MTH_SavedVariables.modules["expammo"] or {}
end
local MTH_EA_OPT_built = false
-- Helper: create a pre-wired checkbox
local function MTH_EA_OPT_CB(container, name, label, yOff, isChecked, onClick)
local cb = MTH_CreateCheckbox(container, name, label, yOff, 20)
if cb then
cb:SetChecked(isChecked and 1 or 0)
cb:SetScript("OnClick", function()
local v = this:GetChecked()
onClick(v == 1 or v == true)
end)
end
return cb
end
-- Helper: section header label
local function MTH_EA_OPT_Header(container, text, yOff)
local fs = container:CreateFontString(nil, "ARTWORK", "GameFontNormal")
fs:SetPoint("TOPLEFT", container, "TOPLEFT", 20, yOff)
fs:SetText("|cffff9900" .. text .. "|r")
end
local function MTH_EA_OPT_BuildUI(container)
if MTH_EA_OPT_built then return end
MTH_EA_OPT_built = true
local cfg = MTH_EA_OPT_GetCfg()
-- Title
local title = container:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
title:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -16)
title:SetText("MM Widget — Experimental Ammunition Tracker")
-- Short description
local desc = container:CreateFontString(nil, "ARTWORK", "GameFontNormal")
desc:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -46)
desc:SetWidth(540)
desc:SetJustifyH("LEFT")
desc:SetJustifyV("TOP")
desc:SetText(
"Tracks the 1.18.1 ammo cycling mechanic.\n"
.. "Aimed Shot starts a 60s cycle: "
.. "|cffff8833Explosive|r \226\134\146 |cff33ff55Poisonous|r \226\134\146 |cff6633ffEnchanted|r\n"
.. "Each state is consumed by its matching shot (Multi-Shot / Serpent Sting / Arcane Shot)."
)
-- ── Section: Module ──────────────────────────────────────
MTH_EA_OPT_Header(container, "Module", -112)
local moduleEnabled = true
if MTH and MTH.IsModuleEnabled then
moduleEnabled = MTH:IsModuleEnabled("expammo", true) and true or false
end
MTH_EA_OPT_CB(container, "MTH_ExpAmmoModuleCB",
"Enable MM Widget module",
-132, moduleEnabled,
function(v)
if MTH and MTH.SetModuleEnabled then
MTH:SetModuleEnabled("expammo", v)
end
if MTH_ExpAmmo then MTH_ExpAmmo.SetEnabled(v) end
end)
-- ── Section: Tracker Visibility ─────────────────────────
MTH_EA_OPT_Header(container, "Tracker Visibility", -166)
MTH_EA_OPT_CB(container, "MTH_ExpAmmoShowLnLCB",
"Show Lock and Load cell (top)",
-186, cfg.showLnL ~= false,
function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetShowLnL(v) end end)
MTH_EA_OPT_CB(container, "MTH_ExpAmmoShowHintCB",
"Show action hint cell (bottom)",
-210, cfg.showHint ~= false,
function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetShowHint(v) end end)
MTH_EA_OPT_CB(container, "MTH_ExpAmmoHideOOCCB",
"Hide widget when out of combat",
-234, cfg.hideOOC == true,
function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetHideOOC(v) end end)
-- ── Section: Appearance ──────────────────────────────────
MTH_EA_OPT_Header(container, "Appearance", -268)
local sizeSlider = MTH_CreateSlider(container, "MTH_ExpAmmoCellSizeSlider",
"Cell Size (px)", 20, 80, 4, -288)
if sizeSlider then
sizeSlider:SetValue(math.max(20, math.min(80, cfg.cellSize or 40)))
sizeSlider.onChange = function(val)
if MTH_ExpAmmo then MTH_ExpAmmo.SetCellSize(val) end
end
end
MTH_EA_OPT_CB(container, "MTH_ExpAmmoBigCDCB",
"Large cooldown numbers (centered in cell)",
-352, cfg.bigCD == true,
function(v) if MTH_ExpAmmo then MTH_ExpAmmo.SetBigCD(v) end end)
-- ── Notes ───────────────────────────────────────────────
local tip = container:CreateFontString(nil, "ARTWORK", "GameFontNormal")
tip:SetPoint("TOPLEFT", container, "TOPLEFT", 20, -388)
tip:SetWidth(540)
tip:SetJustifyH("LEFT")
tip:SetTextColor(0.3, 0.7, 1.0)
tip:SetText("Tip: hold ALT and drag the tracker widget to reposition it.")
end
function MTH_SetupExpAmmoOptions()
if not MTH_EA_OPT_Ready() then return end
local container = MTH_GetFrame("MetaHuntOptionsExpAmmo")
if not container then return end
local ok, err = pcall(MTH_EA_OPT_BuildUI, container)
if not ok and MTH and MTH.Print then
MTH:Print("[MTH ExpAmmo OPTIONS] " .. tostring(err), "error")
end
end
+9 -2
View File
@@ -3,8 +3,8 @@ local MTH_OPTIONS_SETUP = {} -- Track which frames have been set up
MTH_OPTIONS_CONST = MTH_OPTIONS_CONST or {}
MTH_OPTIONS_CONST.NAV_DEFAULT_WIDTH = MTH_OPTIONS_CONST.NAV_DEFAULT_WIDTH or 120
MTH_OPTIONS_CONST.WINDOW_PADDING = MTH_OPTIONS_CONST.WINDOW_PADDING or 12
MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET = MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET or -72
MTH_OPTIONS_CONST.CONTENT_HEIGHT = MTH_OPTIONS_CONST.CONTENT_HEIGHT or 540
MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET = MTH_OPTIONS_CONST.CONTENT_TOP_OFFSET or -58
MTH_OPTIONS_CONST.CONTENT_HEIGHT = MTH_OPTIONS_CONST.CONTENT_HEIGHT or 566
MTH_OPTIONS_CONST.CLOSE_BTN_SIZE = MTH_OPTIONS_CONST.CLOSE_BTN_SIZE or 22
MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_X = MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_X or -8
MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_Y = MTH_OPTIONS_CONST.CLOSE_BTN_OFFSET_Y or -8
@@ -205,6 +205,13 @@ function MTH_SelectOptionsTab(tabKey)
MTH_SetupAutoQuestOptions()
MTH_OPTIONS_SETUP["AutoQuest"] = true
end
elseif tabKey == "ExpAmmo" then
if not MTH_OPTIONS_SETUP["ExpAmmo"] then
if type(MTH_SetupExpAmmoOptions) == "function" then
MTH_SetupExpAmmoOptions()
MTH_OPTIONS_SETUP["ExpAmmo"] = true
end
end
elseif tabKey == "ICU" then
if type(MTH_SetupICUOptions) == "function" then
MTH_SetupICUOptions()
+2
View File
@@ -44,6 +44,7 @@ MTH_OPTIONS_TABS = MTH_OPTIONS_TABS or {
{ key = "AutoBuy", label = "Auto Buy", frame = "MetaHuntOptionsAutoBuy" },
{ key = "AutoQuest", label = "Auto Quest", frame = "MetaHuntOptionsAutoQuest" },
{ key = "ICU", label = "ICU", frame = "MetaHuntOptionsICU" },
{ key = "ExpAmmo", label = "MM Widget", frame = "MetaHuntOptionsExpAmmo" },
{ key = "ChronometerGeneral", label = "General", frame = "MetaHuntOptionsChronometer" },
{ key = "ChronometerBar", label = "Bar", frame = "MetaHuntOptionsChronometer" },
{ key = "ChronometerClassSpells", label = "Hunter Spells", frame = "MetaHuntOptionsChronometer" },
@@ -60,6 +61,7 @@ MTH_OPTIONS_TREE = MTH_OPTIONS_TREE or {
{ label = "Auto Quest", key = "AutoQuest" },
{ label = "Feed-O-Matic", key = "FeedOMatic" },
{ label = "ICU", key = "ICU" },
{ label = "MM Widget", key = "ExpAmmo" },
{
label = "zButtons",
node = "zButtons",
+3 -2
View File
@@ -46,12 +46,12 @@
<Frames>
<Frame name="$parentNav" enableMouse="true">
<Size>
<AbsDimension x="120" y="540"/>
<AbsDimension x="120" y="566"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="12" y="-72"/>
<AbsDimension x="12" y="-58"/>
</Offset>
</Anchor>
</Anchors>
@@ -82,6 +82,7 @@
<Frame name="$parentAutoBuy" hidden="true" enableMouse="true"/>
<Frame name="$parentAutoQuest" hidden="true" enableMouse="true"/>
<Frame name="$parentICU" hidden="true" enableMouse="true"/>
<Frame name="$parentExpAmmo" hidden="true" enableMouse="true"/>
<Frame name="$parentChronometer" hidden="true" enableMouse="true"/>
<Frame name="$parentCredits" hidden="true" enableMouse="true"/>
+3
View File
@@ -6,6 +6,9 @@ function MTH_ST_LifecycleService:HandleEvent(evt, eventArg1, eventArg2)
if type(MTH_ST_HandlePlayerEnteringWorld) == "function" and MTH_ST_HandlePlayerEnteringWorld(evt) then
return true, "lifecycle-enter-world"
end
if type(MTH_ST_HandleSpellcastEvent) == "function" and MTH_ST_HandleSpellcastEvent(evt, eventArg1, eventArg2) then
return true, "lifecycle-spellcast"
end
if type(MTH_ST_HandleUnitPetEvent) == "function" and MTH_ST_HandleUnitPetEvent(evt, eventArg1) then
if evt == "UNIT_PET" then
if eventArg1 == "player" then