diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c2cce7..b94b21a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Resurrecting on Octowow. ### Added +- **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 for you — no manual trip to the pet trainer needed. Toggle it in **Options → General → Pet Auto-Teach** ("Activate Auto-Teach of Growl (Max rank)", on by default) or with `/mth growl on|off`. + - **Animations**: MetaHunt now plays optional animations that make the interface feel alive. A firework-and-banner celebration bursts on screen when your pet learns a new ability or a new rank, and a red shaking banner warns you when a pet runs away for good. Your pet button flashes when its happiness changes, it gains a loyalty level, or you feed it; the ammo button flashes when Smart Ammo swaps your bullets or auto-equips after a weapon swap; and the MM Widget pulses on a Lock & Load proc. The Great Book slides up and fades in when you open it, gently fades its list when you switch tabs or run a search, fades beast models into view, and fades out when closed; stable slots flash when a pet is deposited, withdrawn or swapped. The options window slides in when opened, fades its tabs as you switch them, and fades out when closed. The minimap button pulses when clicked, minimap markers ping as beasts come into range, collapsible zButton bars fade their buttons in as they expand, the zBar fades in when enabled, and world-map markers fade in when you locate a beast, vendor or master. Everything is optional and off-switchable: a new **Animations** tab (Options → Animations) lets you toggle each effect on its own — or disable them all at once. Powered by the PizzaSauce animation library. - **zTrack — Find Fish**: Added new spell Find Fish to the zTrack list of trackings. @@ -17,6 +19,8 @@ Resurrecting on Octowow. ### Changed +- **zButton bars — expand animation**: Collapsible zButton flyout bars and the zBar now fade in together when they open, instead of stacking their buttons one after another, so expanding them feels smoother. + - **Complete revamp of SavedVariables**: All your settings now live in one tidy place instead of being scattered around. A migration happens the first time you log in, and you shouldn't lose any settings. diff --git a/api/core-scan-stablemaster.lua b/api/core-scan-stablemaster.lua index efd50aa..766710e 100644 --- a/api/core-scan-stablemaster.lua +++ b/api/core-scan-stablemaster.lua @@ -3104,6 +3104,9 @@ function MTH_PETS_RefreshCurrentPet() at = now, context = eventContext, }) + if eventType == "pet-tamed" and type(MTH_Growl_OfferForTamedPet) == "function" then + MTH_Growl_OfferForTamedPet(snapshot.name, snapshot.level) + end end end @@ -3733,6 +3736,9 @@ local function MTH_PETS_HandleUnitPetTransition(source) context = MTH_PETS_CaptureContext(), }) end + if eventType == "pet-tamed" and type(MTH_Growl_OfferForTamedPet) == "function" then + MTH_Growl_OfferForTamedPet(snapshot.name, snapshot.level) + end end if not hadPet and hasPendingTame then MTH_PETS_LogTame("Acquire completed; clearing pending tame attempt") diff --git a/api/core.lua b/api/core.lua index 672b481..f1f06b5 100644 --- a/api/core.lua +++ b/api/core.lua @@ -495,6 +495,19 @@ function SlashCmdList.MTH(msg, editbox) else MTH:Print("Dev commands: /mth dev petmigrate") end + elseif lowerCmd == "growl" then + if type(MTH_Growl_SetEnabled) ~= "function" or type(MTH_Growl_IsEnabled) ~= "function" then + MTH:Print("Growl auto-teach is not available.") + elseif lowerArg == "on" or lowerArg == "enable" then + MTH_Growl_SetEnabled(true) + MTH:Print("Growl auto-teach: |cff00ff00ON|r (offers Growl to newly tamed pets).") + elseif lowerArg == "off" or lowerArg == "disable" then + MTH_Growl_SetEnabled(false) + MTH:Print("Growl auto-teach: |cffff0000OFF|r.") + else + local state = MTH_Growl_IsEnabled() and "|cff00ff00ON|r" or "|cffff0000OFF|r" + MTH:Print("Growl auto-teach is " .. state .. ". Usage: /mth growl on | off") + end elseif lowerCmd == "food" then local sub = lowerArg local function MTH_FoodItemLabel(itemId) diff --git a/api/options-zbuttons.lua b/api/options-zbuttons.lua index 60930ec..f5e02d0 100644 --- a/api/options-zbuttons.lua +++ b/api/options-zbuttons.lua @@ -1540,7 +1540,7 @@ function MTH_SetupGeneralOptions() ensureHelpText(smartAmmoSection, "MetaHuntGeneralSmartAmmoWeaponSwapHelp", "When you swap between gun and bow/crossbow, instantly equips the best matching ammo from your bags.", -208) end - local stripSection = ensureSection("MetaHuntGeneralAutoStripBox", "Auto-Strip", topY - 270, 122, "left") + local stripSection = ensureSection("MetaHuntGeneralAutoStripBox", "Auto-Strip", topY - 270, 100, "left") if stripSection then local autoStripToggle = ensureCheckbox(stripSection, "MetaHuntGeneralAutoStripToggle", "Enable Auto-Strip on Combat Exit", -10, stripSaved["autostrip"] and true or false) if autoStripToggle then @@ -1563,7 +1563,7 @@ function MTH_SetupGeneralOptions() ensureHelpText(stripSection, "MetaHuntGeneralAutoStripHelp", "Auto-strip unequips items when combat ends. Requires at least one empty bag slot.", -64) end - local antiSection = ensureSection("MetaHuntGeneralAntiDazeBox", "Anti-Daze", topY - 406, 96, "left") + local antiSection = ensureSection("MetaHuntGeneralAntiDazeBox", "Anti-Daze", topY - 384, 76, "left") if antiSection then local antiDazeEnabled = (AntiDaze_GetEnabled and AntiDaze_GetEnabled()) or false local antiToggle = ensureCheckbox(antiSection, "MetaHuntGeneralAntiDazeToggle", "Enable Anti-Daze", -10, antiDazeEnabled) @@ -1586,6 +1586,21 @@ function MTH_SetupGeneralOptions() ensureHelpText(antiSection, "MetaHuntGeneralAntiDazeHelp", "Cancels Cheetah/Pack when dazed.", -40) end + local petTeachSection = ensureSection("MetaHuntGeneralPetTeachBox", "Pet Auto-Teach", topY - 474, 76, "left") + if petTeachSection then + local growlEnabled = (type(MTH_Growl_IsEnabled) == "function" and MTH_Growl_IsEnabled()) or false + local growlToggle = ensureCheckbox(petTeachSection, "MetaHuntGeneralPetTeachGrowlToggle", "Activate Auto-Teach of Growl (Max rank)", -10, growlEnabled) + if growlToggle then + growlToggle:SetScript("OnClick", function() + if type(MTH_Growl_SetEnabled) == "function" then + MTH_Growl_SetEnabled(MTH_ZB_IsChecked(this)) + end + end) + end + + ensureHelpText(petTeachSection, "MetaHuntGeneralPetTeachGrowlHelp", "When you tame a beast, offers to teach it the best Growl rank for its level.", -40) + end + local tooltipsSection = ensureSection("MetaHuntGeneralTooltipsBox", "Tooltips", topY, 356, "right") if tooltipsSection then local tooltipsStore = MTH and MTH.GetModuleCharSavedVariables and MTH:GetModuleCharSavedVariables("tooltips") diff --git a/api/pet-growl-teach.lua b/api/pet-growl-teach.lua new file mode 100644 index 0000000..bb271a7 --- /dev/null +++ b/api/pet-growl-teach.lua @@ -0,0 +1,333 @@ +-- MetaHunt: auto-train Growl to a freshly tamed pet. +-- +-- Growl is a "trainer" pet ability (Growl / Great Stamina / Natural Armor / +-- Great Resistance) that the hunter learns from a Pet Trainer NPC and then +-- teaches to the active pet through the always-available BEAST TRAINING window +-- (a spellbook-like hunter spell, no NPC required). Growl specifically costs +-- 0 training points, so it can always be taught to a brand-new pet. +-- +-- The Beast Training window is driven by the vanilla Craft API: +-- GetNumCrafts() -> number of rows +-- GetCraftInfo(i) -> name, rankSubText, type, ... +-- DoCraft(i) -> trains that row (the "Select + Train" action) +-- CloseCraft() -> closes the window +-- +-- The correct Growl rank is a function of the pet's level: +-- rank = floor(level / 10) + 1 (ranks 1..7, at pet levels 1/10/.../60) +-- +-- Flow on a confirmed fresh tame (when enabled): compute the target rank, open +-- the Beast Training window, find the Growl row at that rank once the craft list +-- populates, DoCraft() it silently, then close the window if we opened it. + +local MTH_GROWL_MAX_RANK = 7 + +-- --------------------------------------------------------------------------- +-- Saved-variable backed toggle (per character), default ON. +-- --------------------------------------------------------------------------- +local function MTH_Growl_GetStore() + if type(MTH_CharSavedVariables) ~= "table" then + MTH_CharSavedVariables = {} + end + if type(MTH_CharSavedVariables.growlTeach) ~= "table" then + MTH_CharSavedVariables.growlTeach = {} + end + local store = MTH_CharSavedVariables.growlTeach + if store.enabled == nil then + store.enabled = true + end + return store +end + +function MTH_Growl_IsEnabled() + return MTH_Growl_GetStore().enabled and true or false +end + +function MTH_Growl_SetEnabled(enabled) + MTH_Growl_GetStore().enabled = enabled and true or false +end + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- +local function MTH_Growl_DebugLog(line) + if type(MTH_DebugFrame) == "table" and type(MTH_DebugFrame.AddInfo) == "function" then + MTH_DebugFrame:AddInfo("[Growl] " .. tostring(line)) + end +end + +local function MTH_Growl_GetGlobal(name) + if type(getglobal) == "function" then + return getglobal(name) + end + if _G then + return _G[name] + end + return nil +end + +-- Rank appropriate for a pet of the given level. +local function MTH_Growl_RankForLevel(level) + local lvl = tonumber(level) or 0 + if lvl < 1 then lvl = 1 end + local rank = math.floor(lvl / 10) + 1 + if rank < 1 then rank = 1 end + if rank > MTH_GROWL_MAX_RANK then rank = MTH_GROWL_MAX_RANK end + return rank +end + +-- Parse a rank number out of a craft row's name/subtext (e.g. "Growl", "Rank 5"). +local function MTH_Growl_ParseRank(name, subText) + local sub = tostring(subText or "") + local _, _, subRank = string.find(sub, "(%d+)") + if subRank then return tonumber(subRank) end + local nm = tostring(name or "") + local _, _, nameRank = string.find(nm, "(%d+)") + if nameRank then return tonumber(nameRank) end + return nil +end + +-- Find the hunter spellbook slot for a spell by name (e.g. "Beast Training"). +local function MTH_Growl_FindSpellSlot(spellName) + if not spellName or spellName == "" then return nil end + local getSpellName = MTH_Growl_GetGlobal("GetSpellName") + if type(getSpellName) ~= "function" then return nil end + + local getNumSpellTabs = MTH_Growl_GetGlobal("GetNumSpellTabs") + local getSpellTabInfo = MTH_Growl_GetGlobal("GetSpellTabInfo") + if type(getNumSpellTabs) == "function" and type(getSpellTabInfo) == "function" then + local numTabs = getNumSpellTabs() or 0 + for tabIndex = 1, numTabs do + local _, _, offset, numSpells = getSpellTabInfo(tabIndex) + offset = tonumber(offset) or 0 + numSpells = tonumber(numSpells) or 0 + for spellIndex = (offset + 1), (offset + numSpells) do + if getSpellName(spellIndex, "spell") == spellName then + return spellIndex + end + end + end + end + + -- Fallback: linear scan of the spellbook. + local nilStreak = 0 + for spellIndex = 1, 1024 do + local nameAtIndex = getSpellName(spellIndex, "spell") + if nameAtIndex then + nilStreak = 0 + if nameAtIndex == spellName then + return spellIndex + end + else + nilStreak = nilStreak + 1 + if nilStreak >= 30 then break end + end + end + return nil +end + +-- Open the Beast Training window by casting the hunter "Beast Training" spell. +local function MTH_Growl_OpenTrainingWindow() + local spellName = MTH_Growl_GetGlobal("ZHUNTER_PET_TRAINING") or "Beast Training" + local slot = MTH_Growl_FindSpellSlot(spellName) + local castSpell = MTH_Growl_GetGlobal("CastSpell") + if slot and type(castSpell) == "function" then + local getSpellName = MTH_Growl_GetGlobal("GetSpellName") + local atSlot = (type(getSpellName) == "function") and getSpellName(slot, "spell") or "?" + castSpell(slot, "spell") + MTH_Growl_DebugLog("Opened Beast Training via CastSpell(slot=" .. tostring(slot) + .. " name='" .. tostring(atSlot) .. "')") + return true + end + local castSpellByName = MTH_Growl_GetGlobal("CastSpellByName") + if type(castSpellByName) == "function" then + castSpellByName(spellName) + MTH_Growl_DebugLog("Opened Beast Training via CastSpellByName('" .. tostring(spellName) .. "') (slot not found)") + return true + end + MTH_Growl_DebugLog("Could not open Beast Training (no spell slot, no CastSpellByName)") + return false +end + +-- Is the Beast Training / craft window currently visible? +local function MTH_Growl_IsTrainingWindowVisible() + local frameNames = { "BeastTrainingFrame", "PetTrainingFrame", "CraftFrame" } + for i = 1, table.getn(frameNames) do + local frame = MTH_Growl_GetGlobal(frameNames[i]) + if frame then + if frame.IsVisible and frame:IsVisible() then return true end + if frame.IsShown and frame:IsShown() then return true end + end + end + return false +end + +-- --------------------------------------------------------------------------- +-- Pending auto-train request. Set when a fresh tame is confirmed; cleared once +-- we train (or time out). { rank, name, elapsed, closeAfter, done } +-- --------------------------------------------------------------------------- +local MTH_Growl_Pending = nil + +-- The offer currently awaiting the player's Yes/No answer. { rank, name, level } +local MTH_Growl_Offer = nil +local function MTH_Growl_TryTrainNow() + if not MTH_Growl_Pending or MTH_Growl_Pending.done then + return false + end + + local getNumCrafts = MTH_Growl_GetGlobal("GetNumCrafts") + local getCraftInfo = MTH_Growl_GetGlobal("GetCraftInfo") + local doCraft = MTH_Growl_GetGlobal("DoCraft") + if type(getNumCrafts) ~= "function" or type(getCraftInfo) ~= "function" or type(doCraft) ~= "function" then + return false + end + + local total = tonumber(getNumCrafts()) or 0 + if total <= 0 then + return false -- window not populated yet + end + + local target = MTH_Growl_Pending.rank + local bestIndex, bestRank = nil, 0 + for i = 1, total do + local craftName, craftSub, craftType = getCraftInfo(i) + local lname = string.lower(tostring(craftName or "")) + if string.find(lname, "growl", 1, true) and tostring(craftType) ~= "header" then + local rank = MTH_Growl_ParseRank(craftName, craftSub) + if rank then + if rank == target then + bestIndex, bestRank = i, rank + break + elseif rank < target and rank > bestRank then + -- Highest rank we could give if the exact target isn't offered. + bestIndex, bestRank = i, rank + end + end + end + end + + if not bestIndex then + return false -- no Growl row found yet + end + + doCraft(bestIndex) + MTH_Growl_DebugLog("Trained Growl Rank " .. tostring(bestRank) + .. " (target " .. tostring(target) .. ") for '" .. tostring(MTH_Growl_Pending.name) + .. "' via DoCraft(" .. tostring(bestIndex) .. ")") + + MTH_Growl_Pending.done = true + if MTH_Growl_Pending.closeAfter then + local closeCraft = MTH_Growl_GetGlobal("CloseCraft") + if type(closeCraft) == "function" then + closeCraft() + end + end + MTH_Growl_Pending = nil + return true +end + +-- Driver frame: react to the craft window opening/updating, with an OnUpdate +-- poll as backup and a timeout so we never leave a stale pending request. +local MTH_Growl_Frame = CreateFrame("Frame", "MTH_GrowlTeachFrame") +MTH_Growl_Frame:RegisterEvent("CRAFT_SHOW") +MTH_Growl_Frame:RegisterEvent("CRAFT_UPDATE") +MTH_Growl_Frame:RegisterEvent("PET_TRAINING_SHOW") +MTH_Growl_Frame:SetScript("OnEvent", function() + MTH_Growl_TryTrainNow() +end) +MTH_Growl_Frame:SetScript("OnUpdate", function() + if not MTH_Growl_Pending then return end + local p = MTH_Growl_Pending + p.elapsed = (p.elapsed or 0) + (arg1 or 0) + + -- Train as soon as the window is populated. + if MTH_Growl_TryTrainNow() then return end + + if p.elapsed > 8 then + local getNumCrafts = MTH_Growl_GetGlobal("GetNumCrafts") + local crafts = (type(getNumCrafts) == "function" and tonumber(getNumCrafts())) or -1 + MTH_Growl_DebugLog("Gave up training Growl Rank " .. tostring(p.rank) + .. " for '" .. tostring(p.name) .. "' (timeout: windowVisible=" + .. tostring(MTH_Growl_IsTrainingWindowVisible()) .. " numCrafts=" .. tostring(crafts) + .. " openAttempts=" .. tostring(p.openAttempts) .. ")") + MTH_Growl_Pending = nil + return + end + + -- Window not up yet: re-attempt opening every second (the pet may still be + -- settling right after the tame, so the first cast can be ignored). + if not MTH_Growl_IsTrainingWindowVisible() then + p.sinceOpen = (p.sinceOpen or 0) + (arg1 or 0) + if p.sinceOpen >= 1.0 then + p.sinceOpen = 0 + p.openAttempts = (p.openAttempts or 0) + 1 + if p.openAttempts <= 6 then + MTH_Growl_OpenTrainingWindow() + end + end + end +end) + +-- --------------------------------------------------------------------------- +-- Start training the confirmed offer. Called from the popup's Yes button (a +-- hardware event), which makes opening the Beast Training window reliable. +-- --------------------------------------------------------------------------- +local function MTH_Growl_StartTraining(rank, petName) + local alreadyOpen = MTH_Growl_IsTrainingWindowVisible() + MTH_Growl_Pending = { + rank = rank, + name = petName, + elapsed = 0, + sinceOpen = 0, + openAttempts = 0, + closeAfter = not alreadyOpen, + done = false, + } + MTH_Growl_DebugLog("Confirmed: train Growl Rank " .. tostring(rank) .. " for '" + .. tostring(petName) .. "' (windowOpen=" .. tostring(alreadyOpen) .. ")") + if alreadyOpen then + MTH_Growl_TryTrainNow() + else + MTH_Growl_Pending.openAttempts = 1 + MTH_Growl_OpenTrainingWindow() + end +end + +-- --------------------------------------------------------------------------- +-- Confirmation popup. Yes = hardware event -> open Beast Training + DoCraft. +-- --------------------------------------------------------------------------- +StaticPopupDialogs["MTH_GROWL_TEACH"] = { + text = "Teach %s to your newly tamed %s?", + button1 = YES or "Yes", + button2 = NO or "No", + OnAccept = function() + local offer = MTH_Growl_Offer + MTH_Growl_Offer = nil + if not offer then return end + MTH_Growl_StartTraining(offer.rank, offer.name) + end, + OnCancel = function() + MTH_Growl_Offer = nil + end, + timeout = 0, + whileDead = 1, + hideOnEscape = 1, +} + +-- --------------------------------------------------------------------------- +-- Public entry point: called when a fresh tame is confirmed. +-- --------------------------------------------------------------------------- +function MTH_Growl_OfferForTamedPet(petName, petLevel) + if not MTH_Growl_IsEnabled() then + return + end + if type(StaticPopup_Show) ~= "function" then + return + end + + local rank = MTH_Growl_RankForLevel(petLevel) + MTH_Growl_Offer = { rank = rank, name = petName, level = petLevel } + local spellLabel = "Growl (Rank " .. tostring(rank) .. ")" + local name = tostring(petName or "your pet") + MTH_Growl_DebugLog("Offer pet='" .. name .. "' level=" .. tostring(petLevel) .. " -> " .. spellLabel) + StaticPopup_Show("MTH_GROWL_TEACH", spellLabel, name) +end diff --git a/init/api.xml b/init/api.xml index 9387315..e04672e 100644 --- a/init/api.xml +++ b/init/api.xml @@ -45,6 +45,7 @@ + diff --git a/modules/zhunter/ZSpellButtonTemplate.lua b/modules/zhunter/ZSpellButtonTemplate.lua index 1b682b0..6197c96 100644 --- a/modules/zhunter/ZSpellButtonTemplate.lua +++ b/modules/zhunter/ZSpellButtonTemplate.lua @@ -418,11 +418,9 @@ function ZSpellButton_SetChildrenExpanded(parent, expanded) end end ZSpellButton_StartFadeTimer(parent) - -- Animate only on a genuine collapsed->expanded toggle, and never on the - -- very first apply at login (guarded by _zbAnimReady, set in ApplyChildrenExpanded). - if wasCollapsed and parent._zbAnimReady then - ZSpellButton_AnimateExpand(parent) - end + -- Flyout reveal is instant: no stagger/cascade animation. The staggered + -- "stacking one after the other" fly-out was slow and unwanted, so the + -- children just appear immediately when expanded. else saved["children"]["expanded"] = 0 if parent.children then diff --git a/modules/zhunter/zBar.lua b/modules/zhunter/zBar.lua index 0477972..b9d7bdc 100644 --- a/modules/zhunter/zBar.lua +++ b/modules/zhunter/zBar.lua @@ -284,11 +284,9 @@ function MTH_ZBar_Release() end end --- Animations: gentle staggered fade-in of the bar's buttons when it activates. --- NOTE: keep this a plain alpha fade only. A SlideIn (position tween) here caused --- client micro-freezes on reload because it re-anchors every chained button each --- frame, forcing repeated layout recalcs. Use position/scale tweens on the zBar --- with extreme caution. +-- Animations: a single, quick, simultaneous fade-in of the bar's buttons when +-- it activates. NO stagger (no per-button delay) — every button fades in at the +-- same time so it reads as one clean fade instead of a slow "stacking" cascade. function MTH_ZBar_AnimateActivate() if not ZBar_Sauce then return end if type(MTH_FX_IsEnabled) ~= "function" or not MTH_FX_IsEnabled("ui.zbar") then @@ -302,7 +300,7 @@ function MTH_ZBar_AnimateActivate() local btn = getglobal(buttonName) if btn and btn:IsShown() then btn:SetAlpha(0) - ZBar_Sauce:FadeTo(btn, 1, 0.30, "outQuad", { delay = (i - 1) * 0.05 }) + ZBar_Sauce:FadeTo(btn, 1, 0.20, "outQuad") end end end @@ -356,5 +354,8 @@ function MTH_ZBar_Init() if s.enabled then MTH_ZBar_EnsureAnchor() MTH_ZBar_ApplyLayout() + -- Play the gentle fade on login/reload too, not just on checkbox toggle. + -- Safe here because AnimateActivate is a plain alpha fade (no re-anchoring). + MTH_ZBar_AnimateActivate() end end