From 8479607a1396943e15862443be03c90c9be190f3 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Mon, 23 Mar 2026 22:25:10 +0100 Subject: [PATCH] v1.4.0 Twow 1.18.1 --- .gitignore | 4 + Bindings.xml | 5 + CHANGELOG.md | 46 + MetaHunt.lua | 9 + MetaHunt.toc | 7 +- README.MD | 8 +- api/beastlore-scan.lua | 543 + api/constants.lua | 20 +- api/core-framework.lua | 39 +- api/core-scan-beasttraining.lua | 2 - api/core-scan-huntertalents.lua | 172 + api/core-scan-stablemaster.lua | 30 +- api/core.lua | 164 +- api/hunterbook-model-viewer.lua | 34 +- api/hunterbook-tab-beastlore.lua | 106 +- api/hunterbook.lua | 397 +- api/hunterbook.xml | 23 +- api/map-markers.lua | 68 +- api/options-expammo.lua | 133 + api/options-shell.lua | 11 +- api/options-tree.lua | 2 + api/options.xml | 5 +- api/pets-service-lifecycle.lua | 3 + data/ds-beasts-extra.lua | 2163 - data/ds-beasts-families-dbc.lua | 18 +- data/ds-beasts-families.lua | 19 +- data/ds-beasts-html-enrichment.lua | 15506 ------- data/ds-beasts-skins.lua | 22 +- data/ds-beasts-turtledb.lua | 12872 ------ data/ds-beasts.lua | 28022 +++++++++++- data/ds-creature-coords.lua | 23204 +++++----- data/ds-hunter-spells-dbc.lua | 844 + data/ds-hunter-spells.lua | 224 + data/ds-items-atlas-sources.lua | 5 + data/ds-items-origins.lua | 26 + data/ds-items.lua | 45 + data/ds-minimap-sizes.lua | 242 +- data/ds-pet-spells-acquire.lua | 25 +- data/ds-pet-spells.lua | 52 +- data/ds-stable-prices.lua | 12 +- data/ds-unit-coords.lua | 46986 ++++++++++----------- data/ds-vendors-extra.lua | 390 +- data/ds-vendors.lua | 438 +- data/ds-zones-dbc.lua | 15 +- data/ds-zones-fallback.lua | 2 + data/ds-zones-new-dbc.lua | 321 +- data/ds-zones-wma.lua | 402 + data/init.lua | 5 + init/api.xml | 5 +- init/modules.xml | 3 + locales/spells/deDE.lua | 5 + locales/spells/enUS.lua | 5 + locales/spells/esES.lua | 5 + locales/spells/ptBR.lua | 5 + locales/spells/ruRU.lua | 5 + locales/spells/zhCN.lua | 5 + modules/autobuy/engine.lua | 25 - modules/autobuy/module.lua | 29 +- modules/autoquest/module.lua | 2 +- modules/chronometer/data-hunter.lua | 9 + modules/chronometer/engine.lua | 48 +- modules/chronometer/module.lua | 20 +- modules/expammo/engine.lua | 1084 + modules/expammo/loader.xml | 6 + modules/feedomatic/FeedOMatic.lua | 230 +- modules/feedomatic/FeedOMatic_Emotes.lua | 2 +- modules/feedomatic/GFWUtils.lua | 13 - modules/feedomatic/module.lua | 14 +- modules/icu/module.lua | 2 +- modules/smartammo/module.lua | 2 +- modules/tooltips/module.lua | 35 +- modules/zhunter/DeferredInit.lua | 19 +- modules/zhunter/ZHunterMod.lua | 46 - modules/zhunter/ZSpellButtonTemplate.lua | 23 - modules/zhunter/module.lua | 47 +- modules/zhunter/zButtonAmmo.lua | 5 + modules/zhunter/zButtonAspect.lua | 5 +- modules/zhunter/zButtonRanged.lua | 4 - 78 files changed, 66732 insertions(+), 68667 deletions(-) create mode 100644 api/beastlore-scan.lua create mode 100644 api/core-scan-huntertalents.lua create mode 100644 api/options-expammo.lua delete mode 100644 data/ds-beasts-extra.lua delete mode 100644 data/ds-beasts-html-enrichment.lua delete mode 100644 data/ds-beasts-turtledb.lua create mode 100644 data/ds-hunter-spells-dbc.lua create mode 100644 data/ds-hunter-spells.lua create mode 100644 data/ds-zones-wma.lua create mode 100644 modules/expammo/engine.lua create mode 100644 modules/expammo/loader.xml diff --git a/.gitignore b/.gitignore index e3d5407..07377a9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ tools/ LOC*.md LOCALIZATION*.md + +# Dev tooling — Python generators, data processing scripts, scratch files +# Not part of the addon, never ship this +.tools/ diff --git a/Bindings.xml b/Bindings.xml index a2caab5..79ac4b6 100644 --- a/Bindings.xml +++ b/Bindings.xml @@ -118,4 +118,9 @@ zButtonPet_KeyBinding(6) + + + + MTH_EA_CastActionSpell(); + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f603da3..fa9e3f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,52 @@ All notable changes to MetaHunt will be documented in this file. +## [1.4.0] - 2026-03-23 + +### TWoW 1.18.1 "Nightmares of Ursol" support + +- **Complete beast database overhaul**: The MTH beast datastore was reconstructed from scratch, based on a full (beasts) 1.18.1 extract from live DB, gladly provided by Twow staff (thanks @Haaxor!). The beast data should now be 100% accurate, and notably all spawn coordinates, respawn times and pet abilities learnable on all beasts in the world. + +- The **Beast Lore** page of the Book now allows to filter the list by Zone. Additionaly, the beast inspector has been reformated and all meaningful beast's data is displayed nicely. + +- All new and missing **Beasts Models** have been (re)scanned from MPQ game files and are viewable in the Beast Lore page, and browsable in the Family Model viewer. + +- Added **MM widget** module: tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock and Load procs, and has adynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. Lets Make MM great again. + +- Added new **Moth** pet family. + +- Added new **Pollen Burst** pet ability. + +- Added new **Aspect of the Viper** in zAspect. + +- Added new **Chronometer** timer entries for 1.18.1 spells: + - Aimed Shot cooldown tracker + - Kill Command cooldown tracker + - Lock and Load proc timer + - Scent of Blood proc timer + +- Added new **Chronometer** target debuff entries for the Experimental Ammo states: + - Poisonous Ammunition: 15s armor-reduction debuff (triggered when Serpent Sting consumes the Poisonous state) + - Enchanted Ammunition: 6s magic-resist debuff (triggered when Arcane Shot consumes the Enchanted state) + +- Added the **5 new craftable ammo** to the Projectiles book's page, SmartAmmo and zAmmo button: + - **Bright Wood Arrows** (Survival: Journeyman, req. 30) — 10.5 DPS + - **Shade Wood Arrows** (Survival: Expert, req. 40) — 14.5 DPS + - **Smooth Ironfeather Arrows** (Survival: Artisan, req. 50) — 17.5 DPS + - **Starfeather Arrows** (Survival: Artisan, req. 55) — 19 DPS + - **Enchanted Thorium Shells** (Engineering: Artisan, req. 55) — 19 DPS + +- Added new hunter-friends NPC of **Moonwhisper Coast** to the NPC Finder Book's page: + - **Hula Swiftmane** — Horde Stable Master (66.1, 39.1) + - **Ornala** — Horde Ammo Vendor (67.3, 37.1) — sells Arrows and Slugs + - **Badel Wildlance** — Alliance Stable Master (59.0, 24.9) + - **Elendon Truebough** — Alliance Ammo Vendor (58.8, 24.5) — sells Arrows + +### Fixed + +- Fixed **Chronometer** bars appearing from nearby players' combat events (e.g. another hunter's traps or DoTs triggering your own bars). +- Fixed the list in Families Book's page, that wasnt tall enough to display all famillies (wolves were hidden). + ## [1.3.0] - 2026-03-19 diff --git a/MetaHunt.lua b/MetaHunt.lua index 2dbc0c1..3d5045e 100644 --- a/MetaHunt.lua +++ b/MetaHunt.lua @@ -2,6 +2,15 @@ -- MetaHunt: Hunter's Toolkit Addon -- A unified addon merging FeedOMatic, Tooltips, and zhunter -- Modern modular architecture with legacy addon support +-- +-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +-- TARGET RUNTIME: LUA 5.0 (WoW 1.12 / Turtle-WoW) +-- - Max 32 upvalues per function +-- - No # operator (use table.getn) +-- - No string.gmatch (use string.gfind) +-- - NEVER validate with Lua 5.1+ tools (false passes) +-- - Errors: check Logs/FrameXML.log first +-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ------------------------------------------------------ -- Initialize beast database after all files loaded diff --git a/MetaHunt.toc b/MetaHunt.toc index f5da2b1..d6a0525 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -1,5 +1,5 @@ ## Interface: 11200 -## Version: 1.3.0 +## Version: 1.4.0 ## Title: MetaHunt - |cff00ff00Hunter ## Author: Metasploit and his Copilot ;) ## Notes: Unified addon suite for huntards making old addons compatible with TurtleWoW, and adding an arsenal of never seen Hunter's utilities. @@ -20,8 +20,6 @@ init\skins.xml ## Load database files (DB files must come before data\init.lua) data\ds-beasts.lua -data\ds-beasts-extra.lua -data\ds-beasts-html-enrichment.lua data\ds-vendors.lua data\ds-vendors-extra.lua data\ds-items.lua @@ -31,6 +29,7 @@ data\ds-bags.lua data\ds-items-atlas-sources.lua data\ds-zones.lua data\ds-zones-fallback.lua +data\ds-zones-wma.lua data\ds-minimap-sizes.lua data\ds-unit-coords.lua data\ds-creature-coords.lua @@ -39,6 +38,8 @@ data\ds-beasts-families-dbc.lua data\ds-beasts-skins.lua data\ds-pet-spells.lua data\ds-pet-spells-trainer.lua +data\ds-hunter-spells.lua +data\ds-hunter-spells-dbc.lua data\ds-racial.lua data\init.lua diff --git a/README.MD b/README.MD index 71832b3..f70176c 100644 --- a/README.MD +++ b/README.MD @@ -8,7 +8,7 @@ It provides a bunch useful tools for all huntards, wheter they are still levelli MetaHunt ships with large up-to-date datastores for all Turtle WOW data a hunter needs : -- All beasts, their location, and the abilities they can learn you +- 100% accurate beasts' data, their locations, and the abilities they can learn you - All Pet families, their abilities and their diet - All pet abilities ranks - All Ammo (Arrows and Bullets) @@ -21,6 +21,8 @@ MetaHunt ships with large up-to-date datastores for all Turtle WOW data a hunter - Auto equip another ammo instantly during combat whenever you get out of your current ammo. - Auto equip the right ammo when you swap from a box/xbow to a gun and the other way around. +## MM Widget + Lets Make MM great again. Tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock&Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. ## Tooltips @@ -72,7 +74,3 @@ Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. - Keep an history of all your pets after you abandon them or if they run away. - ## What it will never do. - -The goal of MetaHunt is to offer hunter's features that are not covered by existing Twow addons, and in others https://github.com/SabineWren/Quiver . -There's no point to reinvent the wheel, except is the wheel is broken. Nothing will work better than Quiver for auto-shot, castbar and tranqshot mgt. MetaHunt is build to work alongside of Quiver and is complementary to it, and it will remain like this unless Quiver turns to be not upgraded anymore. diff --git a/api/beastlore-scan.lua b/api/beastlore-scan.lua new file mode 100644 index 0000000..1d6b7d1 --- /dev/null +++ b/api/beastlore-scan.lua @@ -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 diff --git a/api/constants.lua b/api/constants.lua index df3eb41..ef3ecc0 100644 --- a/api/constants.lua +++ b/api/constants.lua @@ -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, diff --git a/api/core-framework.lua b/api/core-framework.lua index ec823d1..80a1c5c 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -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 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) diff --git a/api/core-scan-beasttraining.lua b/api/core-scan-beasttraining.lua index bae2bbd..acda272 100644 --- a/api/core-scan-beasttraining.lua +++ b/api/core-scan-beasttraining.lua @@ -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 diff --git a/api/core-scan-huntertalents.lua b/api/core-scan-huntertalents.lua new file mode 100644 index 0000000..6df3b68 --- /dev/null +++ b/api/core-scan-huntertalents.lua @@ -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() diff --git a/api/core-scan-stablemaster.lua b/api/core-scan-stablemaster.lua index 00c50e5..e126329 100644 --- a/api/core-scan-stablemaster.lua +++ b/api/core-scan-stablemaster.lua @@ -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 diff --git a/api/core.lua b/api/core.lua index 987f9b8..9d5d708 100644 --- a/api/core.lua +++ b/api/core.lua @@ -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 diff --git a/api/hunterbook-model-viewer.lua b/api/hunterbook-model-viewer.lua index f353e04..ef0ace4 100644 --- a/api/hunterbook-model-viewer.lua +++ b/api/hunterbook-model-viewer.lua @@ -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() diff --git a/api/hunterbook-tab-beastlore.lua b/api/hunterbook-tab-beastlore.lua index fac08cf..4a2fb89 100644 --- a/api/hunterbook-tab-beastlore.lua +++ b/api/hunterbook-tab-beastlore.lua @@ -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 diff --git a/api/hunterbook.lua b/api/hunterbook.lua index d904f26..14e7f35 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -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 respawnWindow = MTH_BOOK_FormatRespawnWindow(beast) - if respawnWindow then - MTH_BOOK_AddDetailKV(lines, "Respawn", respawnWindow) + 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_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() diff --git a/api/hunterbook.xml b/api/hunterbook.xml index 91f554c..1126347 100644 --- a/api/hunterbook.xml +++ b/api/hunterbook.xml @@ -57,6 +57,14 @@ + + @@ -265,7 +273,7 @@