From 566f3f4925775ca8dbfeb955d68ba06893e9ad36 Mon Sep 17 00:00:00 2001 From: DuvelCorp Date: Thu, 10 Sep 2026 14:41:26 +0200 Subject: [PATCH] Add Animations feature + fix own-pet tooltip false positive Animations (all optional, default on, per-effect toggles): - New Options -> Animations tab with master switch + per-effect checkboxes - Embed PizzaSauce animation library (api/PizzaSauce.lua) - fx-celebrate: firework + banner when a pet learns a new ability/rank, triggered from core-scan-beasttraining pet-learn hook - Hunter's Book: slide + fade on open, results list fade on tab switch - Options window: slide + fade on open - All new user-facing strings keyed (ANIM_* namespace) Fixes: - Tooltip #19: own-pet lines (mood/loyalty/happiness/XP) no longer misfire on party members / targets. Removed the fragile title-name fallback in MTH_TT_IsTooltipOnOwnPet; ownership now requires positive unit identity. Temp (remove before release): /mth fx and /mth fxdebug diagnostics. --- CHANGELOG.md | 37 +- README.MD | 59 +- api/PizzaSauce.lua | 1347 +++++++++++++++++++++++++++++++ api/core-scan-beasttraining.lua | 3 + api/core.lua | 63 ++ api/fx-celebrate.lua | 297 +++++++ api/hunterbook.lua | 32 + api/options-animations.lua | 173 ++++ api/options-shell.lua | 18 + api/options-tree.lua | 2 + api/options.xml | 1 + init/api.xml | 3 + 12 files changed, 1992 insertions(+), 43 deletions(-) create mode 100644 api/PizzaSauce.lua create mode 100644 api/fx-celebrate.lua create mode 100644 api/options-animations.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 8481e41..03852b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,25 +5,31 @@ All notable changes to MetaHunt will be documented in this file. ## [2.0.0] - Unreleased -### Changed - -- **Settings storage tidied into one clean layout**: The settings MetaHunt inherited from the older stand-alone addons it now replaces (Feed-O-Matic, ZHunter/zButtons, SmartAmmo, and the quest/anti-daze/auto-strip/minimap helpers) used to be scattered across many separate saved entries, several of them duplicated on disk. MetaHunt now keeps everything in one organised place, grouped per module. The clean-up runs automatically and once, the first time you log in after updating: all of your existing settings and layouts are carried over unchanged — nothing is reset — and the old duplicate copies are removed so your saved-variables file stays small and consistent. - -### Fixed - -- **Pets — duplicate and phantom entries in your pet collection**: MetaHunt could record the same tamed pet more than once — a fresh copy appeared after you logged out and back in, or after your pet leveled up — and a few very early pets you had long since dismissed lingered as though they were still with you. MetaHunt now recognises each pet reliably from one session to the next (and as it levels), automatically merges the duplicate entries it had already created while keeping the correct taming details (where, and at what level, you tamed each one), and tidies the leftover phantom pets away into your pet history. Your full taming history is preserved — nothing is deleted. - - -## [1.5.2] - 2026-09-08 - -Res on Octowow server. +Resurrecting on Octowow. ### Added +- **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, the Great Book slides up and fades in when you open it and gently fades its list when you switch tabs, and the options window slides in when opened. 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. +- **DDOSers Realm Blocking**: The Addon will auto-disable itself on all Mafia's realms, we know who they are. + +### Changed + +- **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. + + ### Fixed +- **Tooltips — your pet's info showing on other players**: In a party, mousing over or targeting another player could wrongly show YOUR pet's details (mood, loyalty, happiness, XP) on their tooltip. The own-pet detection no longer misfires on other units. + +- **zCraft — Smelting**: Smelting was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. + +- **Wrong icons on pet spells**: Fixed the Rank 0 of Bubble Barrier and Charge that had incorrect icons. + +- **Fixed tracking data in Book's Stables**: Fixed an issue with pets' GUID returned by the server, that was not really unique and caused issues with pets' tracking, notably when you had several pets with the same name. Taming info was disapearing, amongst other weirdness. A migration is done on first load to fix your existing data. + - **FeedOMatic — "Set Key" button error**: Clicking **Set Key** in Options → Feed-o-Matic threw an error and wouldn't let you bind a feed key. It now works again. - **FeedOMatic — pet left hungry when bags were full**: When your bags were full, FeedOMatic could grab a random tiny stack (like a single meat your pet won't eat) to free a bag slot, so your pet stayed hungry. It now always picks food your pet can actually eat. @@ -32,13 +38,8 @@ Res on Octowow server. - **Pet training — new rank of a known ability not registered**: When you trained a higher rank of an ability you already had (e.g. learning Charge Rank 2 after Rank 1), MetaHunt treated it as already known — so the "Pet ability learned and recorded" message never appeared and the pet-bar tooltip kept saying "Not learned yet" for the new rank. Learning a new rank is now recognised on its own, announces correctly, and the tooltip immediately reflects the rank you just learned. -- **Hunter's Book — missing "Unique Appearance" beasts**: The unique-appearance flag had gone missing from the beast database, so the "Unique" badge and filter in the Hunter's Book came up empty. Restored a curated list of the genuinely one-of-a-kind beasts a collector hunts for (Humar the Pridelord, Darksaber, Shar'lan, Frostsaber Pride Watcher, Mazzranache, Moonfeather, Azurebeak, Old Cliff Jumper, Rotting Agam'ar, Razzashi Serpent, Magmalash Scorpid, Spirit Fox and more), so the Unique badge and filter work again without wrongly flagging common colour-variant mobs. +- **Hunter's Book — fixed "Unique" beasts**: Restored a curated list of the one-of-a-kind beasts (Humar the Pridelord, Darksaber, Shar'lan, Frostsaber Pride Watcher, Mazzranache, Moonfeather, Azurebeak, Old Cliff Jumper, Rotting Agam'ar, Razzashi Serpent, Magmalash Scorpid, Spirit Fox and more). -- **ds-pet-spells — wrong rank 0 icons**: Rank 0 (triggered/sub-spell) entries for Bubble Barrier and Charge had incorrect icons (`Spell_Frost_FrostNova` and `Ability_Warrior_Charge` respectively) instead of matching their ranked ability icons (`spell_bubl1` and `Ability_Hunter_Pet_Boar`). Fixed in both `allSpells` and `byAbility` sections. - -- **zCraft — Smelting**: Smelting was previously not appearing in zCraft because incorrectly hardcoded as "Mining", this is fixed. - -- **Realm Blocking**: The Addon will auto-disable itself on all Mafia's realms. ## [1.5.1] - 2026-04-01 diff --git a/README.MD b/README.MD index e916721..8f68b75 100644 --- a/README.MD +++ b/README.MD @@ -1,37 +1,32 @@ # MetaHunt MetaHunt is a Twow Hunter toolkit. -It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken on twow. -It provides a bunch of useful tools for all huntards, wheter they are still levelling, lone-wolf, or HL raider. + +It is a modern modular addon compiling brand new features, and improving a few old hunter add-ons that were broken or disfunctional with twow. + +It provides a bunch of useful functionalities for all huntards, wheter they are still levelling, lone-wolf, or HL raider. +You play the best class, now you have the best companion for it (and its not your pet). ### 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. -- MetaHunt don't and will never support Capycraft/Ravencraft (mafia) servers, after the DDOS attacks they conducted on other servers, to only name that. Fuck you. +- The addon purposely disable itself on all Capycraft/Ravencraft Realms, following the DDOS attacks they conducted on other servers. ## Twow Data -MetaHunt ships with large Twow datastores : +MetaHunt ships with large twow datastores, and notably: - 100% accurate beasts' data, their locations, and the abilities they can learn you - All Pet families, their abilities and their diet - All pet abilities ranks -- All Ammo (Arrows and Bullets) -- All Stable, Hunter and Pet Masters +- All Ammo and ranged weapons +- All Hunter/Pet/Stable Masters - All Ammo vendors -You can browse all the beasts in game within the Book of Huntards, but also on the github site : https://DuvelCorp.github.io/MetaHunt-Web/ +Online bestiary is also available here https://DuvelCorp.github.io/MetaHunt-Web/ - ## The Great Book of Huntards - - The Book is the main GUI of MetaHunt, it allows to display the data collected about you and your pet, and browse the addon's data to find anything a Hunter needs. - -- Browse the MetaHunt datastores of NPCs (Beast, vendors, masters) and open the map to locate them like in pfQuest -- Track all info about the pet abilities that you know and don't know yet -- Track all info about your stabled pets -- Keep an history of all your pets after you abandon them or if they run away. ## Smart Ammo @@ -40,7 +35,8 @@ You can browse all the beasts in game within the Book of Huntards, but also on t - Auto equip the right ammo when you swap from a box/xbow to a gun and the other way around. ## MM Widget - Lets Make MM great again. Tracks the new 3-state of Experimental ammo cycle (Fire → Nature → Arcane), the Lock&Load procs, and has a dynamic cell that shows either Aimed Shot availability or the right shot to use following the current Experimental ammo proc if any. You can also bind it to turn it into a very efficient one-button rotation. No more excuse to not be amongst top DPS. + + 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 ! ## Tooltips @@ -56,27 +52,27 @@ Old zHunterMod addon on steroids. Fully compatible with Twow, and enhanced with - zAmmo : Track all your ammo Live and swap them in a click or bind - zPet : Intelligent collapsible pet Bar regrouping all pet spells and providing a main button that always swap on the right spell to use depending on the situation - zTrack: All your trackings in one collapsible bar -- zAspect: All your trackings in one collapsible bar +- zAspect: All your aspects in one collapsible bar - zRanged : All the ranged weapons currently in your bags to swap them fast - But also zCrafts, zMounts, zToys and zCompanions. -Additionally, a zBar can regroup all the zButtons above into a single bar for people that like clean and ordered stuff. +Additionally, a special _zBar_ can regroup all the zButtons above into a single bar for people that like clean and ordered stuff. ## Feed-O-Matic -Now compatible with Twow, feed your pet with one key bind, it will always pickup the best food for your pet. +Now compatible with Twow, feed your pet with one key bind, it will always pickup the best food for your pet, according to your preferences. ## Chronometer -The good old Chronometer addon, purged of non-necessary things and reconfigured for a pure hunter use. +The good old Chronometer addon, purged of non-necessary things and reconfigured for a pure hunter usage. ## ICU (I see you) -Old addon used to display Targets' enhanced information when you click on your minimap tracking things. MetaHunt offers many more customization options for it. +This old addon is mostly a PVP weapon used to display targets' enhanced information when you click on your minimap tracked things. MetaHunt offers many more customization options for it. ## Ammo labels - You can display on all ammo in your bags and/or bank their type and/or dps to instantly see which of those damn same arrows icons really are. + You can display some labels on all ammo in your bags/bank to instantly see which of those damn same arrows icons really are. ## Auto-Buy @@ -86,10 +82,23 @@ Define your own simple rules for auto buy ammo and pet food when you open a vend Just spam SHIFT-click on the NPC to validate quests Scorpok and Thorium arrows. Additionally you can have an enhanced tooltip that display your actual Scorpok-related items when you mouseover the Blasted Land mobs involved. - ## Beast model Viewer - -Display the model of all beasts in the world. Always know what you are going to tame exactly before running there ! You can also browse all skin models of the same pet family to find the best-looking pet according to your tastes. + ## The Great Book of Huntards + The Book is the main GUI of MetaHunt, it allows to browse the addon's data to find anything a Hunter needs. + +- Search Hunter-related NPCs (Beasts, vendors, masters) and open the map to locate them like in pfQuest. +- Beast model Viewer : Display the model of all beasts in the world. You can also browse all skin models of the same family to find the best-looking pet according to your tastes. +- Instantly find where the beasts having a specific ability are lying and plan ahead your tracking sessions. +- Get a full understanding of all Pet families. +- Display meaningfully the pet abilities that you know and don't know yet +- Get your stable's state everywhere and track all possible information about all your pets +- Keep a detailed history of all the pets you abandonned or those that ran away. + + ## Tune it the way you like + +Metahunt option panel is huge. There are hundreds of settings you can configure in order to tweak it like a boss so it fits YOUR preferences. +Its modular architecture also allows you to completely disable any module in order to decrease the addon's memory footprint and background processes. + ### Happy Hunting Fellas ! diff --git a/api/PizzaSauce.lua b/api/PizzaSauce.lua new file mode 100644 index 0000000..c07de99 --- /dev/null +++ b/api/PizzaSauce.lua @@ -0,0 +1,1347 @@ +-- PizzaSauce: Smooth, declarative animation library for WoW 1.12 + +local _G = getfenv(0) + +local lib = {} +lib.version = "0.1.1" +_G["PizzaSauce"] = lib + +lib._types = {} +lib._easing = {} +lib._active = {} +lib._pending = {} +lib._pendingN = 0 +lib._count = 0 +lib._finBuf = {} + +lib._frame = CreateFrame("Frame", nil, UIParent) +lib._frame:Hide() + +local lastTime = GetTime() + +local function activate(anim) + if anim._active then return end + anim._active = true + lib._count = lib._count + 1 + lib._active[anim] = true + if lib._count == 1 then + lastTime = GetTime() + lib._frame:Show() + end +end + +local function deactivate(anim) + if not anim._active then return end + anim._active = false + lib._count = lib._count - 1 + lib._active[anim] = nil + if lib._count <= 0 then + lib._count = 0 + if lib._pendingN == 0 then lib._frame:Hide() end + end +end + +lib._frame:SetScript("OnUpdate", function() + local now = GetTime() + local dt = now - lastTime + lastTime = now + if dt <= 0 then return end + + -- Process pending auto-play animations + local pendingN = lib._pendingN + if pendingN > 0 then + lib._pendingN = 0 + for i = 1, pendingN do + local anim = lib._pending[i] + lib._pending[i] = nil + if anim._autoplay then anim:Play() end + end + end + + -- Hide driver if nothing to do + if lib._count == 0 then + lib._frame:Hide() + return + end + + -- Tick all active, collect finished into scratch buffer. + -- Can't finish inline because _finish() callbacks may create new animations, + -- which would modify _active while we're iterating it. + local finN = 0 + for anim in pairs(lib._active) do + if anim:_tick(dt, now) then + finN = finN + 1 + lib._finBuf[finN] = anim + end + end + + for i = 1, finN do + lib._finBuf[i]:_finish() + lib._finBuf[i] = nil + end +end) + +lib._activate = activate +lib._deactivate = deactivate + +function lib._schedulePending(anim) + anim._autoplay = true + lib._pendingN = lib._pendingN + 1 + lib._pending[lib._pendingN] = anim + lastTime = GetTime() + lib._frame:Show() +end + +function lib:RegisterType(name, handlers) + lib._types[name] = handlers +end + +function lib:RegisterEasing(name, fn) + lib._easing[name] = fn +end + +function lib:RegisterHelper(name, fn) + if lib[name] then + DEFAULT_CHAT_FRAME:AddMessage( + "PizzaSauce: RegisterHelper('" .. name .. "') would overwrite an existing method") + return + end + lib[name] = fn +end +local E = lib._easing + +local pi = math.pi +local sin = math.sin +local cos = math.cos +local pow = math.pow +local sqrt = math.sqrt + +E["linear"] = function(t) return t end +E["swing"] = function(t) return 0.5 - cos(t * pi) / 2 end + +-- Quad (power of 2) +E["inQuad"] = function(t) return t * t end +E["outQuad"] = function(t) return t * (2 - t) end +E["inOutQuad"] = function(t) + if t < 0.5 then return 2 * t * t end + return -1 + (4 - 2 * t) * t +end + +-- Cubic (power of 3) +E["inCubic"] = function(t) return t * t * t end +E["outCubic"] = function(t) local u = t - 1; return u * u * u + 1 end +E["inOutCubic"] = function(t) + if t < 0.5 then return 4 * t * t * t end + local u = 2 * t - 2; return (u * u * u + 2) / 2 +end + +-- Quart (power of 4) +E["inQuart"] = function(t) return t * t * t * t end +E["outQuart"] = function(t) local u = t - 1; return 1 - u * u * u * u end +E["inOutQuart"] = function(t) + if t < 0.5 then return 8 * t * t * t * t end + local u = t - 1; return 1 - 8 * u * u * u * u +end + +-- Quint (power of 5) +E["inQuint"] = function(t) return t * t * t * t * t end +E["outQuint"] = function(t) local u = t - 1; return 1 + u * u * u * u * u end +E["inOutQuint"] = function(t) + if t < 0.5 then return 16 * t * t * t * t * t end + local u = t - 1; return 1 + 16 * u * u * u * u * u +end + +-- Sine +E["inSine"] = function(t) return 1 - cos(t * pi / 2) end +E["outSine"] = function(t) return sin(t * pi / 2) end +E["inOutSine"] = function(t) return 0.5 - cos(t * pi) / 2 end + +-- Expo +E["inExpo"] = function(t) + if t == 0 then return 0 end + return pow(2, 10 * (t - 1)) +end +E["outExpo"] = function(t) + if t == 1 then return 1 end + return 1 - pow(2, -10 * t) +end +E["inOutExpo"] = function(t) + if t == 0 or t == 1 then return t end + if t < 0.5 then return pow(2, 20 * t - 10) / 2 end + return (2 - pow(2, -20 * t + 10)) / 2 +end + +-- Circ +E["inCirc"] = function(t) return 1 - sqrt(1 - t * t) end +E["outCirc"] = function(t) local u = t - 1; return sqrt(1 - u * u) end +E["inOutCirc"] = function(t) + if t < 0.5 then return (1 - sqrt(1 - 4 * t * t)) / 2 end + local u = 2 * t - 2; return (sqrt(1 - u * u) + 1) / 2 +end + +-- Elastic +E["inElastic"] = function(t) + if t == 0 or t == 1 then return t end + return -pow(2, 10 * t - 10) * sin((t * 10 - 10.75) * (2 * pi) / 3) +end +E["outElastic"] = function(t) + if t == 0 or t == 1 then return t end + return pow(2, -10 * t) * sin((t * 10 - 0.75) * (2 * pi) / 3) + 1 +end +E["inOutElastic"] = function(t) + if t == 0 or t == 1 then return t end + if t < 0.5 then + return -pow(2, 20 * t - 10) * sin((20 * t - 11.125) * (2 * pi) / 4.5) / 2 + end + return pow(2, -20 * t + 10) * sin((20 * t - 11.125) * (2 * pi) / 4.5) / 2 + 1 +end + +-- Back +E["inBack"] = function(t) + local s = 1.70158 + return t * t * ((s + 1) * t - s) +end +E["outBack"] = function(t) + local s = 1.70158 + local u = t - 1 + return u * u * ((s + 1) * u + s) + 1 +end +E["inOutBack"] = function(t) + local s = 1.70158 * 1.525 + if t < 0.5 then + return (2 * t) * (2 * t) * ((s + 1) * (2 * t) - s) / 2 + end + local u = 2 * t - 2 + return (u * u * ((s + 1) * u + s) + 2) / 2 +end + +-- Bounce +E["outBounce"] = function(t) + if t < 1 / 2.75 then + return 7.5625 * t * t + elseif t < 2 / 2.75 then + t = t - 1.5 / 2.75 + return 7.5625 * t * t + 0.75 + elseif t < 2.5 / 2.75 then + t = t - 2.25 / 2.75 + return 7.5625 * t * t + 0.9375 + else + t = t - 2.625 / 2.75 + return 7.5625 * t * t + 0.984375 + end +end +E["inBounce"] = function(t) return 1 - E["outBounce"](1 - t) end +E["inOutBounce"] = function(t) + if t < 0.5 then return (1 - E["outBounce"](1 - 2 * t)) / 2 end + return (1 + E["outBounce"](2 * t - 1)) / 2 +end +local function lerp(a, b, t) return a + (b - a) * t end + +local function rotCorner(a) + local r = math.rad(a) + return 0.5 + math.cos(r) / 1.4142, 0.5 + math.sin(r) / 1.4142 +end + +local function applyRotation(texture, angle) + local a = -angle + local ULx, ULy = rotCorner(a + 225) + local LLx, LLy = rotCorner(a + 135) + local URx, URy = rotCorner(a - 45) + local LRx, LRy = rotCorner(a + 45) + texture:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy) +end + +lib:RegisterType("alpha", { + init = function(target, from) + return from or target:GetAlpha() + end, + apply = function(target, from, to, t) + target:SetAlpha(lerp(from, to, t)) + end, +}) + +lib:RegisterType("scale", { + init = function(target, from, to, tween) + tween._baseW = target:GetWidth() + tween._baseH = target:GetHeight() + return from or 1.0 + end, + apply = function(target, from, to, t, tween) + local s = lerp(from, to, t) + target:SetWidth(tween._baseW * s) + target:SetHeight(tween._baseH * s) + end, +}) + +lib:RegisterType("size", { + init = function(target, from) + return from or { target:GetWidth(), target:GetHeight() } + end, + apply = function(target, from, to, t) + target:SetWidth(lerp(from[1], to[1], t)) + target:SetHeight(lerp(from[2], to[2], t)) + end, +}) + +lib:RegisterType("position", { + init = function(target, from, to, tween) + local point, rel, relPoint, x, y = target:GetPoint(1) + tween._point = point or "CENTER" + tween._rel = rel + tween._relPoint = relPoint or "CENTER" + return from or { x or 0, y or 0 } + end, + apply = function(target, from, to, t, tween) + target:ClearAllPoints() + target:SetPoint(tween._point, tween._rel, tween._relPoint, + lerp(from[1], to[1], t), lerp(from[2], to[2], t)) + end, +}) + +lib:RegisterType("color", { + init = function(target, from) + return from or { 1, 1, 1, 1 } + end, + apply = function(target, from, to, t) + target:SetVertexColor( + lerp(from[1], to[1], t), + lerp(from[2], to[2], t), + lerp(from[3], to[3], t), + lerp(from[4] or 1, to[4] or 1, t)) + end, +}) + +lib:RegisterType("rotation", { + init = function(target, from) + return from or 0 + end, + apply = function(target, from, to, t) + applyRotation(target, lerp(from, to, t)) + end, +}) + +lib:RegisterType("custom", { + init = function(target, from, to, tween) + if from ~= nil then return from end + if tween._getter then return tween._getter(target) end + return 0 + end, + apply = function(target, from, to, t, tween) + if tween._setter then + tween._setter(target, lerp(from, to, t)) + end + end, +}) +local function resolveEasing(easing) + if type(easing) == "function" then return easing end + return lib._easing[easing] or lib._easing["inOutQuad"] +end + +local Tween = {} + +function Tween:Play() + self._elapsed = 0 + self._started = false + self._done = false + lib._activate(self) +end + +function Tween:Cancel() + if not self._active then return end + lib._deactivate(self) + self._done = true + if self._onCancel then self:_onCancel() end +end + +function Tween:IsPlaying() + return self._active == true +end + +function Tween:_start() + local handler = lib._types[self._type] + if handler and handler.init then + self._from = handler.init(self._target, self._from, self._to, self) + end + self._started = true + if self._onStart then self:_onStart() end +end + +function Tween:_tick(dt) + self._elapsed = self._elapsed + dt + + if self._elapsed < self._delay then return false end + + if not self._started then self:_start() end + + local t = self._duration > 0 + and math.min((self._elapsed - self._delay) / self._duration, 1) + or 1 + + local handler = lib._types[self._type] + if handler and handler.apply and self._target then + handler.apply(self._target, self._from, self._to, resolveEasing(self._easing)(t), self) + end + + if self._onUpdate then self:_onUpdate(t) end + + return t >= 1 +end + +function Tween:_finish() + lib._deactivate(self) + self._done = true + + -- Apply one final time at exactly t=1 -- the last _tick may have landed + -- slightly before 1.0 depending on frame timing. + local handler = lib._types[self._type] + if handler and handler.apply and self._target then + handler.apply(self._target, self._from, self._to, 1, self) + end + if handler and handler.cleanup then + handler.cleanup(self._target, self) + end + + if self._onFinish then self:_onFinish() end +end + +function Tween:SetFrom(val) self._from = val end +function Tween:SetTo(val) self._to = val end +function Tween:SetDuration(val) self._duration = val end +function Tween:SetDelay(val) self._delay = val end +function Tween:SetTarget(val) self._target = val end + +function Tween:_reset() + self._elapsed = 0 + self._started = false + self._done = false +end + +local TweenMT = { __index = Tween } + +local KNOWN_OPTS = { + type = true, from = true, to = true, duration = true, easing = true, + delay = true, onStart = true, onUpdate = true, onFinish = true, + onCancel = true, getter = true, setter = true, defer = true, +} + +function lib:Tween(target, opts) + local tween = setmetatable({ + _target = target, + _type = opts.type or "alpha", + _from = opts.from, + _to = opts.to, + _duration = opts.duration or 0.3, + _easing = opts.easing or "inOutQuad", + _delay = opts.delay or 0, + _onStart = opts.onStart, + _onUpdate = opts.onUpdate, + _onFinish = opts.onFinish, + _onCancel = opts.onCancel, + _getter = opts.getter, + _setter = opts.setter, + }, TweenMT) + + -- Forward extra opts to the tween for custom type handlers. + -- Internal fields use underscore prefix, so user params (vx, radius, etc.) + -- are accessible as tween.vx, tween.radius without collision. + for k, v in pairs(opts) do + if not KNOWN_OPTS[k] then + tween[k] = v + end + end + + if not opts.defer then + lib._schedulePending(tween) + end + + return tween +end +local Sequence = {} + +function Sequence:Play() + self._index = 1 + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self:_resetChildren() + lib._activate(self) +end + +function Sequence:Cancel() + if not self._active then return end + local child = self._children[self._index] + if child and child._started and child.Cancel then child:Cancel() end + lib._deactivate(self) + self._done = true + if self._onCancel then self:_onCancel() end +end + +function Sequence:IsPlaying() + return self._active == true +end + +function Sequence:_tick(dt) + self._elapsed = self._elapsed + dt + if self._elapsed < self._delay then return false end + + local child = self._children[self._index] + if not child then return true end + + if not child._started then child:_start() end + + if child:_tick(dt) then + child:_finish() + self._index = self._index + 1 + + if self._index > table.getn(self._children) then + return self:_handleLoop() + end + end + + return false +end + +function Sequence:_finish() + lib._deactivate(self) + self._done = true + if self._onFinish then self:_onFinish() end +end + +function Sequence:_start() + self._started = true +end + +function Sequence:_reset() + self._index = 1 + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self._started = false + self:_resetChildren() +end + +function Sequence:_resetChildren() + for i = 1, table.getn(self._children) do + self._children[i]:_reset() + end +end + +function Sequence:_handleLoop() + self._loopCount = self._loopCount + 1 + if self._loop ~= 0 and self._loopCount >= self._loop then + return true + end + + if self._yoyo then + local c = self._children + local n = table.getn(c) + for i = 1, math.floor(n / 2) do + c[i], c[n - i + 1] = c[n - i + 1], c[i] + end + end + + self._index = 1 + self:_resetChildren() + return false +end + +local SequenceMT = { __index = Sequence } + +function lib:Sequence(children, opts) + opts = opts or {} + + for i = 1, table.getn(children) do + children[i]._autoplay = false + end + + local seq = setmetatable({ + _children = children, + _index = 1, + _loop = opts.loop or 1, + _yoyo = opts.yoyo or false, + _loopCount = 0, + _delay = opts.delay or 0, + _elapsed = 0, + _onFinish = opts.onFinish, + _onCancel = opts.onCancel, + _done = false, + }, SequenceMT) + + if not opts.defer then + lib._schedulePending(seq) + end + + return seq +end +local Group = {} + +function Group:Play() + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self:_resetChildren() + lib._activate(self) +end + +function Group:Cancel() + if not self._active then return end + for i = 1, table.getn(self._children) do + local child = self._children[i] + if child._active or child._started then + if child.Cancel then child:Cancel() end + end + end + lib._deactivate(self) + self._done = true + if self._onCancel then self:_onCancel() end +end + +function Group:IsPlaying() + return self._active == true +end + +function Group:_tick(dt) + self._elapsed = self._elapsed + dt + if self._elapsed < self._delay then return false end + + local allDone = true + + for i = 1, table.getn(self._children) do + local child = self._children[i] + if not child._done then + if not child._started then child:_start() end + if child:_tick(dt) then + child:_finish() + else + allDone = false + end + end + end + + if allDone then + return self:_handleLoop() + end + return false +end + +function Group:_handleLoop() + self._loopCount = self._loopCount + 1 + if self._loop ~= 0 and self._loopCount >= self._loop then + return true + end + + if self._yoyo then + local c = self._children + local n = table.getn(c) + for i = 1, math.floor(n / 2) do + c[i], c[n - i + 1] = c[n - i + 1], c[i] + end + end + + self:_resetChildren() + return false +end + +function Group:_finish() + lib._deactivate(self) + self._done = true + if self._onFinish then self:_onFinish() end +end + +function Group:_start() + self._started = true +end + +function Group:_reset() + self._loopCount = 0 + self._elapsed = 0 + self._done = false + self._started = false + self:_resetChildren() +end + +function Group:_resetChildren() + for i = 1, table.getn(self._children) do + self._children[i]:_reset() + end +end + +local GroupMT = { __index = Group } + +function lib:Group(children, opts) + opts = opts or {} + + for i = 1, table.getn(children) do + children[i]._autoplay = false + end + + local grp = setmetatable({ + _children = children, + _loop = opts.loop or 1, + _yoyo = opts.yoyo or false, + _loopCount = 0, + _delay = opts.delay or 0, + _elapsed = 0, + _onFinish = opts.onFinish, + _onCancel = opts.onCancel, + _done = false, + }, GroupMT) + + if not opts.defer then + lib._schedulePending(grp) + end + + return grp +end +-- Directional offsets for slide animations +local directions = { + LEFT = function(d) return { -d, 0 } end, + RIGHT = function(d) return { d, 0 } end, + UP = function(d) return { 0, d } end, + DOWN = function(d) return { 0,-d } end, +} + +local function dirOffset(dir, dist) + local fn = directions[string.upper(dir or "LEFT")] + return fn and fn(dist) or { 0, 0 } +end + +local function getAnchorPos(frame) + local _, _, _, x, y = frame:GetPoint(1) + return { x or 0, y or 0 } +end + +local EMPTY = {} +local function opts(o) + if type(o) == "table" then return o end + return EMPTY +end + +-- ============================================================ +-- Alpha +-- ============================================================ + +function lib:FadeTo(target, toAlpha, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "alpha", to = toAlpha, + duration = duration, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:FadeIn(target, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "alpha", from = 0, to = 1, + duration = duration, easing = easing, + onStart = function() + target:SetAlpha(0) + target:Show() + if o.onStart then o.onStart() end + end, + delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:FadeOut(target, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return lib:FadeTo(target, 0, duration, easing, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +function lib:Flash(target, count, duration, o) + o = opts(o) + count = count or 3 + duration = duration or 0.6 + local half = duration / count / 2 + local steps = {} + for i = 1, count do + table.insert(steps, lib:Tween(target, { type = "alpha", to = 0, duration = half, easing = "inQuad", defer = true })) + table.insert(steps, lib:Tween(target, { type = "alpha", to = 1, duration = half, easing = "outQuad", defer = true })) + end + return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Breathe(target, minAlpha, maxAlpha, duration, o) + o = opts(o) + duration = duration or 1.6 + local half = duration / 2 + return lib:Sequence({ + lib:Tween(target, { type = "alpha", from = minAlpha, to = maxAlpha, duration = half, easing = "inOutQuad", defer = true }), + lib:Tween(target, { type = "alpha", from = maxAlpha, to = minAlpha, duration = half, easing = "inOutQuad", defer = true }), + }, { loop = 0, delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Scale +-- ============================================================ + +function lib:ScaleIn(target, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "scale", from = 0, to = 1, + duration = duration or 0.3, easing = easing or "outBack", + onStart = function() + target:Show() + if o.onStart then o.onStart() end + end, + delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:ScaleOut(target, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return lib:Tween(target, { + type = "scale", from = 1, to = 0, + duration = duration or 0.3, easing = easing or "inBack", + onStart = o.onStart, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + onCancel = o.onCancel, delay = o.delay, defer = o.defer, + }) +end + +function lib:Pulse(target, scale, duration, o) + o = opts(o) + scale = scale or 1.3 + local half = (duration or 0.4) / 2 + local baseW, baseH = target:GetWidth(), target:GetHeight() + local function set(f, s) + f:SetWidth(baseW * s) + f:SetHeight(baseH * s) + end + return lib:Sequence({ + lib:Tween(target, { type = "custom", from = 1.0, to = scale, duration = half, easing = "outQuad", setter = set, defer = true }), + lib:Tween(target, { type = "custom", from = scale, to = 1.0, duration = half, easing = "inQuad", setter = set, defer = true }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Rubber(target, scale, duration, o) + o = opts(o) + return lib:Tween(target, { + type = "scale", from = scale or 1.3, to = 1.0, + duration = duration or 0.5, easing = "outElastic", + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:Flip(target, axis, duration, o) + o = opts(o) + local quarter = (duration or 0.8) / 4 + local horizontal = axis ~= "y" + local base = horizontal and target:GetWidth() or target:GetHeight() + local set = horizontal + and function(f, v) f:SetWidth(v) end + or function(f, v) f:SetHeight(v) end + + -- Find child textures to mirror via SetTexCoord + local textures = {} + if target.GetTexture then + table.insert(textures, target) + else + local children = { target:GetRegions() } + for i = 1, table.getn(children) do + if children[i].GetTexture then table.insert(textures, children[i]) end + end + end + + local function mirror() + for i = 1, table.getn(textures) do + if horizontal then + textures[i]:SetTexCoord(1, 0, 0, 1) + else + textures[i]:SetTexCoord(0, 1, 1, 0) + end + end + end + + local function unmirror() + for i = 1, table.getn(textures) do + textures[i]:SetTexCoord(0, 1, 0, 1) + end + end + + local shrink = function(f, v) set(f, v) end + return lib:Sequence({ + -- 0°→90°: shrink to invisible + lib:Tween(target, { type = "custom", from = base, to = 0.01, duration = quarter, easing = "inQuad", setter = shrink, defer = true }), + -- 90°→180°: mirror and expand (showing flipped content) + lib:Tween(target, { type = "custom", from = 0.01, to = base, duration = quarter, easing = "outQuad", setter = shrink, defer = true, + onStart = function() mirror() end }), + -- 180°→270°: shrink again + lib:Tween(target, { type = "custom", from = base, to = 0.01, duration = quarter, easing = "inQuad", setter = shrink, defer = true }), + -- 270°→360°: unmirror and expand back to original + lib:Tween(target, { type = "custom", from = 0.01, to = base, duration = quarter, easing = "outQuad", setter = shrink, defer = true, + onStart = function() unmirror() end }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Position +-- ============================================================ + +function lib:Move(target, from, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "position", from = from, to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:MoveTo(target, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "position", to = to, + duration = duration, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:SlideIn(target, direction, distance, duration, easing, o) + o = opts(o) + local dest = getAnchorPos(target) + local offset = dirOffset(direction, distance or 100) + local from = { dest[1] - offset[1], dest[2] - offset[2] } + return lib:Group({ + lib:Tween(target, { + type = "position", from = from, to = dest, duration = duration or 0.3, easing = easing or "outCubic", + onStart = function() + target:SetAlpha(0) + target:Show() + if o.onStart then o.onStart() end + end, + defer = true, + }), + lib:Tween(target, { + type = "alpha", + from = 0, + to = 1, + duration = (duration or 0.3) * 0.6, + easing = "outQuad", + defer = true, + }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:SlideOut(target, direction, distance, duration, easing, o) + o = opts(o) + local from = getAnchorPos(target) + local offset = dirOffset(direction, distance or 100) + local dest = { from[1] + offset[1], from[2] + offset[2] } + local onFinish = o.onFinish + return lib:Group({ + lib:Tween(target, { type = "position", from = from, to = dest, duration = duration or 0.3, easing = easing or "inCubic", defer = true }), + lib:Tween(target, { type = "alpha", to = 0, duration = duration or 0.3, easing = "inQuad", defer = true }), + }, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +function lib:Bounce(target, height, count, duration, o) + o = opts(o) + height = height or 30 + count = count or 3 + duration = duration or 0.6 + local pos = getAnchorPos(target) + local steps = {} + for i = 1, count do + local h = height * (1 - (i - 1) / count) -- decay + table.insert(steps, lib:Tween(target, { + type = "position", from = pos, to = { pos[1], pos[2] + h }, + duration = duration / count / 2, easing = "outQuad", defer = true, + })) + table.insert(steps, lib:Tween(target, { + type = "position", from = { pos[1], pos[2] + h }, to = pos, + duration = duration / count / 2, easing = "inQuad", defer = true, + })) + end + return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Shake(target, intensity, duration, o) + o = opts(o) + intensity = intensity or 5 + duration = duration or 0.3 + local pos = getAnchorPos(target) + local steps = {} + local stepDur = 0.03 + local numSteps = math.floor(duration / stepDur) + for i = 1, numSteps do + local decay = 1 - (i - 1) / numSteps + local dx = (math.random() * intensity * 2 - intensity) * decay + local dy = (math.random() * intensity * 2 - intensity) * decay + table.insert(steps, lib:Tween(target, { + type = "position", to = { pos[1] + dx, pos[2] + dy }, + duration = stepDur, easing = "linear", defer = true, + })) + end + -- Snap back to original position + table.insert(steps, lib:Tween(target, { + type = "position", to = pos, + duration = stepDur, easing = "linear", defer = true, + })) + return lib:Sequence(steps, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:FlyTo(frame, targetFrame, duration, easing, o) + o = opts(o) + local toX = targetFrame:GetLeft() + targetFrame:GetWidth() / 2 - UIParent:GetWidth() / 2 + local toY = targetFrame:GetBottom() + targetFrame:GetHeight() / 2 - UIParent:GetHeight() / 2 + return lib:Tween(frame, { + type = "position", to = { toX, toY }, + duration = duration or 0.5, easing = easing or "inOutQuad", + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Rotation +-- ============================================================ + +function lib:Spin(texture, degreesPerSecond, o) + o = opts(o) + local dps = degreesPerSecond or 90 + return lib:Sequence({ + lib:Tween(texture, { + type = "rotation", + from = 0, + to = dps > 0 and 360 or -360, + duration = 360 / math.abs(dps), + easing = "linear", + defer = true, + }), + }, { loop = 0, delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Color +-- ============================================================ + +function lib:Color(target, from, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "color", from = from, to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:ColorFlash(texture, r, g, b, duration, o) + o = opts(o) + duration = duration or 0.4 + local half = duration / 2 + return lib:Sequence({ + lib:Tween(texture, { type = "color", to = { r, g, b, 1 }, duration = half, easing = "outQuad", defer = true }), + lib:Tween(texture, { type = "color", from = { r, g, b, 1 }, to = { 1, 1, 1, 1 }, duration = half, easing = "inQuad", defer = true }), + }, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +-- ============================================================ +-- Size +-- ============================================================ + +function lib:Size(target, from, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "size", from = from, to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +function lib:Morph(target, to, duration, easing, o) + o = opts(o) + return lib:Tween(target, { + type = "size", to = to, + duration = duration or 0.3, easing = easing, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Value +-- ============================================================ + +function lib:Progress(statusbar, toValue, duration, easing, o) + o = opts(o) + return lib:Tween(statusbar, { + type = "custom", + to = toValue, + duration = duration or 0.4, + easing = easing or "outCubic", + getter = function(bar) return bar:GetValue() end, + setter = function(bar, val) bar:SetValue(val) end, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Text +-- ============================================================ + +function lib:Typewriter(fontstring, text, duration, o) + o = opts(o) + local len = string.len(text) + return lib:Tween(fontstring, { + type = "custom", + from = 0, to = len, + duration = duration or len * 0.05, + easing = "linear", + setter = function(fs, val) + fs:SetText(string.sub(text, 1, math.floor(val))) + end, + delay = o.delay, onStart = o.onStart, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer, + }) +end + +-- ============================================================ +-- Clipping (Reveal / Conceal) +-- ============================================================ + +-- Progressively reveal a texture from a direction by re-anchoring +-- to one edge, animating width/height, and shifting tex coords. +local clipAnchors = { + LEFT = { "TOPLEFT", "TOPLEFT" }, + RIGHT = { "TOPRIGHT", "TOPRIGHT" }, + UP = { "TOPLEFT", "TOPLEFT" }, + DOWN = { "BOTTOMLEFT", "BOTTOMLEFT" }, +} + +local function clipTween(target, direction, duration, easing, from, to, o) + o = opts(o) + direction = string.upper(direction or "LEFT") + duration = duration or 0.3 + + local parent = target:GetParent() + local baseW = target._clipBaseW or parent:GetWidth() + local baseH = target._clipBaseH or parent:GetHeight() + target._clipBaseW = baseW + target._clipBaseH = baseH + + local horizontal = (direction == "LEFT" or direction == "RIGHT") + local reversed = (direction == "RIGHT" or direction == "UP") + local anchor = clipAnchors[direction] + + target:ClearAllPoints() + target:SetPoint(anchor[1], parent, anchor[2], 0, 0) + + return lib:Tween(target, { + type = "custom", from = from, to = to, + duration = duration, easing = easing or "outQuad", + setter = function(tex, t) + if horizontal then + tex:SetWidth(math.max(baseW * t, 0.01)) + tex:SetHeight(baseH) + if reversed then + tex:SetTexCoord(1 - t, 1, 0, 1) + else + tex:SetTexCoord(0, t, 0, 1) + end + else + tex:SetWidth(baseW) + tex:SetHeight(math.max(baseH * t, 0.01)) + if reversed then + tex:SetTexCoord(0, 1, 1 - t, 1) + else + tex:SetTexCoord(0, 1, 0, t) + end + end + end, + onFinish = function() + target:SetTexCoord(0, 1, 0, 1) + target:ClearAllPoints() + target:SetAllPoints(parent) + if o.onFinish then o.onFinish() end + end, + onCancel = o.onCancel, delay = o.delay, defer = o.defer, + }) +end + +function lib:Reveal(target, direction, duration, easing, o) + target:Show() + return clipTween(target, direction, duration, easing, 0, 1, o) +end + +function lib:Conceal(target, direction, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return clipTween(target, direction, duration, easing, 1, 0, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +-- WipeIn/WipeOut: texture slides in from a direction while being clipped. +-- Unlike Reveal/Conceal where the image is stationary and a window opens, +-- here the image physically moves into view (like sliding out from under a cover). +-- WipeIn/WipeOut: the image slides into/out of view while being clipped. +-- The texture stays full-size (no stretching) -- we clip by shrinking the +-- visible region and offsetting tex coords so the revealed portion matches +-- the correct part of the image. +-- +-- For "LEFT" (image slides in from the left edge): +-- anchor at TOPLEFT, width = baseW * t, texCoord left = 1-t +-- → at t=0 nothing visible; at t=1 full image +local wipeAnchors = { + LEFT = "TOPLEFT", + RIGHT = "TOPRIGHT", + UP = "TOPLEFT", + DOWN = "BOTTOMLEFT", +} + +local function wipeTween(target, direction, duration, easing, from, to, o) + o = opts(o) + direction = string.upper(direction or "LEFT") + duration = duration or 0.3 + + local parent = target:GetParent() + local baseW = target._clipBaseW or parent:GetWidth() + local baseH = target._clipBaseH or parent:GetHeight() + target._clipBaseW = baseW + target._clipBaseH = baseH + + local horizontal = (direction == "LEFT" or direction == "RIGHT") + local anchor = wipeAnchors[direction] + + target:ClearAllPoints() + target:SetPoint(anchor, parent, anchor, 0, 0) + + return lib:Tween(target, { + type = "custom", from = from, to = to, + duration = duration, easing = easing or "outQuad", + setter = function(tex, t) + if horizontal then + tex:SetWidth(math.max(baseW * t, 0.01)) + tex:SetHeight(baseH) + if direction == "LEFT" then + tex:SetTexCoord(1 - t, 1, 0, 1) + else + tex:SetTexCoord(0, t, 0, 1) + end + else + tex:SetWidth(baseW) + tex:SetHeight(math.max(baseH * t, 0.01)) + if direction == "DOWN" then + tex:SetTexCoord(0, 1, 1 - t, 1) + else + tex:SetTexCoord(0, 1, 0, t) + end + end + end, + onFinish = function() + target:SetTexCoord(0, 1, 0, 1) + target:ClearAllPoints() + target:SetAllPoints(parent) + if o.onFinish then o.onFinish() end + end, + onCancel = o.onCancel, delay = o.delay, defer = o.defer, + }) +end + +local wipeInFlip = { LEFT="RIGHT", RIGHT="LEFT", UP="DOWN", DOWN="UP" } + +function lib:WipeIn(target, direction, duration, easing, o) + target:Show() + local dir = wipeInFlip[string.upper(direction or "LEFT")] or direction + return wipeTween(target, dir, duration, easing, 0, 1, o) +end + +function lib:WipeOut(target, direction, duration, easing, o) + o = opts(o) + local onFinish = o.onFinish + return wipeTween(target, direction, duration, easing, 1, 0, { + delay = o.delay, onCancel = o.onCancel, defer = o.defer, + onFinish = function() target:Hide(); if onFinish then onFinish() end end, + }) +end + +-- ============================================================ +-- Meta / Utility +-- ============================================================ + +function lib:Stagger(targets, animFn, delay, o) + o = opts(o) + delay = delay or 0.08 + local children = {} + for i = 1, table.getn(targets) do + local anim = animFn(targets[i], i) + anim._autoplay = false + if i > 1 then + table.insert(children, lib:Sequence({ lib:Delay(delay * (i - 1)), anim }, { defer = true })) + else + table.insert(children, anim) + end + end + return lib:Group(children, { delay = o.delay, onFinish = o.onFinish, onCancel = o.onCancel, defer = o.defer }) +end + +function lib:Delay(duration) + -- A no-op tween that just waits. Uses a dummy target-less approach. + local dummy = lib._frame + return lib:Tween(dummy, { + type = "alpha", + from = dummy:GetAlpha(), + to = dummy:GetAlpha(), + duration = duration or 1, + defer = true, + }) +end + +function lib:Run(fn, o) + o = opts(o) + return lib:Tween(nil, { + type = "custom", from = 0, to = 0, duration = 0, + onStart = fn, delay = o.delay, defer = o.defer, + }) +end + +function lib:Stop(target) + for anim in pairs(lib._active) do + if anim._target == target then + anim:Cancel() + elseif anim._children then + lib:_stopInChildren(anim, anim, target) + end + end + -- Also cancel pending (not yet started) animations for this target + for i = 1, lib._pendingN do + local anim = lib._pending[i] + if anim then + if anim._target == target then + anim._autoplay = false + elseif anim._children then + if lib:_hasTargetInChildren(anim, target) then + anim._autoplay = false + end + end + end + end +end + +function lib:_hasTargetInChildren(parent, target) + for i = 1, table.getn(parent._children) do + local child = parent._children[i] + if child._target == target then return true end + if child._children and lib:_hasTargetInChildren(child, target) then return true end + end + return false +end + +function lib:_stopInChildren(root, parent, target) + for i = 1, table.getn(parent._children) do + local child = parent._children[i] + if child._target == target then + root:Cancel() + return + elseif child._children then + lib:_stopInChildren(root, child, target) + if not root._active then return end + end + end +end diff --git a/api/core-scan-beasttraining.lua b/api/core-scan-beasttraining.lua index 4dd0316..11586a6 100644 --- a/api/core-scan-beasttraining.lua +++ b/api/core-scan-beasttraining.lua @@ -432,6 +432,9 @@ local function MTH_PT_ProcessLearnSystemMessage(rawMessage) if changed then MTH_PT_ApplyScan("learn-msg") end + if changed and type(MTH_CelebratePetAbility) == "function" then + MTH_CelebratePetAbility(label) + end if type(MTH_PSP_RequestScan) == "function" then MTH_PSP_RequestScan("train-learn-msg") end diff --git a/api/core.lua b/api/core.lua index dc1d2da..672b481 100644 --- a/api/core.lua +++ b/api/core.lua @@ -325,6 +325,69 @@ function SlashCmdList.MTH(msg, editbox) MTH_CommandBook() elseif lowerMsg == "peers" or lowerMsg == "who" then MTH_CommandPeers() + elseif lowerCmd == "trainscan" or lowerCmd == "petscan" then + if type(MTH_CommandTrainScan) == "function" then + MTH_CommandTrainScan() + else + MTH:Print("Train scan is not available.") + 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)") + end + else + -- Self-diagnosing: report which piece failed to load so we can tell + -- a stale UI from a genuine load error. + MTH:Print("Celebration effect is not available. Diagnostics:") + MTH:Print(" PizzaSauce global: " .. type(PizzaSauce)) + MTH:Print(" MTH_Celebrate: " .. type(MTH_Celebrate)) + MTH:Print(" MTH_FX_IsEnabled: " .. type(MTH_FX_IsEnabled)) + MTH:Print(" MTH_SetupAnimationsOptions: " .. type(MTH_SetupAnimationsOptions)) + MTH:Print(" -> If these are 'nil', do /reload (your UI is running code from before the feature was added).") + end + elseif lowerCmd == "fxdebug" then + -- TEMP: dump the animation load-path tracers so we can see exactly where + -- fx-celebrate.lua / options-animations.lua aborted at load time. + MTH:Print("=== MetaHunt animation load debug ===") + MTH:Print("Globals: PizzaSauce=" .. type(PizzaSauce) + .. " CreateFrame=" .. type(CreateFrame) + .. " MTH=" .. type(MTH)) + if type(MTH_FX_DBG) == "table" then + MTH:Print("fx-celebrate: step=" .. tostring(MTH_FX_DBG.step) + .. " sauceType=" .. tostring(MTH_FX_DBG.sauceType) + .. " registerOk=" .. tostring(MTH_FX_DBG.registerOk) + .. " registerSkipped=" .. tostring(MTH_FX_DBG.registerSkipped)) + if MTH_FX_DBG.registerErr ~= nil then + MTH:Print("fx-celebrate: registerErr=" .. tostring(MTH_FX_DBG.registerErr)) + end + MTH:Print("fx-celebrate: hasCelebrate=" .. tostring(MTH_FX_DBG.hasCelebrate) + .. " hasIsEnabled=" .. tostring(MTH_FX_DBG.hasIsEnabled) + .. " hasAnimEnabled=" .. tostring(MTH_FX_DBG.hasAnimEnabled)) + else + MTH:Print("fx-celebrate: MTH_FX_DBG is " .. type(MTH_FX_DBG) .. " (file did not reach its first line)") + end + if type(MTH_ANIM_DBG) == "table" then + MTH:Print("options-animations: step=" .. tostring(MTH_ANIM_DBG.step) + .. " ready=" .. tostring(MTH_ANIM_DBG.ready) + .. " hasSetup=" .. tostring(MTH_ANIM_DBG.hasSetup)) + MTH:Print("options-animations: req=" .. tostring(MTH_ANIM_DBG.reqType) + .. " getFrame=" .. tostring(MTH_ANIM_DBG.getFrameType) + .. " clear=" .. tostring(MTH_ANIM_DBG.clearType) + .. " checkbox=" .. tostring(MTH_ANIM_DBG.checkboxType)) + MTH:Print("options-animations: setupCalled=" .. tostring(MTH_ANIM_DBG.setupCalled) + .. " setupResult=" .. tostring(MTH_ANIM_DBG.setupResult) + .. " containerType=" .. tostring(MTH_ANIM_DBG.containerType) + .. " fxEnabledType=" .. tostring(MTH_ANIM_DBG.fxEnabledType)) + else + MTH:Print("options-animations: MTH_ANIM_DBG is " .. type(MTH_ANIM_DBG) .. " (file did not reach its first line)") + end + MTH:Print("=== end animation load debug ===") elseif lowerMsg == "zoneinfo" then -- Dumps all available zone data for the current zone, for adding to ds-zones / ds-minimap-sizes pcall(SetMapToCurrentZone) diff --git a/api/fx-celebrate.lua b/api/fx-celebrate.lua new file mode 100644 index 0000000..9cda041 --- /dev/null +++ b/api/fx-celebrate.lua @@ -0,0 +1,297 @@ +-- MetaHunt: center-screen celebration effect (message + firework burst). +-- Uses the embedded PizzaSauce animation library. Degrades to a silent no-op +-- when PizzaSauce or the WoW frame API are unavailable (e.g. offline tests). + +-- Capture our own PizzaSauce instance now, before another addon that ships its +-- own copy can overwrite the global. This is the pattern the library requires. +local Sauce = PizzaSauce + +-- Localization helper: resolve a key through MetaHunt's loc system, falling back +-- to the inline English default. Every user-facing string is keyed so the enUS +-- strings can be harvested later; the inline default is the working English. +local function MTH_FX_L(key, default) + if MTH and type(MTH.GetLocalization) == "function" then + return MTH:GetLocalization(key, default) + end + return default or key +end + +-- --------------------------------------------------------------------------- +-- DEBUG: load-path tracer. Global so /mth fxdebug can dump it. Records how far +-- this file got at load time and any error from the one executable file-scope +-- block (RegisterType). TEMP diagnostic. +-- --------------------------------------------------------------------------- +MTH_FX_DBG = { + sauceType = type(PizzaSauce), + createFrameType = type(CreateFrame), + step = "1-top", +} + +-- --------------------------------------------------------------------------- +-- Animation settings (shared by the effect code and the options "Animations" tab) +-- --------------------------------------------------------------------------- +-- Sections shown, in order, in the options panel. `titleKey` localizes `title`. +MTH_FX_ANIM_SECTIONS = { + { id = "pet", title = "Pet", titleKey = "ANIM_SECTION_PET" }, + { id = "book", title = "Book", titleKey = "ANIM_SECTION_BOOK" }, + { id = "ui", title = "Interface", titleKey = "ANIM_SECTION_UI" }, +} + +-- Every individual animation the addon can play. Each has a stable `key`, the +-- section it belongs to, a checkbox label, an optional tooltip, and a default. +-- 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 = "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.", + tooltipKey = "ANIM_PET_CELEBRATE_TIP" }, + { key = "book.open", section = "book", default = true, + label = "Fade in the Hunter's Book", labelKey = "ANIM_BOOK_OPEN", + tooltip = "Fade the Hunter's Book window in when you open it.", + tooltipKey = "ANIM_BOOK_OPEN_TIP" }, + { key = "book.tabs", section = "book", default = true, + label = "Fade Hunter's Book tab changes", labelKey = "ANIM_BOOK_TABS", + tooltip = "Fade the results list back in when you switch tabs inside the Hunter's Book.", + tooltipKey = "ANIM_BOOK_TABS_TIP" }, + { key = "ui.options", section = "ui", default = true, + label = "Slide the options window open", labelKey = "ANIM_UI_OPTIONS", + tooltip = "Slide and fade the MetaHunt options window in when you open it.", + tooltipKey = "ANIM_UI_OPTIONS_TIP" }, +} + +local function MTH_FX_Store() + if MTH and type(MTH.GetModuleSavedVariables) == "function" then + return MTH:GetModuleSavedVariables("animations") + end + return nil +end + +local function MTH_FX_DefaultFor(key) + local i = 1 + while i <= table.getn(MTH_FX_ANIM_DEFS) do + if MTH_FX_ANIM_DEFS[i].key == key then + return MTH_FX_ANIM_DEFS[i].default and true or false + end + i = i + 1 + end + return true +end + +function MTH_FX_AnimationsEnabled() + local store = MTH_FX_Store() + if not store or store.enabled == nil then + return true + end + return store.enabled and true or false +end + +function MTH_FX_SetAnimationsEnabled(value) + local store = MTH_FX_Store() + if not store then return end + store.enabled = value and true or false +end + +function MTH_FX_GetFlag(key) + local store = MTH_FX_Store() + if store and store.flags and store.flags[key] ~= nil then + return store.flags[key] and true or false + end + return MTH_FX_DefaultFor(key) +end + +function MTH_FX_SetFlag(key, value) + local store = MTH_FX_Store() + if not store then return end + store.flags = store.flags or {} + store.flags[key] = value and true or false +end + +-- Master gate every animation call site checks: false when animations are +-- globally disabled, or when this specific animation is turned off. +function MTH_FX_IsEnabled(key) + if not MTH_FX_AnimationsEnabled() then + return false + end + if key == nil then + return true + end + return MTH_FX_GetFlag(key) +end + +-- Register a one-shot "ballistic" tween type: a spark that flies out from the +-- center along a velocity vector while gravity pulls it back down. +MTH_FX_DBG.step = "2-before-registertype" +if Sauce and type(Sauce.RegisterType) == "function" and not Sauce.MTH_HasBallistic then + local ok, err = pcall(function() + Sauce.MTH_HasBallistic = true + Sauce:RegisterType("mth_ballistic", { + init = function(target, from) + return from or { 0, 0 } + end, + apply = function(target, from, to, t, tween) + local dur = tween.dur or 1 + local elapsed = t * dur + local vx = tween.vx or 0 + local vy = tween.vy or 0 + local gravity = tween.gravity or 0 + local x = from[1] + vx * elapsed + local y = from[2] + vy * elapsed - 0.5 * gravity * elapsed * elapsed + local anchor = tween.anchor or UIParent + target:ClearAllPoints() + target:SetPoint("CENTER", anchor, "CENTER", x, y) + end, + }) + end) + MTH_FX_DBG.registerOk = ok + MTH_FX_DBG.registerErr = err +else + MTH_FX_DBG.registerSkipped = true +end +MTH_FX_DBG.step = "3-after-registertype" + +local MTH_FX_NUM_SPARKS = 22 +local MTH_FX_Container +local MTH_FX_MsgFrame +local MTH_FX_TitleFS +local MTH_FX_SubFS +local MTH_FX_Sparks = {} + +-- Palette for the firework sparks (r, g, b), warm celebratory tones. +local MTH_FX_Colors = { + { 1.00, 0.82, 0.00 }, + { 1.00, 0.45, 0.10 }, + { 1.00, 0.20, 0.30 }, + { 0.35, 0.70, 1.00 }, + { 0.55, 1.00, 0.45 }, + { 0.85, 0.50, 1.00 }, +} + +local function MTH_FX_EnsureFrames() + if type(CreateFrame) ~= "function" then + return false + end + if MTH_FX_Container then + return true + end + + MTH_FX_Container = CreateFrame("Frame", "MTH_FX_Container", UIParent) + MTH_FX_Container:SetWidth(1) + MTH_FX_Container:SetHeight(1) + MTH_FX_Container:SetPoint("CENTER", UIParent, "CENTER", 0, 40) + MTH_FX_Container:SetFrameStrata("FULLSCREEN_DIALOG") + MTH_FX_Container:Hide() + + MTH_FX_MsgFrame = CreateFrame("Frame", "MTH_FX_Message", UIParent) + MTH_FX_MsgFrame:SetWidth(512) + MTH_FX_MsgFrame:SetHeight(90) + MTH_FX_MsgFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 80) + MTH_FX_MsgFrame:SetFrameStrata("FULLSCREEN_DIALOG") + + MTH_FX_TitleFS = MTH_FX_MsgFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge") + MTH_FX_TitleFS:SetPoint("CENTER", MTH_FX_MsgFrame, "CENTER", 0, 14) + MTH_FX_TitleFS:SetTextColor(1, 0.82, 0) + + MTH_FX_SubFS = MTH_FX_MsgFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") + MTH_FX_SubFS:SetPoint("TOP", MTH_FX_TitleFS, "BOTTOM", 0, -8) + MTH_FX_SubFS:SetTextColor(1, 1, 1) + + MTH_FX_MsgFrame:Hide() + + local i = 1 + while i <= MTH_FX_NUM_SPARKS do + local spark = MTH_FX_Container:CreateTexture(nil, "OVERLAY") + spark:SetTexture("Interface\\Cooldown\\star4") + spark:SetWidth(22) + spark:SetHeight(22) + spark:SetBlendMode("ADD") + spark:SetPoint("CENTER", MTH_FX_Container, "CENTER", 0, 0) + spark:Hide() + MTH_FX_Sparks[i] = spark + 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) + if not Sauce then + return + end + if not MTH_FX_EnsureFrames() then + return + 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 }), + }) + + MTH_FX_Container:Show() + local colorCount = table.getn(MTH_FX_Colors) + local colorIdx = 0 + local i = 1 + while i <= MTH_FX_NUM_SPARKS do + local spark = MTH_FX_Sparks[i] + 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 + + 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:Show() + + Sauce:Stop(spark) + Sauce:Group({ + Sauce:Tween(spark, { + type = "mth_ballistic", + from = { 0, 0 }, + duration = dur, + easing = "linear", + anchor = MTH_FX_Container, + dur = dur, + vx = vx, + vy = vy, + gravity = 300, + }), + Sauce:FadeTo(spark, 0, 0.45, "inQuad", { + delay = dur - 0.45, + onFinish = function() + spark:Hide() + end, + }), + }) + i = i + 1 + end +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 + return + end + MTH_Celebrate(MTH_FX_L("ANIM_PET_LEARNED_TITLE", "New Pet Ability!"), label) +end + +-- DEBUG: mark that the whole file finished loading and record which globals it +-- successfully defined. TEMP diagnostic. +MTH_FX_DBG.step = "4-complete" +MTH_FX_DBG.hasCelebrate = type(MTH_Celebrate) +MTH_FX_DBG.hasIsEnabled = type(MTH_FX_IsEnabled) +MTH_FX_DBG.hasAnimEnabled = type(MTH_FX_AnimationsEnabled) diff --git a/api/hunterbook.lua b/api/hunterbook.lua index 2f1808a..f531778 100644 --- a/api/hunterbook.lua +++ b/api/hunterbook.lua @@ -1,5 +1,34 @@ -- MetaHunt standalone Hunter Book UI (XML + Lua) +-- Capture our PizzaSauce instance for opening animations (may be nil if the +-- library is unavailable; all uses are guarded). +local Sauce = PizzaSauce + +-- Fade the book window in on open, unless the player disabled it in options. +local function MTH_BOOK_FadeInWindow(frame) + if not Sauce or not frame then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("book.open") then return end + -- Hide the window's alpha THIS frame: Show() already ran, so without this the + -- book renders fully opaque for one frame before the tween's onStart snaps it + -- to 0 -- a flash that makes the fade look like it never happened. + if type(frame.SetAlpha) == "function" then frame:SetAlpha(0) end + -- Slide the whole window up into place while fading in. Position and alpha are + -- the only safe properties on a complex frame: the built-in "scale" tween + -- resizes width/height, which the child widgets do NOT follow (they clip), so + -- a slide + fade is the strongest effect that keeps the layout intact. + Sauce:SlideIn(frame, "UP", 60, 0.4, "outCubic") +end + +-- Fade the results list back in when switching Hunter's Book tabs, unless disabled. +local function MTH_BOOK_AnimateTabSwitch() + if not Sauce then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("book.tabs") then return end + local list = getglobal("MTH_BOOK_ListBackdrop") + if not list then return end + if type(list.SetAlpha) == "function" then list:SetAlpha(0) end + Sauce:FadeIn(list, 0.25) +end + MTH_HUNTERBOOK_LOADED = true MTH_HUNTERBOOK_TABS = MTH_HUNTERBOOK_TABS or {} @@ -4929,6 +4958,7 @@ local function MTH_BOOK_SetMode(mode) MTH_BOOK_UpdateModeLabels() MTH_BOOK_UpdateSectionTabs() MTH_BOOK_RefreshFilter() + MTH_BOOK_AnimateTabSwitch() end function MTH_BOOK_JumpToBeastById(beastId) @@ -5712,6 +5742,7 @@ function MTH_OpenHunterBook() local frame = MTH_BOOK_EnsureWindow() if not frame then return end frame:Show() + MTH_BOOK_FadeInWindow(frame) if MTH_BOOK_UpdateRealmGateOverlay(frame) then return end MTH_BOOK_RefreshFilter() end @@ -5724,6 +5755,7 @@ function MTH_ToggleHunterBook() frame:Hide() else frame:Show() + MTH_BOOK_FadeInWindow(frame) if MTH_BOOK_UpdateRealmGateOverlay(frame) then return end MTH_BOOK_RefreshFilter() end diff --git a/api/options-animations.lua b/api/options-animations.lua new file mode 100644 index 0000000..f36fd77 --- /dev/null +++ b/api/options-animations.lua @@ -0,0 +1,173 @@ +-- MetaHunt: "Animations" options panel. +-- Global enable/disable plus a per-animation checkbox for each effect, grouped +-- into sections (Pet, Book, ...). The animation registry lives in fx-celebrate.lua +-- (MTH_FX_ANIM_SECTIONS / MTH_FX_ANIM_DEFS) so both the effects and this panel +-- stay in sync. + +local MTH_ANIM_READY = MTH_OptionsRequire and MTH_OptionsRequire("options-animations", { + "MTH_GetFrame", + "MTH_ClearContainer", + "MTH_CreateCheckbox", +}) + +-- DEBUG: load-path tracer, global so /mth fxdebug can dump it. TEMP diagnostic. +MTH_ANIM_DBG = { + ready = tostring(MTH_ANIM_READY), + reqType = type(MTH_OptionsRequire), + getFrameType = type(MTH_GetFrame), + clearType = type(MTH_ClearContainer), + checkboxType = type(MTH_CreateCheckbox), + step = "1-top", +} + +local MTH_ANIM_LINK = "https://codeberg.org/Pizzahawaii/PizzaSauce" +local MTH_ANIM_CTRL = {} + +local function MTH_ANIM_L(key, default) + if MTH and MTH.GetLocalization then + return MTH:GetLocalization(key, default) + end + return default or key +end + +local function MTH_ANIM_InsertLink(url) + local link = tostring(url or "") + if link == "" then return end + if type(MTH_InsertLinkToChat) == "function" and MTH_InsertLinkToChat(link) then + if MTH and MTH.Print then + MTH:Print(MTH_ANIM_L("ANIM_LINK_INSERTED", "Link inserted in chat: ") .. link) + end + return + end + if MTH and MTH.Print then + MTH:Print(MTH_ANIM_L("ANIM_LINK_FALLBACK", "Link: ") .. link) + end +end + +local function MTH_ANIM_UpdateChildStates() + local master = MTH_FX_AnimationsEnabled() + local i = 1 + while i <= table.getn(MTH_ANIM_CTRL) do + local cb = MTH_ANIM_CTRL[i] + if cb then + cb:SetChecked(MTH_FX_GetFlag(cb.mthKey)) + if master then + cb:Enable() + else + cb:Disable() + end + end + i = i + 1 + end +end + +function MTH_SetupAnimationsOptions() + MTH_ANIM_DBG.setupCalled = true + MTH_ANIM_DBG.fxEnabledType = type(MTH_FX_AnimationsEnabled) + if not MTH_ANIM_READY then + MTH_ANIM_DBG.setupResult = "abort-not-ready" + return + end + if type(MTH_FX_AnimationsEnabled) ~= "function" then + MTH_ANIM_DBG.setupResult = "abort-no-fx-globals" + return + end + + local container = MTH_GetFrame("MetaHuntOptionsAnimations") + MTH_ANIM_DBG.containerType = type(container) + if not container then + MTH_ANIM_DBG.setupResult = "abort-no-container" + return + end + + MTH_ClearContainer(container) + MTH_ANIM_CTRL = {} + + local title = container:CreateFontString("MetaHuntOptionsAnimationsTitle", "ARTWORK", "GameFontNormal") + title:SetPoint("TOPLEFT", container, "TOPLEFT", 10, -10) + title:SetText(MTH_ANIM_L("ANIM_TITLE", "Animations")) + title:SetTextColor(1.00, 0.82, 0.00) + + local credit = container:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + credit:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8) + credit:SetWidth(460) + credit:SetJustifyH("LEFT") + credit:SetText(MTH_ANIM_L("ANIM_CREDIT", + "Animations are powered by PizzaSauce, a WoW 1.12 animation library kindly made and shared by Pizzahawaii. Thank you!")) + credit:SetTextColor(0.93, 0.93, 0.93) + + local link = CreateFrame("Button", nil, container) + link:SetPoint("TOPLEFT", credit, "BOTTOMLEFT", 0, -4) + link:SetHeight(16) + link:SetWidth(460) + local linkText = link:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + linkText:SetPoint("LEFT", link, "LEFT", 0, 0) + linkText:SetJustifyH("LEFT") + linkText:SetText(MTH_ANIM_LINK) + linkText:SetTextColor(0.40, 0.75, 1.00) + link:SetScript("OnClick", function() MTH_ANIM_InsertLink(MTH_ANIM_LINK) end) + link:SetScript("OnEnter", function() linkText:SetTextColor(0.60, 0.90, 1.00) end) + link:SetScript("OnLeave", function() linkText:SetTextColor(0.40, 0.75, 1.00) end) + + -- Global master toggle. + local master = MTH_CreateCheckbox(container, "MetaHuntOptionsAnimMaster", + MTH_ANIM_L("ANIM_ENABLE_ALL", "Enable animations"), -70) + if master then + master:SetChecked(MTH_FX_AnimationsEnabled()) + master:SetScript("OnClick", function() + MTH_FX_SetAnimationsEnabled(master:GetChecked()) + MTH_ANIM_UpdateChildStates() + end) + end + + -- Per-animation checkboxes, grouped by section. + local y = -104 + local rowStep = 26 + local sectionGap = 12 + local s = 1 + while MTH_FX_ANIM_SECTIONS and s <= table.getn(MTH_FX_ANIM_SECTIONS) do + local sec = MTH_FX_ANIM_SECTIONS[s] + local header = container:CreateFontString(nil, "ARTWORK", "GameFontNormal") + header:SetPoint("TOPLEFT", container, "TOPLEFT", 15, y) + header:SetText(MTH_ANIM_L(sec.titleKey, sec.title)) + header:SetTextColor(1.00, 0.82, 0.00) + y = y - 22 + + local d = 1 + while MTH_FX_ANIM_DEFS and d <= table.getn(MTH_FX_ANIM_DEFS) do + local def = MTH_FX_ANIM_DEFS[d] + if def.section == sec.id then + local cbName = "MetaHuntOptionsAnim_" .. string.gsub(def.key, "%.", "_") + local cb = MTH_CreateCheckbox(container, cbName, MTH_ANIM_L(def.labelKey, def.label), y, 28) + if cb then + cb.mthKey = def.key + cb:SetChecked(MTH_FX_GetFlag(def.key)) + cb:SetScript("OnClick", function() + MTH_FX_SetFlag(cb.mthKey, cb:GetChecked()) + end) + if def.tooltip and def.tooltip ~= "" then + cb.mthTooltip = MTH_ANIM_L(def.tooltipKey, def.tooltip) + cb:SetScript("OnEnter", function() + GameTooltip:SetOwner(cb, "ANCHOR_RIGHT") + GameTooltip:SetText(cb.mthTooltip, 1, 1, 1, 1, true) + GameTooltip:Show() + end) + cb:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + table.insert(MTH_ANIM_CTRL, cb) + y = y - rowStep + end + end + d = d + 1 + end + y = y - sectionGap + s = s + 1 + end + + MTH_ANIM_UpdateChildStates() + MTH_ANIM_DBG.setupResult = "ok" +end + +-- DEBUG: mark that the whole file finished loading. TEMP diagnostic. +MTH_ANIM_DBG.step = "2-complete" +MTH_ANIM_DBG.hasSetup = type(MTH_SetupAnimationsOptions) diff --git a/api/options-shell.lua b/api/options-shell.lua index 87bbeb7..1a12831 100644 --- a/api/options-shell.lua +++ b/api/options-shell.lua @@ -20,6 +20,16 @@ local function MTH_OPT_L(key, default) return default or key end +-- Capture our PizzaSauce instance for the options-window open animation (guarded). +local MTH_OPT_Sauce = PizzaSauce +local function MTH_OPT_AnimateOpen(frame) + if not MTH_OPT_Sauce or not frame then return end + if type(MTH_FX_IsEnabled) == "function" and not MTH_FX_IsEnabled("ui.options") then return end + -- Hide alpha this frame so there is no one-frame opaque flash before onStart. + if type(frame.SetAlpha) == "function" then frame:SetAlpha(0) end + MTH_OPT_Sauce:SlideIn(frame, "UP", 60, 0.4, "outCubic") +end + local function MTH_RegisterEscClose(frameName) if not (frameName and UISpecialFrames) then return @@ -232,6 +242,13 @@ function MTH_SelectOptionsTab(tabKey) elseif MTH and MTH.Print then MTH:Print("ICU options setup function is not loaded", "error") end + elseif tabKey == "Animations" then + if not MTH_OPTIONS_SETUP["Animations"] then + if type(MTH_SetupAnimationsOptions) == "function" then + MTH_SetupAnimationsOptions() + MTH_OPTIONS_SETUP["Animations"] = true + end + end elseif tabKey == "Credits" then local setupCredits = _G and _G["MTH_SetupCreditsOptions"] if type(setupCredits) == "function" then @@ -324,6 +341,7 @@ function MTH_OpenOptions(tabKey) return end optionsFrame:Show() + MTH_OPT_AnimateOpen(optionsFrame) if MTH_OPTIONS_UpdateRealmGateOverlay(optionsFrame) then return end MTH_SelectOptionsTab(tabKey or "General") end diff --git a/api/options-tree.lua b/api/options-tree.lua index 1bb92b8..15243da 100644 --- a/api/options-tree.lua +++ b/api/options-tree.lua @@ -52,6 +52,7 @@ MTH_OPTIONS_TABS = MTH_OPTIONS_TABS or { { key = "ChronometerClassSpells", label = "Hunter Spells", frame = "MetaHuntOptionsChronometer" }, { key = "ChronometerClassEvents", label = "Hunter Events", frame = "MetaHuntOptionsChronometer" }, { key = "ChronometerRacial", label = "Racial", frame = "MetaHuntOptionsChronometer" }, + { key = "Animations", label = "Animations", frame = "MetaHuntOptionsAnimations" }, { key = "Credits", label = "Credits", frame = "MetaHuntOptionsCredits" }, } @@ -92,6 +93,7 @@ MTH_OPTIONS_TREE = MTH_OPTIONS_TREE or { { label = "Racial", key = "ChronometerRacial" }, }, }, + { label = "Animations", key = "Animations" }, { label = "Credits", key = "Credits" }, } diff --git a/api/options.xml b/api/options.xml index 041aa9d..e3df266 100644 --- a/api/options.xml +++ b/api/options.xml @@ -85,6 +85,7 @@ + diff --git a/init/api.xml b/init/api.xml index c964985..9387315 100644 --- a/init/api.xml +++ b/init/api.xml @@ -1,7 +1,9 @@ + + @@ -14,6 +16,7 @@ +