diff --git a/CHANGELOG.md b/CHANGELOG.md index 063c914..4c9f68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ Resurrecting on Octowow. ### Added +- **Hunter's Book - Hunter and Pet spells**: Added catalog pages for all Hunter and Pet Trainer spells, with spell icons, required level, base/Honored prices, branch, known status, search, level filters, sorting, and a scrollable inspector. + +- **Trainer cost planning**: Hunter's Book now shows a training plan with the cost of spells available now, the next training level, and all remaining spells. Prices can be switched between standard and Honored costs. + +- **Live trainer status tracking**: Visiting a Hunter or Pet Trainer now records available, unavailable, and already-used services without guessing from filtered trainer lists. + +- **Level-up tracking**: Added Hunter, Pet, and Pet Loyalty level-up notifications. Hunter messages list catalog spells unlocked at that level with base and Honored costs; major Hunter milestones use a special celebration. + +- **Leveling animation and message options**: Added separate Animation and Message controls for Hunter training levels, major milestones, pet training levels, and pet loyalty gains. + - **Pet Auto-Teach — Growl**: When you tame a new beast, MetaHunt now offers to teach it Growl at the best rank for the pet's level. Click **Yes** and it opens the Beast Training window, teaches the ability and closes the window automatically. - **Animations**: MetaHunt now plays optional animations that make the interface feel alive. Powered by the PizzaSauce library of [Pizzahawaii](https://codeberg.org/Pizzahawaii/) ! diff --git a/MetaHunt.toc b/MetaHunt.toc index 50a0b74..069cdf0 100644 --- a/MetaHunt.toc +++ b/MetaHunt.toc @@ -36,6 +36,7 @@ data\ds-pet-spells.lua data\ds-pet-spells-trainer.lua data\ds-hunter-spells.lua data\ds-hunter-spells-dbc.lua +data\ds-trainer-catalog.lua data\ds-racial.lua data\init.lua diff --git a/README.MD b/README.MD index 8f68b75..ffd4f81 100644 --- a/README.MD +++ b/README.MD @@ -9,9 +9,9 @@ You play the best class, now you have the best companion for it (and its not you ### Important notes -- Altough Nampower isn't mandatory for most of the features, it is a must-have for some. -- If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you should disable them to avoid conflicts. -- MetaHunt is fully compatible with QUIVER, and is not a replacement for it. +- Altough Nampower isn't mandatory for most of the features, it is a must-have for some, and certainly the MM Widget. +- If you were using old versions of Feed-O-Matic, ICU, zHunterMod and HunterHelper, you dont need those with MetaHunt and you SHOULD disable them to avoid conflicts. +- MetaHunt is fully compatible with QUIVER <3, and is not a replacement for it. - The addon purposely disable itself on all Capycraft/Ravencraft Realms, following the DDOS attacks they conducted on other servers. ## Twow Data @@ -36,7 +36,7 @@ Online bestiary is also available here https://DuvelCorp.github.io/MetaHunt-Web/ ## MM Widget - 1.18.1 weapon of mass-destruction. The widget is composed of 5 cells, tracking the 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock & Load procs, and a smart dynamic cell. The smart cell shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any, and it bypasses the proc spell if your current target is immune to the school. You can bind the smart cell to turn the widget into a very efficient one-button rotation. Lets make MM great again ! + 1.18.1 weapon of mass-destruction. NAMPOWER MANDATORY. The widget is composed of 5 cells, tracking the 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock & Load procs, and a smart dynamic cell. The smart cell shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any, and it bypasses the proc spell if your current target is immune to the school. You can bind the smart cell to turn the widget into a very efficient one-button rotation. Lets make MM great again ! ## Tooltips diff --git a/api/core-framework.lua b/api/core-framework.lua index cd17a90..698ce47 100644 --- a/api/core-framework.lua +++ b/api/core-framework.lua @@ -25,6 +25,11 @@ local MTH_MESSAGE_DEFAULTS = { petHungry = false, beastTrainingScan = true, spellbookScan = false, + trainerScan = true, + hunterLevelUp = true, + hunterLevelQuiet = false, + petLevelUp = true, + petLoyaltyUp = true, petRanAway = true, mapMarkers = true, stableScan = true, @@ -216,6 +221,11 @@ function MTH:IsRealmGateBlocked() end function MTH:CheckRealmGate() + -- TEMP TESTING: allow Ravencraft client access while HunterBook layout is reviewed. + if true then + return false + end + -- Realm name is only reliable once logged in; an empty result never blocks. local realm = self:GetCurrentRealmName() if type(realm) == "string" and realm ~= "" then diff --git a/api/core-level-tracking.lua b/api/core-level-tracking.lua new file mode 100644 index 0000000..d0fedaf --- /dev/null +++ b/api/core-level-tracking.lua @@ -0,0 +1,323 @@ +-- MetaHunt level tracker. All training facts come from MTH_DS_TrainerCatalog +-- through trainer-spell-planner.lua; this module only detects progression and +-- presents the resulting facts through chat and animations. + +local MTH_LT_Frame = nil +local MTH_LT_PendingPetCheck = false + +local MTH_LT_HunterLines = { + [1] = "You can now track beasts. Revolutionary: look for the things you intend to shoot.", + [2] = "The wilderness remains deeply unimpressed.", + [3] = "Still mostly leather, arrows, and misplaced confidence.", + [4] = "You have actual trainer lessons now. Try not to spend all your coin on repairs.", + [5] = "Your pet is doing its share. Please consider doing yours.", + [6] = "A hunter's mark: because shouting at the target was not precise enough.", + [7] = "Every pull is a plan until it becomes cardio.", + [8] = "Concussive Shot. Distance is now a tactical concept, not an accident.", + [9] = "Your arrows have begun their long argument with gravity.", + [10] = "You have a hawk aspect. The bird remains unavailable for comment.", + [11] = "A fine age for discovering that pets also require paperwork.", + [12] = "Mend Pet unlocked. Your pet has noticed this was overdue.", + [13] = "The wilderness has upgraded its complaints department.", + [14] = "Eagle Eye. Finally, a way to inspect trouble before walking into it.", + [15] = "You have survived another level through skill, luck, or a pet-shaped shield.", + [16] = "Traps and sharp things. Subtlety continues to be optional.", + [17] = "Your pet is still carrying the emotional weight of this partnership.", + [18] = "Multi-Shot. One target was apparently an unacceptable limitation.", + [19] = "Soon you may run faster. The enemies will take this personally.", + [20] = "Congratulations: escape now has a speed setting.", + [21] = "Fast enough to arrive at the next questionable decision sooner.", + [22] = "Your trainer has new bills and somehow they are all addressed to you.", + [23] = "The pet remains employed despite the working conditions.", + [24] = "Beast Lore. At last: research before violence. Briefly.", + [25] = "Halfway to better excuses, nowhere near fewer mistakes.", + [26] = "Rapid Fire. Your bow has entered its impatient phase.", + [27] = "Things are going well enough to become dangerous.", + [28] = "Your arrows now contain a measurable amount of intent.", + [29] = "Prepare for the hunter's most respected tactical maneuver.", + [30] = "Feign Death. Why win a fight when you can become administrative debris?", + [31] = "You are alive, which validates absolutely nothing.", + [32] = "Flare. Stealth has been downgraded to a suggestion.", + [33] = "The next trap is definitely part of the plan.", + [34] = "Explosive Trap. Negotiation has left the chat.", + [35] = "Your pet thinks this is all normal. That is worrying.", + [36] = "More damage, more maintenance, same heroic disregard for repair bills.", + [37] = "Keep going. The next disaster is probably already pathing toward you.", + [38] = "Your toolkit is becoming impressively hard to explain to civilized people.", + [39] = "One level until armor finally admits you have been taking hits.", + [40] = "Mail training. Leather got you this far; enemies kept putting holes in it.", + [41] = "You are now visibly better protected from consequences.", + [42] = "More Multi-Shot: because collateral damage needed a sequel.", + [43] = "Your pet has grown used to the sound of avoidable danger.", + [44] = "The trainer is delighted. Your coin pouch is less enthusiastic.", + [45] = "You are a seasoned hunter now, which means the mistakes are expensive.", + [46] = "Nature resistance: proof that the outdoors have been escalating.", + [47] = "Still alive. Still pointing the dangerous end away from yourself. Usually.", + [48] = "Your damage has matured into a fully funded public nuisance.", + [49] = "One more level until the trainer starts charging executive prices.", + [50] = "Your arrows have tenure now.", + [51] = "The wilderness would like a word. It has a queue.", + [52] = "Aimed Shot hits harder, which is your preferred form of diplomacy.", + [53] = "Your pet remains your most reliable party member. Make of that what you will.", + [54] = "Explosive Trap again. The floor continues to be a valid target.", + [55] = "Almost there. Try not to die to something with a tutorial tooltip.", + [56] = "Aspect of the Viper. Mana problems now have a bird-shaped solution.", + [57] = "The finish line is visible. So are several elite mobs. Choose carefully.", + [58] = "The final trainer bills are arriving with alarming confidence.", + [59] = "One last level. Your pet has begun drafting a victory speech.", + [60] = "You made it. The wildlife will remember this unfavorably.", +} + +local MTH_LT_Priority = { + ["Feign Death"] = 100, + ["Mail"] = 95, + ["Aspect of the Cheetah"] = 90, + ["Rapid Fire"] = 88, + ["Aspect of the Pack"] = 86, + ["Explosive Trap"] = 84, + ["Freezing Trap"] = 82, + ["Flare"] = 80, + ["Eagle Eye"] = 78, + ["Multi-Shot"] = 76, + ["Aimed Shot"] = 74, + ["Aspect of the Viper"] = 72, + ["Trueshot Aura"] = 70, + ["Volley"] = 68, + ["Track Hidden"] = 66, +} + +local MTH_LT_PetLines = { + "has leveled up. It is now statistically better than your last pull.", + "has leveled up. The food budget has noticed.", + "has leveled up. Please do not celebrate by pulling three extra mobs.", + "has leveled up. It remains committed to solving problems with teeth.", + "has leveled up. The wilderness has filed no objection.", + "has leveled up. Your dependable colleague is carrying this team again.", +} + +local MTH_LT_LoyaltyLines = { + "has decided you are less terrible than expected.", + "has upgraded its opinion of you. Keep the food coming.", + "is learning to trust your decision-making. This may be a mistake.", + "has accepted the arrangement. The arrangement includes snacks.", + "now trusts you more. Do not immediately test this with a cliff.", +} + +local function MTH_LT_Store() + if not MTH_CharSavedVariables then MTH_CharSavedVariables = {} end + if type(MTH_CharSavedVariables.levelTracker) ~= "table" then + MTH_CharSavedVariables.levelTracker = { petLine = 0, loyaltyLine = 0 } + end + return MTH_CharSavedVariables.levelTracker +end + +local function MTH_LT_MessageEnabled(key, defaultValue) + return MTH and type(MTH.IsMessageEnabled) == "function" and MTH:IsMessageEnabled(key, defaultValue) +end + +local function MTH_LT_FormatCost(copper) + local total = tonumber(copper) or 0 + local gold = math.floor(total / 10000) + local silver = math.floor((total - gold * 10000) / 100) + local bronze = total - gold * 10000 - silver * 100 + local parts = {} + if gold > 0 then table.insert(parts, "|cffffd100" .. gold .. "g|r") end + if silver > 0 or gold > 0 then table.insert(parts, "|cffc7c7cf" .. silver .. "s|r") end + table.insert(parts, "|cffeda55f" .. bronze .. "c|r") + return table.concat(parts, " ") +end + +local function MTH_LT_NextLine(store, key, lines) + store[key] = (tonumber(store[key]) or 0) + 1 + if store[key] > table.getn(lines) then store[key] = 1 end + return lines[store[key]] +end + +local function MTH_LT_GetHeadline(plan) + local best = nil + local bestPriority = -1 + for _, entry in ipairs(plan.rows or {}) do + local priority = MTH_LT_Priority[tostring(entry.row and entry.row.name or "")] or 0 + if priority > bestPriority then best, bestPriority = entry, priority end + end + return best +end + +local function MTH_LT_FormatRows(plan) + local labels = {} + for index, entry in ipairs(plan.rows or {}) do + local row = entry.row or {} + local label = tostring(row.name or "Unknown") + if row.rank and row.rank ~= "" then label = label .. " " .. tostring(row.rank) end + table.insert(labels, label) + end + return table.concat(labels, ", ") +end + +local function MTH_LT_PrintSpellList(plan) + local line = "" + local isFirstLine = true + for _, entry in ipairs(plan.rows or {}) do + local row = entry.row or {} + local label = tostring(row.name or "Unknown") + if row.rank and row.rank ~= "" then label = label .. " " .. tostring(row.rank) end + if line == "" then + line = label + elseif string.len(line) + string.len(label) + 2 > 92 then + MTH:Print((isFirstLine and "New spells available: " or "And also: ") .. line) + isFirstLine = false + line = label + else + line = line .. ", " .. label + end + end + if line ~= "" then MTH:Print((isFirstLine and "New spells available: " or "And also: ") .. line) end +end + +local function MTH_LT_AnnounceTraining(kind, level, eventKind, preview) + if type(MTH_TrainerPlan_BuildLevel) ~= "function" then return end + local base = MTH_TrainerPlan_BuildLevel(kind, level, { honored = false, includeKnown = preview }) + local honored = MTH_TrainerPlan_BuildLevel(kind, level, { honored = true, includeKnown = preview }) + if base.unlearned.count <= 0 then + if kind == "hunter" and MTH_LT_MessageEnabled("hunterLevelUp", true) then + MTH:Print("Congratz! You hit level " .. tostring(level) .. ".") + end + if preview then MTH:Print("Level test: no " .. kind .. " trainer spells in the 1.18.1 catalog at level " .. tostring(level) .. ".") end + return + end + local headline = MTH_LT_GetHeadline(base) + local headlineText = headline and headline.row and tostring(headline.row.name or "") or "New training" + if headline and headline.row and headline.row.rank and headline.row.rank ~= "" then headlineText = headlineText .. " " .. headline.row.rank end + local isLevelCap = kind == "hunter" and level == 60 + if isLevelCap then headlineText = "The hunt is yours" end + if MTH_LT_MessageEnabled(eventKind == "hunter" and "hunterLevelUp" or "petLevelUp", true) then + if kind == "hunter" then + MTH:Print("Congratz! You hit level " .. tostring(level) .. ".") + MTH:Print(MTH_LT_HunterLines[level] or "New training is available.") + MTH:Print("The spells you can learn will cost you " .. MTH_LT_FormatCost(base.unlearned.cost) + .. ", but only " .. MTH_LT_FormatCost(honored.unlearned.cost) .. " if you are Honored with the master.") + MTH_LT_PrintSpellList(base) + else + MTH:Print("" .. MTH_LT_FormatRows(base)) + MTH:Print("Trainer cost: Base " .. MTH_LT_FormatCost(base.unlearned.cost) .. " | Honored " .. MTH_LT_FormatCost(honored.unlearned.cost)) + end + end + local isMilestone = kind == "hunter" and (level == 10 or level == 20 or level == 30 or level == 40 or level == 50 or level == 60) + local flag = isMilestone and "level.huntermilestone" or (kind == "hunter" and "level.hunter" or "level.pet") + if type(MTH_FX_IsEnabled) == "function" and MTH_FX_IsEnabled(flag) and type(MTH_Celebrate) == "function" then + local subtitle = kind == "hunter" and (MTH_LT_HunterLines[level] or "New training is available.") or "New pet training is available. The trainer wants coin." + if isMilestone and type(MTH_CelebrateBlackHole) == "function" then + MTH_CelebrateBlackHole("Level " .. tostring(level) .. " - " .. headlineText, subtitle) + else + MTH_Celebrate("Level " .. tostring(level) .. " - " .. headlineText, subtitle, "fanfare", 4.8) + end + end +end + +local function MTH_LT_HandleHunterLevel(level) + local store = MTH_LT_Store() + local previous = tonumber(store.hunterLevel) + store.hunterLevel = level + if previous and level > previous then + MTH_LT_AnnounceTraining("hunter", level, "hunter") + end +end + +local function MTH_LT_GetPetState() + if type(UnitExists) ~= "function" or not UnitExists("pet") then return nil end + local level = type(UnitLevel) == "function" and tonumber(UnitLevel("pet")) or nil + local name = type(UnitName) == "function" and UnitName("pet") or "Your pet" + if not level or level <= 0 then return nil end + local loyalty = nil + local getPetLoyalty = (type(getglobal) == "function" and getglobal("GetPetLoyalty")) or (_G and _G["GetPetLoyalty"]) + if type(getPetLoyalty) == "function" then loyalty = tonumber(getPetLoyalty()) end + return { key = tostring(name or "pet"), name = tostring(name or "Your pet"), level = level, loyalty = loyalty } +end + +local function MTH_LT_HandlePetState() + MTH_LT_PendingPetCheck = false + local state = MTH_LT_GetPetState() + if not state then return end + local store = MTH_LT_Store() + local pet = store.pet + if type(pet) ~= "table" or pet.key ~= state.key then + store.pet = state + return + end + if state.level > (tonumber(pet.level) or 0) then + local line = MTH_LT_NextLine(store, "petLine", MTH_LT_PetLines) + if MTH_LT_MessageEnabled("petLevelUp", true) then MTH:Print(state.name .. " reached level " .. state.level .. ". " .. line) end + MTH_LT_AnnounceTraining("pet", state.level, "pet") + end + if state.loyalty and state.loyalty > (tonumber(pet.loyalty) or 0) then + local line = MTH_LT_NextLine(store, "loyaltyLine", MTH_LT_LoyaltyLines) + if state.loyalty == 6 then line = "is your Best Friend. Against all evidence, it trusts you completely." end + if MTH_LT_MessageEnabled("petLoyaltyUp", true) then MTH:Print(state.name .. " reached Loyalty Level " .. state.loyalty .. ". " .. line) end + if type(MTH_FX_IsEnabled) == "function" and MTH_FX_IsEnabled("level.loyalty") and type(MTH_Celebrate) == "function" then + MTH_Celebrate(state.name .. " - Loyalty " .. state.loyalty, line, state.loyalty == 6 and "victory" or "fanfare") + end + end + store.pet = state +end + +local function MTH_LT_RequestPetCheck() + if MTH_LT_PendingPetCheck or not MTH_LT_Frame then return end + MTH_LT_PendingPetCheck = true + MTH_LT_Frame.elapsed = 0 + MTH_LT_Frame:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0) + if this.elapsed >= 0.35 then + this:SetScript("OnUpdate", nil) + MTH_LT_HandlePetState() + end + end) +end + +function MTH_LevelTracker_Test(kind, level) + if kind == "hunter" or kind == "pet" then + local targetLevel = tonumber(level) or ((kind == "hunter") and (tonumber(UnitLevel("player")) or 1) or 1) + MTH:Print("Level test: previewing " .. kind .. " trainer spells at level " .. tostring(targetLevel) .. ".") + MTH_LT_AnnounceTraining(kind, targetLevel, kind, true) + return true + end + if kind == "loyalty" then + local state = MTH_LT_GetPetState() + local loyalty = tonumber(level) or (state and state.loyalty) or 1 + local petName = state and state.name or "Your pet" + local line = loyalty == 6 and "is your Best Friend. Against all evidence, it trusts you completely." + or MTH_LT_LoyaltyLines[1] + MTH:Print(petName .. " reached Loyalty Level " .. tostring(loyalty) .. ". " .. line) + if type(MTH_Celebrate) == "function" then + MTH_Celebrate(petName .. " - Loyalty " .. tostring(loyalty), line, loyalty == 6 and "victory" or "fanfare") + end + return true + end + return false +end + +function MTH_LevelTracker_Initialize() + if MTH_LT_Frame then return end + MTH_LT_Frame = CreateFrame("Frame", "MTH_LevelTracker") + MTH_LT_Frame:RegisterEvent("PLAYER_ENTERING_WORLD") + MTH_LT_Frame:RegisterEvent("PLAYER_LEVEL_UP") + MTH_LT_Frame:RegisterEvent("UNIT_LEVEL") + MTH_LT_Frame:RegisterEvent("UNIT_PET") + MTH_LT_Frame:RegisterEvent("PET_BAR_UPDATE") + MTH_LT_Frame:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + local level = type(UnitLevel) == "function" and tonumber(UnitLevel("player")) or nil + if level then MTH_LT_Store().hunterLevel = level end + MTH_LT_RequestPetCheck() + elseif event == "PLAYER_LEVEL_UP" then + MTH_LT_HandleHunterLevel(tonumber(arg1) or tonumber(UnitLevel("player")) or 1) + elseif event == "UNIT_LEVEL" and arg1 == "pet" then + MTH_LT_RequestPetCheck() + elseif event == "UNIT_PET" and arg1 == "player" then + MTH_LT_RequestPetCheck() + elseif event == "PET_BAR_UPDATE" then + MTH_LT_RequestPetCheck() + end + end) +end + +MTH_LevelTracker_Initialize() \ No newline at end of file diff --git a/api/core-scan-trainer.lua b/api/core-scan-trainer.lua new file mode 100644 index 0000000..68b8aab --- /dev/null +++ b/api/core-scan-trainer.lua @@ -0,0 +1,251 @@ +-- MetaHunt: one-time live trainer catalog collector. +-- Developer workflow: /mth trainerscan, then open a Hunter or Pet Trainer. + +local MTH_TS_Frame +local MTH_TS_Armed = false +local MTH_TS_AutoArmed = false + +local function MTH_TS_Log(text) + if MTH_DebugFrame and type(MTH_DebugFrame.AddInfo) == "function" then + MTH_DebugFrame:AddInfo("[TrainerScan] " .. tostring(text)) + end +end + +local function MTH_TS_Call(name, ...) + local fn = getglobal and getglobal(name) or (_G and _G[name]) + if type(fn) ~= "function" then return nil, "missing" end + local ok, a, b, c, d, e, f, g = pcall(fn, unpack(arg)) + if not ok then return nil, "error=" .. tostring(a) end + return { a, b, c, d, e, f, g }, nil +end + +local function MTH_TS_Values(values) + if not values then return "" end + local parts = {} + local i = 1 + while i <= table.getn(values) do + if values[i] ~= nil then + table.insert(parts, tostring(values[i])) + end + i = i + 1 + end + return table.concat(parts, " | ") +end + +local function MTH_TS_TargetName() + if type(UnitName) == "function" then + local name = UnitName("target") + if name and name ~= "" then return name end + end + local title = getglobal and getglobal("TrainerFrameTitleText") or nil + if title and title.GetText then return title:GetText() or "?" end + return "?" +end + +local function MTH_TS_GetLiveStatusStore() + if type(MTH_CharSavedVariables) ~= "table" then MTH_CharSavedVariables = {} end + if type(MTH_CharSavedVariables.trainerLiveStatus) ~= "table" then + MTH_CharSavedVariables.trainerLiveStatus = {} + end + return MTH_CharSavedVariables.trainerLiveStatus +end + +local function MTH_TS_GetCatalogKind(name, rank) + local catalog = MTH_DS_TrainerCatalog + if type(catalog) ~= "table" or type(catalog.byNameRank) ~= "table" then return nil end + local keySuffix = "\031" .. tostring(name or "") .. "\031" .. tostring(rank or "") + if catalog.byNameRank["hunter" .. keySuffix] then return "hunter" end + if catalog.byNameRank["pet" .. keySuffix] then return "pet" end + return nil +end + +function MTH_GetTrainerLiveStatus(kind, name, rank) + local store = type(MTH_CharSavedVariables) == "table" and MTH_CharSavedVariables.trainerLiveStatus or nil + if type(store) ~= "table" then return nil end + return store[tostring(kind or "") .. "\031" .. tostring(name or "") .. "\031" .. tostring(rank or "")] +end + +local function MTH_TS_CollectLiveStatuses() + local countValues = MTH_TS_Call("GetNumTrainerServices") + local total = countValues and tonumber(countValues[1]) or 0 + if total <= 0 then return false end + + local trainerName = MTH_TS_TargetName() + local store = MTH_TS_GetLiveStatusStore() + local observed = 0 + for index = 1, total do + local info = MTH_TS_Call("GetTrainerServiceInfo", index) + local name = info and info[1] or nil + local rank = info and info[2] or "" + local status = info and info[3] or nil + local kind = MTH_TS_GetCatalogKind(name, rank) + if kind and (status == "available" or status == "unavailable" or status == "used") then + local key = kind .. "\031" .. tostring(name) .. "\031" .. tostring(rank) + store[key] = { + status = status, + observedAt = time(), + trainer = trainerName, + } + observed = observed + 1 + end + end + + if observed > 0 then + MTH_TS_Log("Live trainer overlay: " .. tostring(observed) .. " observed catalog row(s) from " .. tostring(trainerName) .. ".") + if MTH and MTH.Print and (not MTH.IsMessageEnabled or MTH:IsMessageEnabled("trainerScan", true)) then + MTH:Print("Trainer scan: " .. tostring(observed) .. " Hunter/Pet spell status" .. (observed == 1 and "" or "es") .. " observed.") + end + return true + end + return false +end + +local function MTH_TS_Collect() + local countValues, countError = MTH_TS_Call("GetNumTrainerServices") + local total = countValues and tonumber(countValues[1]) or 0 + if not total or total <= 0 then + MTH_TS_Log("No trainer services yet. GetNumTrainerServices=" .. tostring(MTH_TS_Values(countValues)) + .. " " .. tostring(countError or "")) + return false + end + + local trainerName = MTH_TS_TargetName() + local capture = { + trainer = trainerName, + capturedAt = time(), + services = {}, + } + MTH_TRAINER_SCAN_LAST = capture + if type(MTH_CharSavedVariables) == "table" then + if type(MTH_CharSavedVariables.trainerCatalogScans) ~= "table" then + MTH_CharSavedVariables.trainerCatalogScans = {} + end + MTH_CharSavedVariables.trainerCatalogScans[trainerName] = capture + end + MTH_TS_Log("=== " .. tostring(capture.trainer) .. " | " .. tostring(total) .. " trainer services ===") + + local index = 1 + while index <= total do + local info, infoError = MTH_TS_Call("GetTrainerServiceInfo", index) + local icon, iconError = MTH_TS_Call("GetTrainerServiceIcon", index) + local cost, costError = MTH_TS_Call("GetTrainerServiceCost", index) + local level, levelError = MTH_TS_Call("GetTrainerServiceLevelReq", index) + local skill, skillError = MTH_TS_Call("GetTrainerServiceSkillReq", index) + local description, descriptionError = MTH_TS_Call("GetTrainerServiceDescription", index) + local reqCountValues = MTH_TS_Call("GetTrainerServiceNumAbilityReq", index) + local reqCount = reqCountValues and tonumber(reqCountValues[1]) or 0 + local requirements = {} + local reqIndex = 1 + while reqIndex <= reqCount do + local requirement, reqError = MTH_TS_Call("GetTrainerServiceAbilityReq", index, reqIndex) + table.insert(requirements, { + name = requirement and requirement[1] or nil, + flag = requirement and requirement[2] or nil, + error = reqError, + }) + reqIndex = reqIndex + 1 + end + + local row = { + index = index, + name = info and info[1] or nil, + rank = info and info[2] or nil, + status = info and info[3] or nil, + expanded = info and info[4] or nil, + icon = icon and icon[1] or nil, + cost = cost and cost[1] or nil, + requiredLevel = level and level[1] or nil, + skillRequirement = skill, + description = description and description[1] or nil, + abilityRequirements = requirements, + } + table.insert(capture.services, row) + + MTH_TS_Log(string.format("%02d name=%s | rank=%s | status=%s | expanded=%s | level=%s | cost=%s | icon=%s", + index, tostring(row.name), tostring(row.rank), tostring(row.status), tostring(row.expanded), + tostring(row.requiredLevel), tostring(row.cost), tostring(row.icon))) + if row.description and row.description ~= "" then + MTH_TS_Log(" description=" .. tostring(row.description)) + end + if table.getn(requirements) > 0 then + local requirementText = {} + local requirementIndex = 1 + while requirementIndex <= table.getn(requirements) do + local requirement = requirements[requirementIndex] + table.insert(requirementText, tostring(requirement.name) + .. " [flag=" .. tostring(requirement.flag) .. "]") + requirementIndex = requirementIndex + 1 + end + MTH_TS_Log(" abilityRequirements=" .. table.concat(requirementText, ", ")) + end + if skill and table.getn(skill) > 0 then + MTH_TS_Log(" skillRequirement=" .. MTH_TS_Values(skill)) + end + if infoError or iconError or costError or levelError or skillError or descriptionError then + MTH_TS_Log(" API=" .. tostring(infoError or "ok") .. ", " .. tostring(iconError or "ok") + .. ", " .. tostring(costError or "ok") .. ", " .. tostring(levelError or "ok") + .. ", " .. tostring(skillError or "ok") .. ", " .. tostring(descriptionError or "ok")) + end + index = index + 1 + end + + MTH_TS_Log("=== Capture complete. " .. tostring(total) + .. " rows retained in MTH_TRAINER_SCAN_LAST and trainerCatalogScans. ===") + return true +end + +local function MTH_TS_EnsureFrame() + if not MTH_TS_Frame then + MTH_TS_Frame = CreateFrame("Frame", "MTH_TrainerCatalogScanner") + MTH_TS_Frame:SetScript("OnEvent", function() + if event == "TRAINER_SHOW" then + MTH_TS_AutoArmed = true + this.elapsed = 0 + this:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0) + if this.elapsed < 0.25 then return end + this:SetScript("OnUpdate", nil) + MTH_TS_CollectLiveStatuses() + end) + return + end + if event ~= "TRAINER_LIST_UPDATE" or not (MTH_TS_Armed or MTH_TS_AutoArmed) then return end + this.elapsed = 0 + this:SetScript("OnUpdate", function() + this.elapsed = (this.elapsed or 0) + (arg1 or 0) + if this.elapsed < 0.25 then return end + this:SetScript("OnUpdate", nil) + local manualArmed = MTH_TS_Armed + MTH_TS_Armed = false + MTH_TS_AutoArmed = false + MTH_TS_CollectLiveStatuses() + if manualArmed and not MTH_TS_Collect() and MTH and MTH.Print then + MTH:Print("Trainer list was empty. Open the trainer again and run /mth trainerscan.") + end + end) + end) + MTH_TS_Frame:RegisterEvent("TRAINER_SHOW") + MTH_TS_Frame:RegisterEvent("TRAINER_LIST_UPDATE") + end +end + +function MTH_CommandTrainerScan() + if MTH_DebugFrame and type(MTH_DebugFrame.Show) == "function" then MTH_DebugFrame:Show() end + local currentRows = MTH_TS_Call("GetNumTrainerServices") + if currentRows and (tonumber(currentRows[1]) or 0) > 0 then + MTH_TS_Collect() + if MTH and MTH.Print then + MTH:Print("Trainer catalog captured from the open trainer window.") + end + return + end + + MTH_TS_EnsureFrame() + + MTH_TS_Armed = true + if MTH and MTH.Print then + MTH:Print("Trainer catalog scan armed. Open a Hunter Trainer or Pet Trainer; results will appear in the debug window.") + end +end + +MTH_TS_EnsureFrame() \ No newline at end of file diff --git a/api/core.lua b/api/core.lua index f1f06b5..89299e3 100644 --- a/api/core.lua +++ b/api/core.lua @@ -331,15 +331,40 @@ function SlashCmdList.MTH(msg, editbox) else MTH:Print("Train scan is not available.") end + elseif lowerCmd == "trainerscan" then + if type(MTH_CommandTrainerScan) == "function" then + MTH_CommandTrainerScan() + else + MTH:Print("Trainer catalog scanner is not available.") + end + elseif lowerCmd == "spellscan" then + if type(MTH_HT_RescanSpellbook) ~= "function" then + MTH:Print("Hunter spellbook scanner is not available.") + return + end + MTH_HT_RescanSpellbook() + local mail = type(MTH_HT_GetSpellInfo) == "function" and MTH_HT_GetSpellInfo("Mail") or nil + if mail then + MTH:Print("Hunter spellbook scan: Mail found (rank=" .. tostring(mail.rank or "") .. ", slot=" .. tostring(mail.slot or "?") .. ").") + else + MTH:Print("Hunter spellbook scan: Mail was not returned by the client spellbook API.") + end + elseif lowerCmd == "leveltest" then + local _, _, kind, levelText = string.find(lowerArg or "", "^(%S+)%s*(%d*)") + if type(MTH_LevelTracker_Test) ~= "function" then + MTH:Print("Level tracker is not available.") + elseif not MTH_LevelTracker_Test(kind, tonumber(levelText)) then + MTH:Print("Use /mth leveltest hunter [level], pet [level], or loyalty [level]") + end elseif lowerCmd == "fx" or lowerCmd == "fireworks" or lowerCmd == "celebrate" then - -- TEMP: preview the pet-ability celebration effect. - if type(MTH_Celebrate) == "function" then - local label = msg - local _, _, rest = string.find(msg, "^%S+%s+(.-)%s*$") - if rest and rest ~= "" then - MTH_Celebrate("New Pet Ability!", rest) - else - MTH_Celebrate("New Pet Ability!", "Growl (Rank 3)") + if type(MTH_CelebratePreview) == "function" then + local style = lowerArg or "" + if style == "" then style = "firework" end + if style == "help" or style == "list" then + MTH:Print("Celebrations: firework, blackhole, shockwave, shatter, all") + elseif not MTH_CelebratePreview(style) then + MTH:Print("Unknown celebration: " .. tostring(style)) + MTH:Print("Use /mth fx help for: firework, blackhole, shockwave, shatter, all") end else -- Self-diagnosing: report which piece failed to load so we can tell diff --git a/api/fx-celebrate.lua b/api/fx-celebrate.lua index 3649c85..2bb1fff 100644 --- a/api/fx-celebrate.lua +++ b/api/fx-celebrate.lua @@ -33,6 +33,7 @@ MTH_FX_DBG = { -- Sections shown, in order, in the options panel. `titleKey` localizes `title`. MTH_FX_ANIM_SECTIONS = { { id = "pet", title = "Pet", titleKey = "ANIM_SECTION_PET" }, + { id = "level", title = "Leveling", titleKey = "ANIM_SECTION_LEVEL" }, { id = "combat", title = "Combat", titleKey = "ANIM_SECTION_COMBAT" }, { id = "book", title = "Book", titleKey = "ANIM_SECTION_BOOK" }, { id = "ui", title = "Interface", titleKey = "ANIM_SECTION_UI" }, @@ -43,6 +44,18 @@ MTH_FX_ANIM_SECTIONS = { -- labelKey/tooltipKey localize label/tooltip (resolved at render time). -- Adding a new animation = one entry here + one MTH_FX_IsEnabled("key") guard. MTH_FX_ANIM_DEFS = { + { key = "level.hunter", section = "level", default = true, + label = "Celebrate Hunter training levels", + tooltip = "Show a banner and burst when a level unlocks Hunter trainer spells." }, + { key = "level.huntermilestone", section = "level", default = true, + label = "Celebrate Hunter milestones", + tooltip = "Use a large celebration at levels 10, 20, 30, 40, 50, and 60." }, + { key = "level.pet", section = "level", default = true, + label = "Celebrate pet training levels", + tooltip = "Show a banner when your current pet gains a level with Pet Trainer spells." }, + { key = "level.loyalty", section = "level", default = true, + label = "Celebrate pet loyalty gains", + tooltip = "Show a golden banner when your current pet gains a loyalty level." }, { key = "pet.celebrate", section = "pet", default = true, label = "Celebrate new pet abilities", labelKey = "ANIM_PET_CELEBRATE", tooltip = "Play a firework and a banner in the middle of the screen when your pet learns a new ability or a new rank.", @@ -244,6 +257,27 @@ if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasShake t MTH_FX_DBG.shakeErr = err end +if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasSpiral then + local ok, err = pcall(function() + Sauce.MTH_HasSpiral = true + Sauce:RegisterType("mth_spiral", { + init = function(target, from) + return from or 0 + end, + apply = function(target, from, to, t, tween) + local radius = (tween.radius or 0) * (1 - t) + local angle = (tween.startAngle or 0) + (tween.rotations or 1) * 2 * math.pi * t + target:ClearAllPoints() + target:SetPoint("CENTER", tween.anchor or UIParent, "CENTER", + (tween.cx or 0) + math.cos(angle) * radius, + (tween.cy or 0) + math.sin(angle) * radius) + end, + }) + end) + MTH_FX_DBG.spiralOk = ok + MTH_FX_DBG.spiralErr = err +end + -- Reusable flash: overlays a short additive colour glow on any frame/button and -- fades it out. Safe on any frame that supports CreateTexture; silent no-op -- otherwise. The overlay texture is cached on the frame for reuse. @@ -277,11 +311,15 @@ end local MTH_FX_NUM_SPARKS = 22 +local MTH_FX_NUM_CONFETTI = 48 +local MTH_FX_NUM_BLACKHOLE = 40 local MTH_FX_Container local MTH_FX_MsgFrame local MTH_FX_TitleFS local MTH_FX_SubFS local MTH_FX_Sparks = {} +local MTH_FX_Confetti = {} +local MTH_FX_BlackHole = {} -- Palette for the firework sparks (r, g, b), warm celebratory tones. local MTH_FX_Colors = { @@ -337,33 +375,75 @@ local function MTH_FX_EnsureFrames() i = i + 1 end + i = 1 + while i <= MTH_FX_NUM_CONFETTI do + local bit = MTH_FX_Container:CreateTexture(nil, "OVERLAY") + bit:SetTexture("Interface\\Buttons\\WHITE8X8") + bit:SetWidth(5) + bit:SetHeight(8) + bit:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0) + bit:Hide() + MTH_FX_Confetti[i] = bit + i = i + 1 + end + + i = 1 + while i <= MTH_FX_NUM_BLACKHOLE do + local particle = MTH_FX_Container:CreateTexture(nil, "OVERLAY") + particle:SetTexture("Interface\\Cooldown\\star4") + particle:SetWidth(10) + particle:SetHeight(10) + particle:SetBlendMode("ADD") + particle:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0) + particle:Hide() + MTH_FX_BlackHole[i] = particle + i = i + 1 + end + return true end --- Public entry point: play a celebratory message + firework burst in the middle --- of the screen. `title` and `subtitle` are optional strings. -function MTH_Celebrate(title, subtitle) +-- Public entry point: play a full-screen celebration. `style` defaults to the +-- original firework burst and may be firework, victory, starfall, fanfare, or comet. +function MTH_Celebrate(title, subtitle, style, displaySeconds) if not Sauce then return end if not MTH_FX_EnsureFrames() then return end + style = string.lower(tostring(style or "firework")) + local fadeDelay = tonumber(displaySeconds) or 2.1 + if fadeDelay < 0.5 then fadeDelay = 0.5 end - MTH_FX_TitleFS:SetTextColor(1, 0.82, 0) + if style == "starfall" then + MTH_FX_TitleFS:SetTextColor(0.45, 0.80, 1.00) + else + MTH_FX_TitleFS:SetTextColor(1, 0.82, 0) + end MTH_FX_TitleFS:SetText(title or "") MTH_FX_SubFS:SetText(subtitle or "") Sauce:Stop(MTH_FX_MsgFrame) MTH_FX_MsgFrame:SetAlpha(0) MTH_FX_MsgFrame:Show() - Sauce:Sequence({ - Sauce:Group({ - Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.25), - Sauce:Pulse(MTH_FX_MsgFrame, 1.22, 0.5), - }), - Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = 2.2 }), - }) + if style == "victory" or style == "fanfare" then + Sauce:Sequence({ + Sauce:Group({ + Sauce:SlideIn(MTH_FX_MsgFrame, "UP", 80, 0.36, "outBack", { defer = true }), + Sauce:Pulse(MTH_FX_MsgFrame, style == "victory" and 1.32 or 1.18, 0.52, { defer = true }), + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = fadeDelay }), + }) + else + Sauce:Sequence({ + Sauce:Group({ + Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.25), + Sauce:Pulse(MTH_FX_MsgFrame, 1.22, 0.5), + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.6, "inQuad", { delay = fadeDelay }), + }) + end MTH_FX_Container:Show() local colorCount = table.getn(MTH_FX_Colors) @@ -371,34 +451,70 @@ function MTH_Celebrate(title, subtitle) local i = 1 while i <= MTH_FX_NUM_SPARKS do local spark = MTH_FX_Sparks[i] + spark:SetTexture("Interface\\Cooldown\\star4") + spark:SetTexCoord(0, 1, 0, 1) + local startX, startY = 0, 0 + local delay, gravity = 0, 300 local angle = (i / MTH_FX_NUM_SPARKS) * 2 * math.pi + (math.random() - 0.5) * 0.4 local speed = 150 + math.random() * 130 local vx = math.cos(angle) * speed local vy = math.sin(angle) * speed + 70 local dur = 0.9 + math.random() * 0.4 + if style == "victory" then + startX = ((i - math.floor(i / 2) * 2) == 0) and -130 or 130 + speed = 100 + math.random() * 100 + vx = math.cos(angle) * speed + vy = math.sin(angle) * speed + 105 + elseif style == "starfall" then + startX = -230 + math.random() * 460 + startY = 200 + math.random() * 100 + vx = -35 + math.random() * 70 + vy = -10 + math.random() * 25 + gravity = 235 + dur = 1.1 + math.random() * 0.5 + elseif style == "fanfare" then + delay = ((i - 1) - math.floor((i - 1) / 3) * 3) * 0.18 + speed = 110 + math.random() * 90 + vx = math.cos(angle) * speed + vy = math.sin(angle) * speed + 90 + elseif style == "comet" then + if i == 1 then + startY = -180 + vx, vy, gravity, dur = 0, 430, 0, 0.52 + else + delay = 0.5 + startY = 45 + speed = 130 + math.random() * 120 + vx = math.cos(angle) * speed + vy = math.sin(angle) * speed + 70 + end + end + colorIdx = colorIdx + 1 if colorIdx > colorCount then colorIdx = 1 end local color = MTH_FX_Colors[colorIdx] spark:SetVertexColor(color[1], color[2], color[3]) - spark:SetAlpha(1) + spark:SetAlpha(delay > 0 and 0 or 1) spark:Show() Sauce:Stop(spark) Sauce:Group({ Sauce:Tween(spark, { type = "mth_ballistic", - from = { 0, 0 }, + from = { startX, startY }, duration = dur, easing = "linear", + delay = delay, anchor = MTH_FX_Container, dur = dur, vx = vx, vy = vy, - gravity = 300, + gravity = gravity, }), + Sauce:FadeTo(spark, 1, 0.05, "outQuad", { delay = delay }), Sauce:FadeTo(spark, 0, 0.45, "inQuad", { - delay = dur - 0.45, + delay = delay + dur - 0.45, onFinish = function() spark:Hide() end, @@ -408,6 +524,203 @@ function MTH_Celebrate(title, subtitle) end end +local function MTH_FX_ShowPreviewBanner(title, subtitle, r, g, b) + MTH_FX_TitleFS:SetTextColor(r, g, b) + MTH_FX_TitleFS:SetText(title) + MTH_FX_SubFS:SetText(subtitle) + Sauce:Stop(MTH_FX_MsgFrame) + MTH_FX_MsgFrame:SetAlpha(0) + MTH_FX_MsgFrame:Show() + Sauce:Sequence({ + Sauce:Group({ + Sauce:FadeTo(MTH_FX_MsgFrame, 1, 0.18), + Sauce:Pulse(MTH_FX_MsgFrame, 1.18, 0.45), + }), + Sauce:FadeOut(MTH_FX_MsgFrame, 0.55, "inQuad", { delay = 2.1 }), + }) +end + +function MTH_CelebrateConfetti(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Celebration!", subtitle or "Confetti", 1, 0.82, 0.15) + MTH_FX_Container:Show() + local count = table.getn(MTH_FX_Colors) + local i = 1 + while i <= MTH_FX_NUM_CONFETTI do + local bit = MTH_FX_Confetti[i] + local color = MTH_FX_Colors[((i - 1) - math.floor((i - 1) / count) * count) + 1] + local size = 3 + math.random() * 5 + local angle = math.random() * 2 * math.pi + local speed = 90 + math.random() * 210 + local dur = 1.25 + math.random() * 0.7 + bit:SetWidth(size) + bit:SetHeight(size * 1.6) + bit:SetVertexColor(color[1], color[2], color[3]) + bit:SetAlpha(1) + bit:Show() + Sauce:Stop(bit) + Sauce:Group({ + Sauce:Tween(bit, { + type = "mth_ballistic", from = { 0, 0 }, duration = dur, + easing = "linear", anchor = MTH_FX_Container, dur = dur, + vx = math.cos(angle) * speed, vy = math.sin(angle) * speed + 100, gravity = 255, + }), + Sauce:FadeTo(bit, 0, 0.35, "inQuad", { + delay = dur - 0.35, onFinish = function() bit:Hide() end, + }), + }) + i = i + 1 + end +end + +function MTH_CelebrateBlackHole(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Gravity Well", subtitle or "Black Hole", 0.68, 0.45, 1.00) + MTH_FX_Container:Show() + local colors = { { 0.45, 0.25, 1 }, { 0.30, 0.50, 1 }, { 0.80, 0.55, 1 } } + local i = 1 + while i <= MTH_FX_NUM_BLACKHOLE do + local particle = MTH_FX_BlackHole[i] + local color = colors[((i - 1) - math.floor((i - 1) / 3) * 3) + 1] + local startAngle = math.random() * 2 * math.pi + local radius = 95 + math.random() * 145 + local duration = 1.5 + math.random() * 1.0 + local size = 7 + math.random() * 8 + particle:SetWidth(size) + particle:SetHeight(size) + particle:SetVertexColor(color[1], color[2], color[3]) + particle:SetAlpha(1) + particle:Show() + Sauce:Stop(particle) + Sauce:Group({ + Sauce:Tween(particle, { + type = "mth_spiral", from = 0, to = 1, duration = duration, + easing = "inQuad", anchor = MTH_FX_Container, cx = 0, cy = 0, + radius = radius, startAngle = startAngle, rotations = 1.5 + math.random() * 2.0, + }), + Sauce:Size(particle, { size, size }, { 2, 2 }, duration, "inQuad"), + Sauce:FadeTo(particle, 0, duration * 0.3, "inQuad", { + delay = duration * 0.7, onFinish = function() particle:Hide() end, + }), + }) + i = i + 1 + end +end + +function MTH_CelebrateShockwave(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Impact!", subtitle or "Shockwave", 1, 0.62, 0.15) + MTH_FX_Container:Show() + local dotsPerWave = 16 + local i = 1 + while i <= MTH_FX_NUM_CONFETTI do + local dot = MTH_FX_Confetti[i] + local wave = math.floor((i - 1) / dotsPerWave) + 1 + local pos = (i - 1) - math.floor((i - 1) / dotsPerWave) * dotsPerWave + local angle = pos / dotsPerWave * 2 * math.pi + local duration = 1.1 + wave * 0.18 + local radius = 115 + wave * 52 + local delay = (wave - 1) * 0.18 + local intensity = 1 - (wave - 1) * 0.20 + dot:SetWidth(5) + dot:SetHeight(5) + dot:SetVertexColor(1 * intensity, 0.65 * intensity, 0.18 * intensity) + dot:SetAlpha(0) + dot:Show() + Sauce:Stop(dot) + Sauce:Group({ + Sauce:Tween(dot, { + type = "mth_ballistic", from = { 0, 0 }, duration = duration, + easing = "outQuad", delay = delay, anchor = MTH_FX_Container, dur = duration, + vx = math.cos(angle) * radius / duration, vy = math.sin(angle) * radius / duration, gravity = 0, + }), + Sauce:FadeTo(dot, 1, 0.04, "outQuad", { delay = delay }), + Sauce:FadeTo(dot, 0, 0.4, "inQuad", { + delay = delay + duration - 0.4, onFinish = function() dot:Hide() end, + }), + }) + i = i + 1 + end +end + +function MTH_CelebrateShatter(title, subtitle) + if not Sauce or not MTH_FX_EnsureFrames() then return end + MTH_FX_ShowPreviewBanner(title or "Shattered!", subtitle or "Shatter", 0.70, 0.85, 1.00) + MTH_FX_Container:Show() + local grid = 4 + local iconSize = 80 + local fragmentSize = iconSize / grid + local i = 1 + while i <= MTH_FX_NUM_SPARKS do + local spark = MTH_FX_Sparks[i] + if i <= grid * grid then + local row = math.floor((i - 1) / grid) + local col = (i - 1) - row * grid + local x = (col - grid / 2 + 0.5) * fragmentSize + local y = ((grid - 1 - row) - grid / 2 + 0.5) * fragmentSize + local duration = 1.1 + math.random() * 0.45 + spark:SetTexture("Interface\\Icons\\Ability_Hunter_SniperShot") + spark:SetTexCoord(col / grid, (col + 1) / grid, row / grid, (row + 1) / grid) + spark:SetWidth(fragmentSize) + spark:SetHeight(fragmentSize) + spark:SetVertexColor(1, 1, 1) + spark:SetAlpha(1) + spark:Show() + Sauce:Stop(spark) + Sauce:Group({ + Sauce:Tween(spark, { + type = "mth_ballistic", from = { x, y }, duration = duration, + easing = "linear", anchor = MTH_FX_Container, dur = duration, + vx = x * 4 + (math.random() - 0.5) * 90, + vy = y * 4 + 95 + math.random() * 80, gravity = 265, + }), + Sauce:FadeTo(spark, 0, 0.35, "inQuad", { + delay = duration - 0.35, onFinish = function() spark:Hide() end, + }), + }) + end + i = i + 1 + end +end + +function MTH_CelebratePreview(style) + style = string.lower(tostring(style or "firework")) + if style == "all" then + if not Sauce then return end + local styles = { "firework", "blackhole", "shockwave", "shatter" } + local steps = {} + local i = 1 + while i <= table.getn(styles) do + local previewStyle = styles[i] + table.insert(steps, Sauce:Run(function() + MTH_CelebratePreview(previewStyle) + end, { defer = true })) + table.insert(steps, Sauce:Delay(3.7)) + i = i + 1 + end + Sauce:Sequence(steps) + return true + end + + if style == "blackhole" then + MTH_CelebrateBlackHole("Celebration Preview", "Black Hole") + return true + elseif style == "shockwave" then + MTH_CelebrateShockwave("Celebration Preview", "Shockwave") + return true + elseif style == "shatter" then + MTH_CelebrateShatter("Celebration Preview", "Shatter") + return true + end + + local labels = { + firework = "Firework Burst", + } + if not labels[style] then return false end + MTH_Celebrate("Celebration Preview", labels[style], style) + return true +end + -- Convenience wrapper used when the hunter's pet learns a new ability/rank. function MTH_CelebratePetAbility(label) if not MTH_FX_IsEnabled("pet.celebrate") then diff --git a/api/hunterbook-tab-trainers.lua b/api/hunterbook-tab-trainers.lua new file mode 100644 index 0000000..240aac9 --- /dev/null +++ b/api/hunterbook-tab-trainers.lua @@ -0,0 +1,225 @@ +if type(MTH_HUNTERBOOK_TABS) ~= "table" then MTH_HUNTERBOOK_TABS = {} end + +local trainerColumns = { + { x = 28, width = 150, align = "LEFT" }, + { x = 180, width = 62, align = "LEFT" }, + { x = 244, width = 43, align = "LEFT" }, + { x = 289, width = 80, align = "LEFT" }, + { x = 371, width = 105, align = "LEFT" }, + { x = 478, width = 74, align = "LEFT" }, +} + +MTH_HUNTERBOOK_TABS.hunterspells = { + headerLabel = "Hunter Spells", + columnLabels = { "Spell", "Rank", "Level", "Price", "Branch", "Status" }, + columnLayout = trainerColumns, +} + +MTH_HUNTERBOOK_TABS.petspells = { + headerLabel = "Pet Spells", + columnLabels = { "Spell", "Rank", "Level", "Price", "Branch", "Status" }, + columnLayout = trainerColumns, +} + +function MTH_BOOKTAB_IsTrainerSpellMode(mode) + return mode == "hunterspells" or mode == "petspells" +end + +local function MTH_BOOKTAB_GetTrainerKind() + if MTH_BOOK_STATE and MTH_BOOK_STATE.mode == "petspells" then return "pet" end + return "hunter" +end + +function MTH_BOOKTAB_FormatTrainerCost(copper) + local value = tonumber(copper) or 0 + local gold = math.floor(value / 10000) + value = value - gold * 10000 + local silver = math.floor(value / 100) + local bronze = value - silver * 100 + local parts = {} + if gold > 0 then table.insert(parts, "|cffffd100" .. tostring(gold) .. "g|r") end + if silver > 0 or gold > 0 then table.insert(parts, "|cffc7c7cf" .. tostring(silver) .. "s|r") end + table.insert(parts, "|cffeda55f" .. tostring(bronze) .. "c|r") + return table.concat(parts, " ") +end + +function MTH_BOOKTAB_GetTrainerDisplayCost(copper) + if type(MTH_TrainerPlan_GetPrice) == "function" then + return MTH_TrainerPlan_GetPrice({ trainerCost = copper }, MTH_BOOK_STATE and MTH_BOOK_STATE.trainerHonoredDiscount) + end + return tonumber(copper) or 0 +end + +function MTH_BOOKTAB_GetTrainerSpellStatus(row) + local kind = MTH_BOOKTAB_GetTrainerKind() + local playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil + local status = type(MTH_TrainerPlan_GetStatus) == "function" and MTH_TrainerPlan_GetStatus(kind, row, playerLevel) or "notyet" + if status == "known" then return "Known", "|cff40ff40" end + if status == "notlearned" then return "Not learned", "|cffffa500" end + return "Not yet learnable", "|cffff4040" +end + +function MTH_BOOKTAB_BuildTrainerSpellResults() + local results = {} + local kind = MTH_BOOKTAB_GetTrainerKind() + local plan = type(MTH_TrainerPlan_Build) == "function" and MTH_TrainerPlan_Build(kind, { + playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil, + honored = MTH_BOOK_STATE and MTH_BOOK_STATE.trainerHonoredDiscount, + }) or nil + if type(plan) ~= "table" or type(plan.rows) ~= "table" then return results end + local search = MTH_BOOK_SafeLower(MTH_BOOK_STATE.search or "") + for _, planned in ipairs(plan.rows) do + local row = planned.row + local level = tonumber(row.requiredLevel) or 0 + local haystack = tostring(row.name or "") .. " " .. tostring(row.rank or "") .. " " .. tostring(row.section or "") .. " " .. tostring(row.description or "") + if type(row.prerequisites) == "table" then + for _, prerequisite in ipairs(row.prerequisites) do + haystack = haystack .. " " .. tostring(prerequisite) + end + end + haystack = MTH_BOOK_SafeLower(haystack) + if (search == "" or string.find(haystack, search, 1, true) ~= nil) + and (not MTH_BOOK_STATE.minLevel or level >= MTH_BOOK_STATE.minLevel) + and (not MTH_BOOK_STATE.maxLevel or level <= MTH_BOOK_STATE.maxLevel) + and (type(MTH_TrainerPlan_MatchesQuick) ~= "function" or MTH_TrainerPlan_MatchesQuick(planned.status, MTH_BOOK_STATE.quick)) + then + table.insert(results, row) + end + end + return results +end + +function MTH_BOOKTAB_UpdateTrainerPlanPanel(detail) + local parent = detail and detail:GetParent() + if not parent then return end + if not parent.mthTrainerPlan then + local panel = CreateFrame("Frame", nil, parent) + panel:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -238) + panel:SetWidth(136) + panel:SetHeight(128) + panel:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) + panel:SetBackdropColor(0.06, 0.06, 0.06, 1) + panel:SetBackdropBorderColor(0.28, 0.28, 0.28, 1) + panel.text = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + panel.text:SetPoint("TOPLEFT", panel, "TOPLEFT", 7, -7) + panel.text:SetWidth(122) + panel.text:SetJustifyH("LEFT") + panel.text:SetJustifyV("TOP") + parent.mthTrainerPlan = panel + end + local plan = type(MTH_TrainerPlan_Build) == "function" and MTH_TrainerPlan_Build(MTH_BOOKTAB_GetTrainerKind(), { + playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil, + honored = MTH_BOOK_STATE.trainerHonoredDiscount, + }) or nil + if not plan then return end + local nextLevel = plan.nextLevel.level + local nextLabel = "Next" + if nextLevel then nextLabel = "At level " .. tostring(nextLevel) end + local function formatBlock(label, summary) + return "|cffffffff" .. label .. "|r\n" + .. "|cff4da6ff" .. tostring(summary.count) .. "|r |cff9a9a9aspells|r |cffffffff| |r" + .. MTH_BOOKTAB_FormatTrainerCost(summary.cost) + end + parent.mthTrainerPlan.text:SetText("|cffffd100Training Plan|r\n" + .. formatBlock("Now", plan.now) + .. "\n\n" .. formatBlock(nextLabel, plan.nextLevel) + .. "\n\n" .. formatBlock("Remaining", plan.remaining)) + parent.mthTrainerPlan:Show() +end + +function MTH_BOOKTAB_GetTrainerSpellTotal() + local catalog = MTH_DS_TrainerCatalog + local rows = catalog and catalog[MTH_BOOKTAB_GetTrainerKind()] or nil + return type(rows) == "table" and table.getn(rows) or 0 +end + +function MTH_BOOKTAB_TrainerSpellSort(a, b) + local al = tonumber(a and a.requiredLevel) or 0 + local bl = tonumber(b and b.requiredLevel) or 0 + if al ~= bl then return al < bl end + local an = MTH_BOOK_SafeLower(a and a.name) + local bn = MTH_BOOK_SafeLower(b and b.name) + if an ~= bn then return an < bn end + return MTH_BOOK_SafeLower(a and a.rank) < MTH_BOOK_SafeLower(b and b.rank) +end + +function MTH_BOOKTAB_GetTrainerSpellSortKey(row, col) + if col == 1 then return MTH_BOOK_SafeLower(row and row.name) end + if col == 2 then return MTH_BOOK_SafeLower(row and row.rank) end + if col == 3 then return tonumber(row and row.requiredLevel) or 0 end + if col == 4 then return tonumber(row and row.trainerCost) or 0 end + if col == 5 then return MTH_BOOK_SafeLower(row and row.section) end + if col == 6 then + local status = MTH_BOOKTAB_GetTrainerSpellStatus(row) + return MTH_BOOK_SafeLower(status) + end + return nil +end + +function MTH_BOOKTAB_GetTrainerSpellRowValues(row) + local requiredLevel = tonumber(row and row.requiredLevel) or 0 + local playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil + local levelColor = (playerLevel and playerLevel >= requiredLevel) and "|cff40ff40" or "|cffff4040" + local status, statusColor = MTH_BOOKTAB_GetTrainerSpellStatus(row) + return { + tostring(row and row.name or "-"), + tostring(row and row.rank or "-"), + levelColor .. tostring(row and row.requiredLevel or "-") .. "|r", + MTH_BOOKTAB_FormatTrainerCost(MTH_BOOKTAB_GetTrainerDisplayCost(row and row.trainerCost)), + tostring(row and row.section or "-"), + statusColor .. status .. "|r", + } +end + +function MTH_BOOKTAB_UpdateTrainerSpellDetail(detail, selected) + if not MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE and MTH_BOOK_STATE.mode) then return false end + MTH_BOOKTAB_UpdateTrainerPlanPanel(detail) + if not selected then + MTH_BOOK_SetDetailText(detail, "|cFFFFD100Trainer Spells|r\n\nSelect a spell rank to inspect its trainer level, undiscounted cost, and prerequisites.") + return true + end + local lines = {} + table.insert(lines, "|cffffff00" .. tostring(selected.name or "Unknown") .. "|r") + if selected.rank and selected.rank ~= "" then table.insert(lines, "|cffaaaaaa" .. tostring(selected.rank) .. "|r") end + table.insert(lines, "") + table.insert(lines, "|cff9a9a9aBranch:|r |cffffffff" .. tostring(selected.section or "-") .. "|r") + local requiredLevel = tonumber(selected.requiredLevel) or 0 + local playerLevel = MTH_BOOK_GetPlayerLevelValue and MTH_BOOK_GetPlayerLevelValue() or nil + local levelColor = (playerLevel and playerLevel >= requiredLevel) and "|cff40ff40" or "|cffff4040" + table.insert(lines, "|cff9a9a9aRequired Level:|r " .. levelColor .. tostring(selected.requiredLevel or "-") .. "|r") + local status, statusColor = MTH_BOOKTAB_GetTrainerSpellStatus(selected) + table.insert(lines, "|cff9a9a9aStatus:|r " .. statusColor .. status .. "|r") + table.insert(lines, "|cff9a9a9aBase Price:|r " .. MTH_BOOKTAB_FormatTrainerCost(selected.trainerCost)) + table.insert(lines, "|cff9a9a9aHonored Price:|r " .. MTH_BOOKTAB_FormatTrainerCost(math.floor((tonumber(selected.trainerCost) or 0) * 0.90))) + if type(selected.prerequisites) == "table" and table.getn(selected.prerequisites) > 0 then + table.insert(lines, "") + table.insert(lines, "|cffffd100Prerequisites|r") + for _, prerequisite in ipairs(selected.prerequisites) do + table.insert(lines, " " .. tostring(prerequisite)) + end + end + if selected.description and selected.description ~= "" then + table.insert(lines, "") + table.insert(lines, "|cffffd100Description|r") + table.insert(lines, "|cffffffff " .. tostring(selected.description) .. "|r") + end + local detailFrame = detail.mthTrainerDetailParent or detail:GetParent() + if not detail.mthTrainerIcon then + detail.mthTrainerIcon = detail:CreateTexture(nil, "ARTWORK") + detail.mthTrainerIcon:SetWidth(28) + detail.mthTrainerIcon:SetHeight(28) + end + detail.mthTrainerIcon:SetParent(detail) + detail.mthTrainerIcon:ClearAllPoints() + detail.mthTrainerIcon:SetPoint("TOPLEFT", detail, "TOPLEFT", 0, 0) + detail.mthTrainerIcon:SetTexture(MTH_BOOK_ResolveIconPath(selected.icon) or "Interface\\Icons\\INV_Misc_QuestionMark") + detail.mthTrainerIcon:Show() + if detail.SetTextInsets then detail:SetTextInsets(4, 4, 36, 4) end + if not detail.mthTrainerDetailScroll then + detail:ClearAllPoints() + detail:SetPoint("TOPLEFT", detailFrame, "TOPLEFT", 8, -42) + detail:SetWidth(detailFrame:GetWidth() - 16) + end + MTH_BOOK_SetDetailText(detail, table.concat(lines, "\n")) + return true +end \ No newline at end of file diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 0576fff..e4ef175 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -147,6 +147,7 @@ local MTH_BOOK_STATE = { familyStatsPercent = false, petHideNoAbilities = true, petHideUnknown = true, + trainerHonoredDiscount = false, forcedBeastId = nil, page = 1, pageSize = 16, @@ -170,6 +171,8 @@ local MTH_BOOK_STATE = { npcs = { col = nil, asc = true }, stable = { col = nil, asc = true }, pethistory = { col = 5, asc = false }, + hunterspells = { col = 3, asc = true }, + petspells = { col = 3, asc = true }, }, sliderDragging = false, } @@ -308,6 +311,17 @@ local MTH_BOOK_CONTENT_LAYOUT_ITEMS = { detailH = 366, } +local MTH_BOOK_CONTENT_LAYOUT_TRAINERS = { + listX = 16, + listY = -146, + listW = 564, + listH = 374, + detailX = 608, + detailY = -146, + detailW = 136, + detailH = 230, +} + local MTH_BOOK_CONTENT_LAYOUT_PETABILITIES = { listX = 16, listY = -106, @@ -363,7 +377,52 @@ local MTH_BOOK_CONTENT_LAYOUT_HISTORY = { detailH = 446, } -local function MTH_BOOK_ApplyContentLayoutForMode() +function MTH_BOOK_ConfigureTrainerDetailScroll(isTrainerMode, detailParent, detailText) + if not (detailParent and detailText) then return end + if isTrainerMode then + if not detailParent.mthTrainerDetailScroll then + local scroll = CreateFrame("ScrollFrame", "MTH_BOOK_TrainerDetailScroll", detailParent, "UIPanelScrollFrameTemplate") + scroll:SetPoint("TOPLEFT", detailParent, "TOPLEFT", 4, -4) + scroll:SetPoint("BOTTOMRIGHT", detailParent, "BOTTOMRIGHT", -24, 4) + detailParent.mthTrainerDetailScroll = scroll + detailText:EnableMouseWheel(true) + detailText:SetScript("OnMouseWheel", function() + local current = scroll:GetVerticalScroll() + if arg1 and arg1 > 0 then + scroll:SetVerticalScroll(current - 24) + else + scroll:SetVerticalScroll(current + 24) + end + end) + end + local scroll = detailParent.mthTrainerDetailScroll + detailText.mthTrainerDetailParent = detailParent + detailText.mthTrainerDetailScroll = scroll + if detailText:GetParent() ~= scroll then detailText:SetParent(scroll) end + detailText:ClearAllPoints() + detailText:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0) + scroll:SetScrollChild(detailText) + scroll:SetWidth(detailParent:GetWidth() - 28) + scroll:SetHeight(detailParent:GetHeight() - 8) + detailText:SetWidth(detailParent:GetWidth() - 28) + detailText:SetHeight(scroll:GetHeight()) + scroll:Show() + else + local scroll = detailParent.mthTrainerDetailScroll + if scroll then scroll:Hide() end + if detailText.mthTrainerDetailParent then + detailText:SetParent(detailText.mthTrainerDetailParent) + detailText:ClearAllPoints() + detailText:SetPoint("TOPLEFT", detailParent, "TOPLEFT", 6, -6) + detailText:SetWidth(detailParent:GetWidth() - 12) + detailText:SetHeight(detailParent:GetHeight() - 12) + if detailText.SetTextInsets then detailText:SetTextInsets(4, 4, 4, 4) end + detailText.mthTrainerDetailScroll = nil + end + end +end + +function MTH_BOOK_ApplyContentLayoutForMode() local listParent = getglobal("MTH_BOOK_ListBackdrop") local detailParent = getglobal("MTH_BOOK_DetailBackdrop") local detailText = getglobal("MTH_BOOK_DetailBackdropDetailText") @@ -374,6 +433,9 @@ local function MTH_BOOK_ApplyContentLayoutForMode() local layout = MTH_BOOK_CONTENT_LAYOUT_DEFAULT if MTH_BOOK_STATE.mode == "petabilities" then layout = MTH_BOOK_CONTENT_LAYOUT_PETABILITIES + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + layout = MTH_BOOK_CONTENT_LAYOUT_TRAINERS elseif MTH_BOOK_IsItemMode() then layout = MTH_BOOK_CONTENT_LAYOUT_ITEMS elseif MTH_BOOK_STATE.mode == "npcs" then @@ -411,6 +473,9 @@ local function MTH_BOOK_ApplyContentLayoutForMode() if listSlider then listSlider:SetHeight(sliderHeight) end + local isTrainerMode = type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) + MTH_BOOK_ConfigureTrainerDetailScroll(isTrainerMode, detailParent, detailText) if MTH_BOOK_STATE.petUI then local paneHeight = layout.listH - 8 @@ -477,6 +542,45 @@ local function MTH_BOOK_DropdownCreateInfo() return {} end +function MTH_BOOK_EnsureTrainerStatusDropdown() + local dropdown = getglobal("MTH_BOOK_TrainerStatusDropdown") + if not dropdown then + local filter = getglobal("MTH_BOOK_Filter1") + local parent = filter and filter:GetParent() + if not parent then return nil end + dropdown = CreateFrame("Frame", "MTH_BOOK_TrainerStatusDropdown", parent, "UIDropDownMenuTemplate") + dropdown:SetPoint("TOPLEFT", parent, "TOPLEFT", 150, -116) + UIDropDownMenu_Initialize(dropdown, function() + local options = { + { value = "all", label = "All" }, + { value = "known", label = "Known" }, + { value = "notlearned", label = "Not learned" }, + { value = "notyet", label = "Not yet learnable" }, + } + for i = 1, table.getn(options) do + local option = options[i] + local value = option.value + local label = option.label + local info = MTH_BOOK_DropdownCreateInfo() + info.text = label + info.checked = MTH_BOOK_STATE.quick == value + info.func = function() + MTH_BOOK_STATE.quick = value + MTH_BOOK_STATE.page = 1 + MTH_BOOK_STATE.selectedEntry = nil + MTH_BOOK_DropdownSetText(dropdown, label) + MTH_BOOK_RefreshFilter() + end + UIDropDownMenu_AddButton(info) + end + end) + UIDropDownMenu_SetWidth(136, dropdown) + UIDropDownMenu_JustifyText("LEFT", dropdown) + dropdown:Hide() + end + return dropdown +end + local function MTH_BOOK_SetSliderFromCursor(slider) if not slider then return end local minValue, maxValue = slider:GetMinMaxValues() @@ -1459,6 +1563,12 @@ local function MTH_BOOK_GetSortKey(entry, col) if col == 6 then return MTH_BOOK_SafeLower(tostring(row.abandonReason or "")) end end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_GetTrainerSpellSortKey) == "function" + and MTH_BOOKTAB_GetTrainerSpellSortKey(entry, col) or nil + end + local items = MTH_BOOK_GetItemsTable() local item = items and items[entry] if not item then return nil end @@ -1514,6 +1624,10 @@ local function MTH_BOOK_DefaultCompare(a, b) if MTH_BOOK_STATE.mode == "npcs" then return MTH_BOOK_NPCSort(a, b) end if MTH_BOOK_STATE.mode == "stable" then return MTH_BOOK_StableSort(a, b) end if MTH_BOOK_STATE.mode == "pethistory" then return MTH_BOOK_PetHistorySort(a, b) end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_TrainerSpellSort) == "function" and MTH_BOOKTAB_TrainerSpellSort(a, b) or false + end return MTH_BOOK_ItemSort(a, b) end @@ -1782,6 +1896,15 @@ local function MTH_BOOK_BuildResults() return results end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + if type(MTH_BOOKTAB_BuildTrainerSpellResults) == "function" then + results = MTH_BOOKTAB_BuildTrainerSpellResults() + end + MTH_BOOK_ApplyActiveSort(results) + return results + end + local items = MTH_BOOK_GetItemsTable() if not items then return results end for itemId, item in pairs(items) do @@ -1825,6 +1948,10 @@ local function MTH_BOOK_GetTotalCount() local store = MTH_BOOK_GetPetDatastore() return (type(store) == "table" and type(store.historyById) == "table") and MTH_BOOK_CountMap(store.historyById) or 0 end + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_GetTrainerSpellTotal) == "function" and MTH_BOOKTAB_GetTrainerSpellTotal() or 0 + end local items = MTH_BOOK_GetItemsTable() return items and MTH_BOOK_CountMap(items) or 0 end @@ -2328,7 +2455,9 @@ end local function MTH_BOOK_UpdateQuickFilterControls() MTH_BOOK_UpdateCheckboxLayoutByMode() - if MTH_BOOK_IsItemMode() then + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + MTH_BOOK_STATE.pageSize = 17 + elseif MTH_BOOK_IsItemMode() then MTH_BOOK_STATE.pageSize = 18 elseif MTH_BOOK_STATE.mode == "families" then MTH_BOOK_STATE.pageSize = MTH_BOOK_MAX_ROWS @@ -2350,6 +2479,7 @@ local function MTH_BOOK_UpdateQuickFilterControls() local itemSubtypeDropdown = getglobal("MTH_BOOK_ItemSubtypeDropdown") local npcFunctionDropdown = getglobal("MTH_BOOK_NPCFunctionDropdown") local npcZoneDropdown = getglobal("MTH_BOOK_NPCZoneDropdown") + local trainerStatusDropdown = MTH_BOOK_EnsureTrainerStatusDropdown() local searchLabel = getglobal("MTH_BOOK_SearchLabel") local minLabel = getglobal("MTH_BOOK_MinLabel") local maxLabel = getglobal("MTH_BOOK_MaxLabel") @@ -2359,6 +2489,7 @@ local function MTH_BOOK_UpdateQuickFilterControls() local hideNoAbilities = getglobal("MTH_BOOK_HideNoAbilities") local hideUnknown = getglobal("MTH_BOOK_HideUnknown") local petOnlyMyLevel = getglobal("MTH_BOOK_PetOnlyMyLevel") + local trainerHonoredDiscount = getglobal("MTH_BOOK_TrainerHonoredDiscount") local petInZoneOnly = getglobal("MTH_BOOK_PetInZoneOnly") local npcInZoneOnly = getglobal("MTH_BOOK_NPCInZoneOnly") local showAllOnMapButton = getglobal("MTH_BOOK_ShowAllOnMapButton") @@ -2369,6 +2500,7 @@ local function MTH_BOOK_UpdateQuickFilterControls() local nextButton = getglobal("MTH_BOOK_NextButton") local listSlider = getglobal("MTH_BOOK_ListSlider") local sliderBackdrop = getglobal("MTH_BOOK_ListSliderBackdrop") + if trainerStatusDropdown then trainerStatusDropdown:Hide() end if MTH_BOOK_STATE.mode == "petabilities" or MTH_BOOK_STATE.mode == "stable" then if stats then stats:Hide() end @@ -2543,6 +2675,59 @@ local function MTH_BOOK_UpdateQuickFilterControls() resetButton:ClearAllPoints() resetButton:SetPoint("TOPLEFT", resetButton:GetParent(), "TOPLEFT", 546, -80) end + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + if searchLabel then searchLabel:Show() end + if minLabel then minLabel:Show() end + if maxLabel then maxLabel:Show() end + if search then search:Show(); search:ClearAllPoints(); search:SetPoint("TOPLEFT", search:GetParent(), "TOPLEFT", 66, -80); search:SetWidth(160) end + if minLevel then minLevel:Show(); minLevel:ClearAllPoints(); minLevel:SetPoint("TOPLEFT", minLevel:GetParent(), "TOPLEFT", 302, -80) end + if maxLevel then maxLevel:Show(); maxLevel:ClearAllPoints(); maxLevel:SetPoint("TOPLEFT", maxLevel:GetParent(), "TOPLEFT", 414, -80) end + if applyButton then applyButton:Show(); applyButton:ClearAllPoints(); applyButton:SetPoint("TOPLEFT", applyButton:GetParent(), "TOPLEFT", 478, -80) end + local resetButton = getglobal("MTH_BOOK_ResetButton") + if resetButton then resetButton:Show(); resetButton:ClearAllPoints(); resetButton:SetPoint("TOPLEFT", resetButton:GetParent(), "TOPLEFT", 546, -80) end + if scanButton then scanButton:Hide() end + for i = 1, 6 do + local btn = getglobal("MTH_BOOK_Filter" .. i) + if btn then btn:Hide() end + end + if trainerStatusDropdown then + local labels = { all = "All", known = "Known", notlearned = "Not learned", notyet = "Not yet learnable" } + trainerStatusDropdown:ClearAllPoints() + trainerStatusDropdown:SetPoint("TOPLEFT", trainerStatusDropdown:GetParent(), "TOPLEFT", 150, -116) + MTH_BOOK_DropdownSetText(trainerStatusDropdown, labels[MTH_BOOK_STATE.quick] or "All") + trainerStatusDropdown:Show() + end + if familyDropdown then familyDropdown:Hide() end + 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:Hide() end + if npcZoneDropdown then npcZoneDropdown:Hide() end + if hideNoAbilities then hideNoAbilities:Hide() end + if hideUnknown then hideUnknown:Hide() end + if petOnlyMyLevel then petOnlyMyLevel:Hide() end + if trainerHonoredDiscount then + trainerHonoredDiscount:ClearAllPoints() + trainerHonoredDiscount:SetPoint("TOPLEFT", trainerHonoredDiscount:GetParent(), "TOPLEFT", 18, -110) + trainerHonoredDiscount:SetChecked(MTH_BOOK_STATE.trainerHonoredDiscount and 1 or nil) + local trainerHonoredDiscountText = getglobal("MTH_BOOK_TrainerHonoredDiscountText") + if trainerHonoredDiscountText then trainerHonoredDiscountText:SetText("Honored prices") end + trainerHonoredDiscount:Show() + end + if petInZoneOnly then petInZoneOnly:Hide() end + if npcInZoneOnly then npcInZoneOnly:Hide() end + if showAllOnMapButton then showAllOnMapButton:Hide() end + local requireVendor = getglobal("MTH_BOOK_RequireVendor") + local requireDrop = getglobal("MTH_BOOK_RequireDrop") + local requireObject = getglobal("MTH_BOOK_RequireObject") + if requireVendor then requireVendor:Hide() end + if requireDrop then requireDrop:Hide() end + if requireObject then requireObject:Hide() end elseif MTH_BOOK_STATE.mode == "npcs" then if searchLabel then searchLabel:Show() end if minLabel then minLabel:Hide() end @@ -2768,6 +2953,9 @@ local function MTH_BOOK_UpdateQuickFilterControls() if MTH_BOOK_STATE.mode ~= "petabilities" and not MTH_BOOK_IsItemMode() and petOnlyMyLevel then petOnlyMyLevel:Hide() end + if (type(MTH_BOOKTAB_IsTrainerSpellMode) ~= "function" or not MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode)) and trainerHonoredDiscount then + trainerHonoredDiscount:Hide() + end if MTH_BOOK_STATE.mode ~= "pets" and petInZoneOnly then petInZoneOnly:Hide() end @@ -3270,13 +3458,20 @@ end function MTH_BOOK_SetDetailText(detail, text) if not detail then return end local wrapped = tostring(text or "") - if MTH_BOOK_STATE.mode ~= "pethistory" and MTH_BOOK_STATE.mode ~= "pets" and MTH_BOOK_STATE.mode ~= "abilities" then + if MTH_BOOK_STATE.mode ~= "pethistory" and MTH_BOOK_STATE.mode ~= "pets" and MTH_BOOK_STATE.mode ~= "abilities" + and (type(MTH_BOOKTAB_IsTrainerSpellMode) ~= "function" or not MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode)) then wrapped = MTH_BOOK_WrapDetailText(wrapped) end detail._mthLocking = true detail:SetText(wrapped) detail._mthLockedText = wrapped detail._mthLocking = false + local scroll = detail.mthTrainerDetailScroll + if scroll then + local textHeight = detail.GetStringHeight and detail:GetStringHeight() or 0 + detail:SetHeight(math.max(scroll:GetHeight(), textHeight + 12)) + scroll:SetVerticalScroll(0) + end end function MTH_BOOK_ConfigureReadOnlyEditBox(detail) @@ -3323,6 +3518,9 @@ end function MTH_BOOK_UpdateDetail() local detail = getglobal("MTH_BOOK_DetailBackdropDetailText") if not detail then return end + if detail.mthTrainerIcon then detail.mthTrainerIcon:Hide() end + local detailParent = detail:GetParent() + if detailParent and detailParent.mthTrainerPlan then detailParent.mthTrainerPlan:Hide() 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 @@ -3330,6 +3528,11 @@ function MTH_BOOK_UpdateDetail() end local petTop = MTH_BOOK_STATE.petUI and MTH_BOOK_STATE.petUI.detailTop local petBottom = MTH_BOOK_STATE.petUI and MTH_BOOK_STATE.petUI.detailBottom + if type(MTH_BOOKTAB_UpdateTrainerSpellDetail) == "function" + and MTH_BOOKTAB_UpdateTrainerSpellDetail(detail, MTH_BOOK_STATE.selectedEntry) then + MTH_BOOK_UpdateOpenMapButton() + return + end if not MTH_BOOK_STATE.selectedEntry then if MTH_BOOK_ModelViewer_Clear then MTH_BOOK_ModelViewer_Clear() end if MTH_BOOK_STATE.mode == "petabilities" then @@ -4031,6 +4234,12 @@ local function MTH_BOOK_ApplyColumnLayout(parent) end local function MTH_BOOK_GetRowValues(entry) + if type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + return type(MTH_BOOKTAB_GetTrainerSpellRowValues) == "function" + and MTH_BOOKTAB_GetTrainerSpellRowValues(entry) or { "", "", "", "", "" } + end + if MTH_BOOK_STATE.mode == "pets" then -- MetaHunt DB entry local beast = MTH_DS_Beasts and MTH_DS_Beasts[entry] @@ -4317,6 +4526,9 @@ local function MTH_BOOK_AssignEntry(btn, entry) btn.entry = entry elseif MTH_BOOK_STATE.mode == "pethistory" then btn.entry = { petId = entry } + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" + and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + btn.entry = entry else btn.entry = { itemId = entry } end @@ -4482,11 +4694,18 @@ MTH_BOOK_UpdateResults = function() btn.itemIcon:SetTexture(nil) btn.itemIcon:Hide() end + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + local iconPath = MTH_BOOK_ResolveIconPath(entry.icon) + btn.itemIcon:ClearAllPoints() + btn.itemIcon:SetPoint("LEFT", btn, "LEFT", 8, 0) + btn.itemIcon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark") + btn.itemIcon:Show() else btn.itemIcon:SetTexture(nil) btn.itemIcon:Hide() end end + if btn.trainerCoinIcon then btn.trainerCoinIcon:Hide() end if btn.familyAbilityButtons then local abilityEntries = nil local abilityColIndex = nil @@ -4572,6 +4791,7 @@ MTH_BOOK_UpdateResults = function() btn.itemIcon:SetTexture(nil) btn.itemIcon:Hide() end + if btn.trainerCoinIcon then btn.trainerCoinIcon:Hide() end if btn.familyAbilityButtons then for chipIndex = 1, table.getn(btn.familyAbilityButtons) do local chip = btn.familyAbilityButtons[chipIndex] @@ -4628,6 +4848,11 @@ local function MTH_BOOK_UpdateModeLabels() check1 = "" check2 = "" check3 = "" + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + filterNames = { "All", "Known", "Not learned", "Not yet learnable", "", "" } + check1 = "" + check2 = "" + check3 = "" elseif MTH_BOOK_STATE.mode == "npcs" then filterNames = { "", "", "", "", "", "" } check1 = "Alliance" @@ -4769,6 +4994,9 @@ local function MTH_BOOK_SetQuickFilter(index) MTH_BOOK_STATE.quick = "all" elseif MTH_BOOK_STATE.mode == "petabilities" then MTH_BOOK_STATE.quick = "all" + elseif type(MTH_BOOKTAB_IsTrainerSpellMode) == "function" and MTH_BOOKTAB_IsTrainerSpellMode(MTH_BOOK_STATE.mode) then + local map = { [1] = "all", [2] = "known", [3] = "notlearned", [4] = "notyet" } + MTH_BOOK_STATE.quick = map[index] or "all" elseif MTH_BOOK_STATE.mode == "npcs" then MTH_BOOK_STATE.quick = "all" elseif MTH_BOOK_STATE.mode == "abilities" then @@ -4786,10 +5014,94 @@ end local function MTH_BOOK_StyleSectionTab(button) if not button then return end - button:SetHeight(24) + button:SetHeight(26) + button:SetNormalTexture(nil) + button:SetPushedTexture(nil) + button:SetHighlightTexture(nil) + button:SetDisabledTexture(nil) + if not button.mthMenuBackground then + button.mthMenuBackground = button:CreateTexture(nil, "BACKGROUND") + button.mthMenuBackground:SetAllPoints(button) + button.mthMenuBackground:SetTexture("Interface\\Buttons\\WHITE8X8") + end + local regions = { button:GetRegions() } + for _, region in ipairs(regions) do + if region ~= button.mthMenuBackground and region.GetObjectType + and region:GetObjectType() == "Texture" then + region:Hide() + end + end + button:SetScript("OnEnter", function() + if not this.mthSelected then + this.mthMenuBackground:SetVertexColor(0.20, 0.17, 0.06, 1.00) + end + end) + button:SetScript("OnLeave", function() + if not this.mthSelected then + this.mthMenuBackground:SetVertexColor(0.10, 0.10, 0.10, 1.00) + end + end) button:SetScript("OnShow", nil) end +function MTH_BOOK_EnsureVerticalNavigation(frame) + if not frame then return end + local contentPane = getglobal("MTH_BOOK_ContentPane") + if not contentPane then + contentPane = CreateFrame("Frame", "MTH_BOOK_ContentPane", frame) + contentPane:SetPoint("TOPLEFT", frame, "TOPLEFT", 112, 0) + contentPane:SetWidth(760) + contentPane:SetHeight(560) + + local navBackdrop = CreateFrame("Frame", "MTH_BOOK_NavBackdrop", frame) + navBackdrop:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -42) + navBackdrop:SetWidth(100) + navBackdrop:SetHeight(476) + navBackdrop:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + navBackdrop:SetBackdropColor(0.07, 0.07, 0.07, 1.00) + navBackdrop:SetBackdropBorderColor(0.28, 0.28, 0.28, 1.00) + navBackdrop:SetFrameLevel(1) + + local contentChildren = { + "StatsText", "SearchLabel", "MinLabel", "MaxLabel", "BeastZoneLabel", + "ListBackdrop", "DetailBackdrop", "OpenMapButton", "TopAreaBackdrop", + "Search", "MinLevel", "MaxLevel", "ApplyButton", "PetLearnSourceDropdown", + "ItemSubtypeDropdown", "PetOnlyMyLevel", "PetBookScanButton", "ResetButton", "TrainerHonoredDiscount", + "Filter1", "Filter2", "Filter3", "Filter4", "Filter5", "Filter6", + "FamilyDropdown", "AbilityDropdown", "RankDropdown", "PetInZoneOnly", + "ShowAllOnMapButton", "NPCFunctionDropdown", "NPCInZoneOnly", "RequireVendor", + "RequireDrop", "RequireObject", "HideNoAbilities", "HideUnknown", + "BeastContinentDropdown", "BeastZoneDropdown", "ListSliderBackdrop", "ListSlider", + "PrevButton", "NextButton", + } + for _, suffix in ipairs(contentChildren) do + local child = getglobal("MTH_BOOK_" .. suffix) + if child and child:GetParent() == frame then + local point, relativeTo, relativePoint, x, y = child:GetPoint(1) + child:SetParent(contentPane) + if relativeTo == frame then + child:ClearAllPoints() + child:SetPoint(point, contentPane, relativePoint, x, y) + end + end + end + end + + local topAreaBackdrop = getglobal("MTH_BOOK_TopAreaBackdrop") + if topAreaBackdrop then + topAreaBackdrop:ClearAllPoints() + topAreaBackdrop:SetPoint("TOPLEFT", contentPane, "TOPLEFT", 0, -44) + topAreaBackdrop:SetWidth(760) + end +end + local function MTH_BOOK_UpdateSectionTabs() local sectionPets = getglobal("MTH_BOOK_SectionPets") local sectionFamilies = getglobal("MTH_BOOK_SectionFamilies") @@ -4800,13 +5112,18 @@ local function MTH_BOOK_UpdateSectionTabs() local sectionNPCs = getglobal("MTH_BOOK_SectionNPCs") local sectionStable = getglobal("MTH_BOOK_SectionStable") local sectionPetHistory = getglobal("MTH_BOOK_SectionPetHistory") + local sectionHunterSpells = getglobal("MTH_BOOK_SectionHunterSpells") + local sectionPetSpells = getglobal("MTH_BOOK_SectionPetSpells") local function setState(button, selected) if not button then return end + button.mthSelected = selected and true or false if selected then - PanelTemplates_SelectTab(button) + button.mthMenuBackground:SetVertexColor(0.38, 0.28, 0.05, 1.00) + button:SetTextColor(1.00, 0.82, 0.00) else - PanelTemplates_DeselectTab(button) + button.mthMenuBackground:SetVertexColor(0.10, 0.10, 0.10, 1.00) + button:SetTextColor(0.90, 0.90, 0.90) end end @@ -4819,6 +5136,8 @@ local function MTH_BOOK_UpdateSectionTabs() setState(sectionNPCs, MTH_BOOK_STATE.mode == "npcs") setState(sectionStable, MTH_BOOK_STATE.mode == "stable") setState(sectionPetHistory, MTH_BOOK_STATE.mode == "pethistory") + setState(sectionHunterSpells, MTH_BOOK_STATE.mode == "hunterspells") + setState(sectionPetSpells, MTH_BOOK_STATE.mode == "petspells") end local function MTH_BOOK_LayoutSectionTabs() @@ -4832,40 +5151,23 @@ local function MTH_BOOK_LayoutSectionTabs() local sectionNPCs = getglobal("MTH_BOOK_SectionNPCs") local sectionStable = getglobal("MTH_BOOK_SectionStable") local sectionPetHistory = getglobal("MTH_BOOK_SectionPetHistory") + local sectionHunterSpells = getglobal("MTH_BOOK_SectionHunterSpells") + local sectionPetSpells = getglobal("MTH_BOOK_SectionPetSpells") if not (tabBar and sectionPets and sectionFamilies and sectionPetAbilities and sectionItems and sectionNPCs and sectionProjectiles and sectionAmmoBags) then return end - sectionPets:ClearAllPoints() - sectionPets:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0) - - sectionPetAbilities:ClearAllPoints() - sectionPetAbilities:SetPoint("LEFT", sectionPets, "RIGHT", 2, 0) - - sectionFamilies:ClearAllPoints() - sectionFamilies:SetPoint("LEFT", sectionPetAbilities, "RIGHT", 2, 0) - - sectionNPCs:ClearAllPoints() - sectionNPCs:SetPoint("LEFT", sectionFamilies, "RIGHT", 2, 0) - - sectionItems:ClearAllPoints() - sectionItems:SetPoint("LEFT", sectionNPCs, "RIGHT", 2, 0) - - sectionProjectiles:ClearAllPoints() - sectionProjectiles:SetPoint("LEFT", sectionItems, "RIGHT", 2, 0) - - sectionAmmoBags:ClearAllPoints() - sectionAmmoBags:SetPoint("LEFT", sectionProjectiles, "RIGHT", 2, 0) - - if sectionStable then - sectionStable:ClearAllPoints() - sectionStable:SetPoint("LEFT", sectionAmmoBags, "RIGHT", 2, 0) - end - - if sectionPetHistory then - sectionPetHistory:ClearAllPoints() - if sectionStable then - sectionPetHistory:SetPoint("LEFT", sectionStable, "RIGHT", 2, 0) - else - sectionPetHistory:SetPoint("LEFT", sectionAmmoBags, "RIGHT", 2, 0) + local sections = { + sectionPets, sectionPetAbilities, sectionFamilies, sectionNPCs, sectionItems, + sectionProjectiles, sectionAmmoBags, sectionStable, sectionPetHistory, + sectionHunterSpells, sectionPetSpells, + } + local y = 0 + for _, section in ipairs(sections) do + if section then + section:ClearAllPoints() + section:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, y) + section:SetWidth(96) + section:SetHeight(26) + y = y - 28 end end end @@ -5304,6 +5606,7 @@ local function MTH_BOOK_WireUI(frame) frame:SetBackdropColor(0.05, 0.05, 0.05, 1.00) frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 1.00) end + MTH_BOOK_EnsureVerticalNavigation(frame) local closeButton = getglobal("MTH_BOOK_CloseButton") local applyButton = getglobal("MTH_BOOK_ApplyButton") @@ -5317,6 +5620,9 @@ local function MTH_BOOK_WireUI(frame) local sectionNPCs = getglobal("MTH_BOOK_SectionNPCs") local sectionStable = getglobal("MTH_BOOK_SectionStable") local sectionPetHistory = getglobal("MTH_BOOK_SectionPetHistory") + local sectionHunterSpells = getglobal("MTH_BOOK_SectionHunterSpells") + local sectionPetSpells = getglobal("MTH_BOOK_SectionPetSpells") + local trainerHonoredDiscount = getglobal("MTH_BOOK_TrainerHonoredDiscount") local requireVendor = getglobal("MTH_BOOK_RequireVendor") local requireDrop = getglobal("MTH_BOOK_RequireDrop") local requireObject = getglobal("MTH_BOOK_RequireObject") @@ -5358,6 +5664,14 @@ local function MTH_BOOK_WireUI(frame) if sectionNPCs then sectionNPCs:SetScript("OnClick", function() MTH_BOOK_SetMode("npcs") end) end if sectionStable then sectionStable:SetScript("OnClick", function() MTH_BOOK_SetMode("stable") end) end if sectionPetHistory then sectionPetHistory:SetScript("OnClick", function() MTH_BOOK_SetMode("pethistory") end) end + if sectionHunterSpells then sectionHunterSpells:SetScript("OnClick", function() MTH_BOOK_SetMode("hunterspells") end) end + if sectionPetSpells then sectionPetSpells:SetScript("OnClick", function() MTH_BOOK_SetMode("petspells") end) end + if trainerHonoredDiscount then + trainerHonoredDiscount:SetScript("OnClick", function() + MTH_BOOK_STATE.trainerHonoredDiscount = this:GetChecked() == 1 + MTH_BOOK_UpdateResults() + end) + end MTH_BOOK_StyleSectionTab(sectionPets) MTH_BOOK_StyleSectionTab(sectionFamilies) @@ -5368,7 +5682,10 @@ local function MTH_BOOK_WireUI(frame) MTH_BOOK_StyleSectionTab(sectionNPCs) MTH_BOOK_StyleSectionTab(sectionStable) MTH_BOOK_StyleSectionTab(sectionPetHistory) + MTH_BOOK_StyleSectionTab(sectionHunterSpells) + MTH_BOOK_StyleSectionTab(sectionPetSpells) MTH_BOOK_LayoutSectionTabs() + MTH_BOOK_ApplyContentLayoutForMode() for i = 1, 6 do local filterBtn = getglobal("MTH_BOOK_Filter" .. i) @@ -5384,6 +5701,7 @@ local function MTH_BOOK_WireUI(frame) MTH_BOOK_InitPetLearnSourceDropdown() MTH_BOOK_InitItemSubtypeDropdown() MTH_BOOK_InitNPCFunctionDropdown() + MTH_BOOK_EnsureTrainerStatusDropdown() if petBookScanButton then petBookScanButton:SetScript("OnClick", function() diff --git a/api/hunterbook.xml b/api/hunterbook.xml index 1126347..7da9855 100644 --- a/api/hunterbook.xml +++ b/api/hunterbook.xml @@ -2,7 +2,7 @@ ..\FrameXML\UI.xsd"> - + @@ -128,7 +128,7 @@ - + @@ -188,6 +188,18 @@ + + @@ -235,6 +247,12 @@ + +