diff --git a/CHANGELOG.md b/CHANGELOG.md index fb22307..6881b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ Changes in OEVG/ShaguTweaks-ClassicAPI, newest first. Entries summarize released Based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at `4391564` (2026-09-08). Earlier history is available upstream. +## 3.8.6 — 2026-11-06 — Module fixes and native timers + +- Prevent recursive merchant-click hooks when Buy Em All runs alongside another merchant addon. +- Keep the viewed world map unchanged when hovering the Turtle clock. +- Refresh reagent counts when conditional macros change spells, and apply Minimap Clean's hidden state to late addon buttons. +- Keep the dungeon-finder minimap button's size and position stable when dragging it with Enlarged Minimap enabled. +- Stop Sell Junk's completion check when sales stall, and refresh rarity borders/glows when previously uncached item data arrives. +- Use native ClassicAPI cooldown numbers when native countdowns are enabled, retaining the existing appearance and legacy fallback; remove unused static spell/debuff locale data. +- Use native target cast-bar animation and player countdown text where supported, and limit power/portrait styling updates to affected unit frames. +- Use native totem/control-alert timers where supported, and reduce repeated resource/map styling writes and chat-cache allocations. +- Keep swing-timer and cast-bar preview borders above their fills and match them to the selected UI theme. +- Remove AoE Loot and retire its saved setting. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for AutoAreaLoot. + ## 3.8.5 — 2026-11-06 — UI polish and initialization cleanup - Replace the theme checkboxes with one **UI:** dropdown below Search: Default, Darkened or Golden, preserving existing choices. diff --git a/Docs/EXTRAS_MERGE.md b/Docs/EXTRAS_MERGE.md index ee7a339..3517696 100644 --- a/Docs/EXTRAS_MERGE.md +++ b/Docs/EXTRAS_MERGE.md @@ -6,6 +6,19 @@ Copied from Extras commit `8e14bb4` (2026-09-18). Credits and licenses are in `T ## What to do when updating +From 3.8.6, Cooldown Numbers uses ClassicAPI's native countdown text when +`countdownForCooldowns` is enabled. It preserves ShaguTweaks' colors, rounding +and duration threshold. The addon leaves that global CVar unchanged; when it is +off, or the required native APIs are unavailable, the existing Lua renderer +continues to work. Legacy Model indicators retain their sweep and use a native +child for numbers when supported. + +For custom-module authors: 3.8.6 removes unused `ShaguTweaks.L` data sections +`bagtypes`, `itemtypes`, `hunterpaging`, `critters`, `dyndebuffs`, `judgements`, +`interrupts`, `resurrections`, `spells`, `debuffs` and `totems`. The `class` and +`customcast` sections remain. Custom modules using the removed tables must +supply their own data or use client APIs. + 1. Install this addon (3.7 or newer). 2. Disable or delete the `ShaguTweaks-extras` folder in `Interface\AddOns`. While it stays enabled, its older copies replace the built-in ones, and a message in chat reminds you at login. 3. Your on/off choices carry over, because the modules kept their names. @@ -53,6 +66,7 @@ The textures these modules use (the gryphons and the XP bar) moved to this addon | **Key-Down Casting** and **Alt Self-Cast** | 3.7.1 | ClassicAPI now has the game's own `ActionButtonUseKeyDown` setting. Type `/console ActionButtonUseKeyDown 1` once instead. Details in the [changelog](../CHANGELOG.md). | | **Debuff Timer** | 3.8 | [TargetFrameBuff](https://octowow.st/git/OEVG/TargetFrameBuff) already provides target buff/debuff countdowns with ClassicAPI. Use its timer settings instead of running overlapping timers in ShaguTweaks. | | **Auto Dismount** | 3.8.1 | ClassicAPI provides Auto Dismount, Auto Stand and Auto Cancel Form. | +| **AoE Loot** | 3.8.6 | Removed in favor of a dedicated area-looting addon. Use [AutoAreaLoot](https://github.com/rotaruandrei93/AutoAreaLoot) for this functionality; check its README for required dependencies. The old ShaguTweaks setting is retired automatically. | | **Nameplate Castbar**, **Nameplate Scale** and **Nameplate Class Colors** | 3.8 | Enabling these together with another nameplate addon can cause conflicts because both addons modify the same nameplates. Use one dedicated nameplate addon instead; alternatives are listed below. | ### Nameplate alternatives @@ -61,4 +75,4 @@ Choose one to handle your nameplates: - [BlizzNameplatesPlus](https://github.com/Wurmschwanz/BlizzNameplatesPlus) for a similar Blizzard-like appearance. - [UnitPlates](https://github.com/Redbu11dev/UnitPlates) for more detailed nameplates. -- [ShaguPlates](https://github.com/Dinkleberrrg/ShaguPlates) for extensive customization. \ No newline at end of file +- [ShaguPlates](https://github.com/Dinkleberrrg/ShaguPlates) for extensive customization. diff --git a/README.md b/README.md index 35eed36..c151b00 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ English-only fork of [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/S Focused on **stability, compatibility, customization, and performance** while keeping the original ShaguTweaks experience. -**Version 3.8.5.** What changed in each update: see the [changelog](CHANGELOG.md). +**Version 3.8.6.** What changed in each update: see the [changelog](CHANGELOG.md). > **Upgrading to 3.7:** the modules of the separate *ShaguTweaks Extras ClassicAPI* addon are now built into ShaguTweaks. **Disable or delete `ShaguTweaks-extras`** after updating, otherwise its older copies replace the built-in ones. Your on/off settings carry over. Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy were not merged. See [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md) for where each module went and why some were left out. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for removed modules. @@ -119,7 +119,6 @@ ClassicAPI backs casts, auras, items and unit data: native cooldown support, fil Modules that use ClassicAPI directly: - Range Color -- AoE Loot - Chat Tweaks - Equip Compare - Free Slot Count @@ -236,7 +235,6 @@ Nameplate Scale, Class Colors and Castbar were removed in 3.8 because enabling t ### Loot -- AoE Loot *(off)* - Improved Roll Frames *(off)* - Loot Cursor - Loot History (`/loothistory`) @@ -303,7 +301,7 @@ Additional maintenance and Turtle WoW work by **paokkerkir**. ClassicAPI compatibility fork maintained by **Dusk-92**. -Special thanks to **Sandrea** for providing **AoELoot**, adapted here as the **AoE Loot** module. +The former **AoE Loot** module was removed in 3.8.6. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for the recommended replacement. Special thanks to **Jesper_92** for reporting issues that helped improve the addon. diff --git a/ShaguTweaks.toc b/ShaguTweaks.toc index 275763d..109188a 100644 --- a/ShaguTweaks.toc +++ b/ShaguTweaks.toc @@ -2,7 +2,7 @@ ## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI] ## Author: Shagu ## Notes: A collection of tweaks and enhancements to the Default UI -## Version: 3.8.5 +## Version: 3.8.6 ## Dependencies: !!!ClassicAPI ## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache ## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache @@ -78,7 +78,6 @@ mods\reduced-actionbar.lua mods\actionbar-move.lua mods\actionbar-grid.lua mods\no-toggle.lua -mods\aoe-loot.lua mods\loot-cursor.lua mods\loot-history.lua mods\loss-of-control.lua diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 0192bf1..8843408 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -117,12 +117,13 @@ ClassicAPI binaries or source are distributed as part of this addon. ## AoELoot -`mods/aoe-loot.lua` is adapted from the standalone AoELoot addon provided to -the project by Sandrea: +Before 3.8.6, `mods/aoe-loot.lua` was adapted from the standalone AoELoot +addon provided to the project by Sandrea. The module has since been removed; +this historical attribution remains for earlier releases: - Original addon title: `AoELoot` - Original metadata credit: Sandrea / ChatGPT -- Adapted module: `mods/aoe-loot.lua` +- Former adapted module: `mods/aoe-loot.lua` The supplied package did not include an upstream URL or a separate license file. This notice records the known provenance and preserves attribution; it diff --git a/api.lua b/api.lua index 8d7bc4f..a957457 100644 --- a/api.lua +++ b/api.lua @@ -60,9 +60,6 @@ API.containeritems = type(_G.C_Container) == "table" API.containeriteminfo = type(_G.C_Container) == "table" and type(_G.C_Container.GetContainerItemInfo) == "function" -API.containeropenable = type(_G.C_Container) == "table" - and type(_G.C_Container.IsContainerItemOpenable) == "function" - API.items = type(_G.C_Item) == "table" API.iteminfo = API.items and type(_G.C_Item.GetItemInfo) == "function" API.iteminfoinstant = API.items @@ -145,11 +142,6 @@ API.loothistoryevents = API.loothistory and API.eventutils and _G.C_EventUtils.IsEventValid("LOOT_HISTORY_FULL_UPDATE") API.lootrollitemid = type(_G.GetLootRollItemID) == "function" -API.aoeloot = type(_G.C_Loot) == "table" - and type(_G.C_Loot.GetNearbyLootableUnits) == "function" - and type(_G.C_Loot.IsScanInProgress) == "function" - and type(_G.C_Loot.LootAllCorpses) == "function" - API.overridebindings = type(_G.SetOverrideBindingClick) == "function" and type(_G.ClearOverrideBindings) == "function" @@ -247,6 +239,24 @@ API.GetChannelInfo = function(unit) isTradeSkill, notInterruptible, spellID, true end +-- Native cast durations are snapshots: fetch again after pushback/channel +-- updates. Synthetic/SuperWoW timing uses a separate reusable object, never +-- mutating a duration returned by the engine. +API.GetCastDuration = function(unit, channel, startTime, endTime, fallback) + if not API.durationtimers then return end + local query = channel and _G.UnitChannelDuration or _G.UnitCastingDuration + if type(query) == "function" then + local duration = query(unit) + if duration and math.abs(duration:GetStartTime() - startTime) < .001 + and math.abs(duration:GetEndTime() - endTime) < .001 then + return duration, fallback + end + end + fallback = fallback or _G.C_DurationUtil.CreateDuration() + fallback:SetTimeSpan(startTime, endTime) + return fallback, fallback +end + API.IsSpellInRange = function(spell, unit) if API.spellrange then return _G.C_Spell.IsSpellInRange(spell, unit) @@ -442,30 +452,6 @@ API.GetContainerNumFreeSlots = function(bag) return 0, 0 end -API.IsContainerItemOpenable = function(bag, slot) - if API.containeropenable then - return _G.C_Container.IsContainerItemOpenable(bag, slot) - end -end - -API.GetNearbyLootableUnits = function() - if API.aoeloot then - return _G.C_Loot.GetNearbyLootableUnits() - end -end - -API.IsLootScanInProgress = function() - return API.aoeloot and _G.C_Loot.IsScanInProgress() or false -end - -API.LootAllCorpses = function(maxCount) - if API.aoeloot then - if maxCount then return _G.C_Loot.LootAllCorpses(maxCount) end - return _G.C_Loot.LootAllCorpses() - end - return false -end - -- ClassicAPI updates its own left/right modifier bitmap before firing -- MODIFIER_STATE_CHANGED. Prefer that state over vanilla's merged Win32 key -- state when the richer ClassicAPI functions are available. diff --git a/locales/locales_enUS.lua b/locales/locales_enUS.lua index fa9ccce..58236e3 100644 --- a/locales/locales_enUS.lua +++ b/locales/locales_enUS.lua @@ -13,2420 +13,7 @@ ShaguTweaks_locale["enUS"]["class"] = { ["Rogue"] = "ROGUE", } -ShaguTweaks_locale["enUS"]["bagtypes"] = { - ["Quiver"] = "QUIVER", - ["Soul Bag"] = "SOULBAG", - ["Bag"] = "DEFAULT", -} - -ShaguTweaks_locale["enUS"]["itemtypes"] = { - ["INVTYPE_WAND"] = "Wand", - ["INVTYPE_THROWN"] = "Thrown", - ["INVTYPE_GUN"] = "Gun", - ["INVTYPE_CROSSBOW"] = "Crossbow", - ["INVTYPE_PROJECTILE"] = "Projectile", -} - -ShaguTweaks_locale["enUS"]["hunterpaging"] = { - ["MELEE"] = "Wing Clip", - ["RANGED"] = "Arcane Shot", -} - ShaguTweaks_locale["enUS"]["customcast"] = { ["AIMEDSHOT"] = "Aimed Shot", ["MULTISHOT"] = "Multi-Shot", } - -ShaguTweaks_locale["enUS"]["critters"] = { - 'Adder', - 'Beetle', - 'Belfry Bat', - 'Biletoad', - 'Black Rat', - 'Brown Prairie Dog', - 'Caged Rabbit', - 'Caged Sheep', - 'Caged Squirrel', - 'Caged Toad', - 'Cat', - 'Chicken', - 'Cleo', - 'Core Rat', - 'Cow', - 'Cow', - 'Cured Deer', - 'Cured Gazelle', - 'Deeprun Rat', - 'Deer', - 'Dog', - 'Effsee', - 'Enthralled Deeprun Rat', - 'Fang', - 'Fawn', - 'Fire Beetle', - 'Fluffy', - 'Frog', - 'Gazelle', - 'Hare', - 'Horse', - 'Huge Toad', - 'Infected Deer', - 'Infected Squirrel', - 'Jungle Toad', - 'Krakle\'s Thermometer', - 'Lady', - 'Larva', - 'Lava Crab', - 'Maggot', - 'Moccasin', - 'Mouse', - 'Mr. Bigglesworth', - 'Nibbles', - 'Noarm', - 'Old Blanchy', - 'Parrot', - 'Pig', - 'Pirate treasure trigger mob', - 'Plagued Insect', - 'Plagued Maggot', - 'Plagued Rat', - 'Plagueland Termite', - 'Polymorphed Chicken', - 'Polymorphed Rat', - 'Prairie Dog', - 'Rabbit', - 'Ram', - 'Rat', - 'Riding Ram', - 'Roach', - 'Salome', - 'School of Fish', - 'Scorpion', - 'Sheep', - 'Sheep', - 'Shen\'dralar Wisp', - 'Sickly Deer', - 'Sickly Gazelle', - 'Snake', - 'Spider', - 'Spike', - 'Squirrel', - 'Swine', - 'Tainted Cockroach', - 'Tainted Rat', - 'Toad', - 'Transporter Malfunction', - 'Turtle', - 'Underfoot', - 'Voice of Elune', - 'Waypoint (Only GM can see it)', - 'Wisp', -} - -ShaguTweaks_locale["enUS"]["dyndebuffs"] = { - ["Rupture"] = "Rupture", - ["Kidney Shot"] = "Kidney Shot", - ["Rend"] = "Rend", - ["Shadow Word: Pain"] = "Shadow Word: Pain", - ["Demoralizing Shout"] = "Demoralizing Shout", - ["Frostbolt"] = "Frostbolt", - ["Gouge"] = "Gouge", -} - -ShaguTweaks_locale["enUS"]["judgements"] = { - ["Judgement of Justice"] = true, - ["Judgement of Light"] = true, - ["Judgement of Wisdom"] = true, - ["Judgement of the Crusader"] = true, -} - -ShaguTweaks_locale["enUS"]["interrupts"] = { - ["Shield Bash"] = true, - ["Pummel"] = true, - ["Kick"] = true, - ["Earth Shock"] = true, - ["War Stomp"] = true, - ["Concussion Blow"] = true, - ["Charge Stun"] = true, - ["Intercept Stun"] = true, - ["Hammer of Justice"] = true, - ["Cheap Shot"] = true, - ["Gouge"] = true, - ["Kidney Shot"] = true, - ["Silence"] = true, - ["Counterspell"] = true, - ["Counterspell - Silenced"] = true, - ["Bash"] = true, - ["Fear"] = true, - ["Howl of Terror"] = true, - ["Psychic Scream"] = true, - ["Intimidating Shout"] = true, - ["Starfire Stun"] = true, - ["Revenge Stun"] = true, - ["Improved Concussive Shot"] = true, - ["Impact"] = true, - ["Pyroclasm"] = true, - ["Blackout"] = true, - ["Stun"] = true, - ["Mace Stun Effect"] = true, - ["Earthshaker"] = true, - ["Repentance"] = true, - ["Scatter Shot"] = true, - ["Blind"] = true, - ["Hibernate"] = true, - ["Wyvern Sting"] = true, - ["Rough Copper Bomb"] = true, - ["Large Copper Bomb"] = true, - ["Small Bronze Bomb"] = true, - ["Big Bronze Bomb"] = true, - ["Big Iron Bomb"] = true, - ["Mithril Frag Bomb"] = true, - ["Hi-Explosive Bomb"] = true, - ["Dark Iron Bomb"] = true, - ["Iron Grenade"] = true, - ["M73 Frag Grenade"] = true, - ["Thorium Grenade"] = true, - ["Goblin Mortar"] = true, -} - -ShaguTweaks_locale["enUS"]["resurrections"] = { - ["Resurrection"] = true, - ["Rebirth"] = true, - ["Redemption"] = true, - ["Ancestral Spirit"] = true, -} - -ShaguTweaks_locale["enUS"]["spells"] = { - ['"Plucky" Resumes Chicken Form']={t=1000,icon='Ability_Racial_BearForm'}, - ['Abomination Spit']={t=2500,icon='Spell_Nature_CorrosiveBreath'}, - ['Acid Spit']={t=3000,icon='Spell_Nature_Acid_01'}, - ['Acid Splash']={t=1000,icon='INV_Drink_06'}, - ['Acid Spray']={t=2000,icon='Spell_Nature_Acid_01'}, - ['Acid of Hakkar']={t=1000,icon='Spell_Nature_Acid_01'}, - ['Activating Defenses']={t=5000,icon='Temp'}, - ['Adjust Attitude']={t=2000,icon='INV_Gizmo_01'}, - ['Aimed Shot']={t=3000,icon='INV_Spear_07'}, - ['Amplify Damage']={t=2000,icon='Spell_Nature_AbolishMagic'}, - ['Amplify Flames']={t=1000,icon='Spell_Fire_Fireball'}, - ['Ancestral Spirit']={t=10000,icon='Spell_Nature_Regenerate'}, - ['Anti-Magic Shield']={t=2000,icon='Spell_Shadow_AntiMagicShell'}, - ['Apply Salve']={t=1300,icon='Temp'}, - ['Apply Seduction Gland']={t=2500,icon='INV_Misc_Bowl_01'}, - ['Applying the Lure']={t=4000,icon='Temp'}, - ['Aquadynamic Fish Attractor']={t=5000,icon='INV_Misc_Orb_03'}, - ['Aquadynamic Fish Lens']={t=5000,icon='INV_Misc_Spyglass_01'}, - ['Aqual Quintessence - Dowse Molten Core Rune']={t=1000,icon='Temp'}, - ['Arcane Bolt']={t=1000,icon='Spell_Arcane_StarFire'}, - ['Arcane Bomb']={t=1500,icon='Spell_Holy_Silence'}, - ['Arcane Explosion']={t=1500,icon='Spell_Nature_WispSplode'}, - ['Arcane Spirit II']={t=1000,icon='Spell_Holy_MagicalSentry'}, - ['Arcane Spirit III']={t=1000,icon='Spell_Holy_MagicalSentry'}, - ['Arcane Spirit IV']={t=1000,icon='Spell_Holy_MagicalSentry'}, - ['Arcane Spirit V']={t=1000,icon='Spell_Holy_MagicalSentry'}, - ['Arcane Weakness']={t=5000,icon='INV_Misc_QirajiCrystal_01'}, - ['Arcanite Skeleton Key']={t=5000,icon='Temp'}, - ['Archaedas Awaken Visual (DND)']={t=1500,icon='Spell_Nature_Earthquake'}, - ['Arctic Wolf']={t=3000,icon='Ability_Mount_WhiteDireWolf'}, - ['Area Burn']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Arugal spawn-in spell']={t=2000,icon='Temp'}, - ['Arugal\'s Gift']={t=2500,icon='Spell_Shadow_ChillTouch'}, - ['Arygos\'s Vengeance']={t=2000,icon='Temp'}, - ['Ashcrombe\'s Teleport']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Ashcrombe\'s Unlock']={t=4000,icon='Spell_Nature_MoonKey'}, - ['Aspect of Neptulon']={t=1000,icon='Temp'}, - ['Astral Recall']={t=10000,icon='Spell_Nature_AstralRecal'}, - ['Atal\'ai Altar Light Visual (DND)']={t=1000,icon='Temp'}, - ['Attuned Dampener']={t=2000,icon='Spell_Holy_SearingLight'}, - ['Awaken Kerlonian']={t=4500,icon='Temp'}, - ['Awaken Vault Warder']={t=5000,icon='Spell_Nature_Earthquake'}, - ['Awaken the Soulflayer']={t=5000,icon='Temp'}, - ['Aynasha\'s Arrow']={t=500,icon='Temp'}, - ['Backhand']={t=1000,icon='Spell_Shadow_LifeDrain'}, - ['Balance of Nature']={t=3000,icon='Temp'}, - ['Balance of Nature Failure']={t=10000,icon='Temp'}, - ['Ball Lightning']={t=1000,icon='Spell_Lightning_LightningBolt01'}, - ['Banish']={t=1500,icon='Spell_Shadow_Cripple'}, - ['Banish Burning Exile']={t=1000,icon='Spell_Shadow_LifeDrain'}, - ['Banish Cresting Exile']={t=1000,icon='Spell_Shadow_LifeDrain'}, - ['Banish Thundering Exile']={t=1000,icon='Spell_Shadow_LifeDrain'}, - ['Banshee Curse']={t=2000,icon='Spell_Nature_Drowsy'}, - ['Banshee Wail']={t=1500,icon='Spell_Shadow_ShadowBolt'}, - ['Barrier of Light']={t=2000,icon='Temp'}, - ['Basic Campfire']={t=10000,icon='Spell_Fire_Fire'}, - ['Beast Claws']={t=1000,icon='Spell_Nature_Regeneration'}, - ['Beast Claws II']={t=1000,icon='Spell_Nature_Regeneration'}, - ['Beast Claws III']={t=1000,icon='Spell_Nature_Regeneration'}, - ['Bellowing Roar']={t=1500,icon='Spell_Fire_Fire'}, - ['Bending Shinbone']={t=1500,icon='Temp'}, - ['Big Bronze Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Big Iron Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Birth']={t=2000,icon='Temp'}, - ['Black Arrow']={t=2000,icon='Ability_TheBlackArrow'}, - ['Black Battlestrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Black Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Black Sludge']={t=3000,icon='Spell_Shadow_CallofBone'}, - ['Black Stallion']={t=3000,icon='Ability_Mount_NightmareHorse'}, - ['Black War Kodo']={t=3000,icon='Ability_Mount_Kodo_01'}, - ['Black War Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Black War Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Black War Steed']={t=3000,icon='Ability_Mount_NightmareHorse'}, - ['Black War Tiger']={t=3000,icon='Ability_Mount_BlackPanther'}, - ['Black War Wolf']={t=3000,icon='Ability_Mount_BlackDireWolf'}, - ['Black Wolf']={t=3000,icon='Ability_Mount_BlackDireWolf'}, - ['Blessed Wizard Oil']={t=3000,icon='Temp'}, - ['Blessing of Shahram']={t=1000,icon='Spell_Holy_LayOnHands'}, - ['Blizzard']={t=2000,icon='Spell_Frost_IceStorm'}, - ['Blood Howl']={t=1000,icon='Spell_Shadow_LifeDrain'}, - ['Blue Dragon Transform DND']={t=1000,icon='Temp'}, - ['Blue Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Blue Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Blue Skeletal Horse']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Bly\'s Band\'s Escape']={t=10000,icon='INV_Misc_Rune_01'}, - ['Bomb']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Bombard']={t=3000,icon='Ability_GolemStormBolt'}, - ['Bombard II']={t=3000,icon='Ability_GolemStormBolt'}, - ['Bombard Slime']={t=1000,icon='Temp'}, - ['Bone Shards']={t=500,icon='Spell_Shadow_ScourgeBuild'}, - ['Boulder']={t=2000,icon='Ability_Throw'}, - ['Break Big Stuff']={t=2000,icon='Spell_Shadow_CurseOfAchimonde'}, - ['Break Stuff']={t=2000,icon='Spell_Shadow_CurseOfAchimonde'}, - ['Breath']={t=5000,icon='Spell_Fire_Fire'}, - ['Breath of Sargeras']={t=2000,icon='Spell_Shadow_Metamorphosis'}, - ['Bright Baubles']={t=5000,icon='INV_Misc_Orb_03'}, - ['Bright Campfire']={t=10000,icon='Spell_Fire_Fire'}, - ['Brilliant Mana Oil']={t=3000,icon='Temp'}, - ['Brilliant Wizard Oil']={t=3000,icon='Temp'}, - ['Brood of Nozdormu Factoin +1000']={t=1000,icon='Temp'}, - ['Brown Horse']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Brown Kodo']={t=3000,icon='Ability_Mount_Kodo_03'}, - ['Brown Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Brown Skeletal Horse']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Brown Wolf']={t=3000,icon='Ability_Mount_BlackDireWolf'}, - ['Burning Winds']={t=1000,icon='Spell_Nature_Cyclone'}, - ['Burrow']={t=1000,icon='Ability_Vanish'}, - ['Bury Samuel\'s Remains']={t=2000,icon='Temp'}, - ['Buttermilk Delight']={t=1000,icon='INV_ValentinesChocolate01'}, - ['CHU\'s QUEST SPELL']={t=4000,icon='Spell_Shadow_LifeDrain'}, - ['Call Anathema']={t=1000,icon='Temp'}, - ['Call Ancients']={t=7000,icon='Temp'}, - ['Call Benediction']={t=1000,icon='Temp'}, - ['Call Bleak Worg']={t=1300,icon='Spell_Shadow_ChillTouch'}, - ['Call Glyphs of Warding']={t=3000,icon='Temp'}, - ['Call Lupine Horror']={t=1300,icon='Spell_Shadow_ChillTouch'}, - ['Call Prismatic Barrier']={t=10000,icon='Temp'}, - ['Call Slavering Worg']={t=1300,icon='Spell_Shadow_ChillTouch'}, - ['Call of Thund']={t=1500,icon='Spell_Frost_Wisp'}, - ['Call of the Grave']={t=2000,icon='Spell_Shadow_ChillTouch'}, - ['Call of the Nether']={t=10000,icon='Temp'}, - ['Call of the Void']={t=3000,icon='Spell_Shadow_DeathCoil'}, - ['Call to Ivus']={t=10000,icon='Temp'}, - ['Cannon Fire']={t=1000,icon='Spell_Fire_FireBolt02'}, - ['Cantation of Manifestation']={t=2000,icon='Spell_Magic_LesserInvisibilty'}, - ['Capture Grark']={t=3000,icon='Temp'}, - ['Capture Worg Pup']={t=2500,icon='Temp'}, - ['Capturing Termites']={t=5000,icon='Temp'}, - ['Cascade of Roses']={t=500,icon='INV_Misc_Dust_04'}, - ['Chain Bolt']={t=2500,icon='Spell_Nature_ChainLightning'}, - ['Chain Burn']={t=3000,icon='Spell_Shadow_ManaBurn'}, - ['Chain Heal']={t=2500,icon='Spell_Nature_HealingWaveGreater'}, - ['Chain Lightning']={t=2500,icon='Spell_Nature_ChainLightning'}, - ['Chained Bolt']={t=1800,icon='Spell_Nature_ChainLightning'}, - ['Chains of Ice']={t=1300,icon='Spell_Frost_ChainsOfIce'}, - ['Charged Arcane Bolt']={t=7000,icon='Spell_Arcane_StarFire'}, - ['Charging']={t=5000,icon='Spell_Shadow_EvilEye'}, - ['Chestnut Mare']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Chilling Breath']={t=1000,icon='Spell_Frost_Wisp'}, - ['Chromatic Mount']={t=3000,icon='INV_Misc_Head_Dragon_Black'}, - ['Clean Up Stink Bomb']={t=5000,icon='Temp'}, - ['Cleanse Thunderhorn Well']={t=10000,icon='Temp'}, - ['Cleanse Wildmane Well']={t=10000,icon='Temp'}, - ['Cleanse Winterhoof Well']={t=10000,icon='Temp'}, - ['Cleave']={t=2500,icon='Ability_Warrior_Cleave'}, - ['Clone']={t=2500,icon='Spell_Shadow_BlackPlague'}, - ['Closing']={t=1000,icon='Temp'}, - ['Coarse Dynamite']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Collect Rookery Egg']={t=500,icon='Temp'}, - ['Collecting Fallout']={t=4000,icon='Temp'}, - ['Colossal Smash']={t=5000,icon='Temp'}, - ['Conjure Altar of Summoning']={t=10000,icon='Temp'}, - ['Conjure Circle of Calling']={t=10000,icon='Temp'}, - ['Conjure Dream Rift']={t=10000,icon='Temp'}, - ['Conjure Furis Felsteed DUMMY DND']={t=5000,icon='Temp'}, - ['Consecrated Weapon']={t=3000,icon='Temp'}, - ['Cookie\'s Cooking']={t=2000,icon='Spell_Holy_Heal'}, - ['Corrosive Acid']={t=1500,icon='Spell_Nature_Acid_01'}, - ['Corrosive Acid Spit']={t=3000,icon='Spell_Nature_Acid_01'}, - ['Corrosive Poison']={t=1500,icon='Spell_Nature_CorrosiveBreath'}, - ['Corrosive Venom Spit']={t=2500,icon='Spell_Nature_CorrosiveBreath'}, - ['Corrupt Redpath']={t=2000,icon='Temp'}, - ['Corruption']={t=2000,icon='Spell_Shadow_AbominationExplosion'}, - ['Crash of Waves']={t=2000,icon='Spell_Frost_FrostNova'}, - ['Create Cleansing Totem']={t=5000,icon='Spell_Shadow_LifeDrain'}, - ['Create Cluster Rocket Launcher']={t=2000,icon='INV_Misc_EngGizmos_03'}, - ['Create Containment Coffer']={t=500,icon='Temp'}, - ['Create Filled Containment Coffer']={t=2000,icon='Temp'}, - ['Create Firework Rocket Launcher']={t=2000,icon='INV_Musket_04'}, - ['Create Healthstone']={t=3000,icon='INV_Stone_04'}, - ['Create Healthstone (Greater)']={t=3000,icon='INV_Stone_04'}, - ['Create Healthstone (Lesser)']={t=3000,icon='INV_Stone_04'}, - ['Create Healthstone (Major)']={t=3000,icon='INV_Stone_04'}, - ['Create Healthstone (Minor)']={t=3000,icon='INV_Stone_04'}, - ['Create Item Visual (DND)']={t=5000,icon='Spell_Shadow_SoulGem'}, - ['Create Mage\'s Orb']={t=4000,icon='Temp'}, - ['Create Mage\'s Robe']={t=4000,icon='Temp'}, - ['Create PX83-Enigmatron']={t=2000,icon='INV_Misc_Bowl_01'}, - ['Create Relic Bundle']={t=1000,icon='Temp'}, - ['Create Rift']={t=3000,icon='Temp'}, - ['Create Sapta']={t=3000,icon='INV_Misc_Food_09'}, - ['Create Scroll']={t=5000,icon='INV_Scroll_05'}, - ['Create Scrying Bowl']={t=2500,icon='INV_Misc_Bowl_01'}, - ['Create Shredder']={t=1000,icon='INV_Misc_Gear_01'}, - ['Create Water of the Seers']={t=5000,icon='Spell_Shadow_LifeDrain'}, - ['Create Witherbark Totem Bundle']={t=2000,icon='Spell_Lightning_LightningBolt01'}, - ['CreatureSpecial']={t=2000,icon='Temp'}, - ['Creeper Venom']={t=2000,icon='Spell_Nature_NullifyPoison'}, - ['Creeping Mold']={t=3000,icon='Spell_Shadow_CreepingPlague'}, - ['Crest of Retribution']={t=1000,icon='INV_Shield_19'}, - ['Cripple']={t=3000,icon='Spell_Shadow_Cripple'}, - ['Crippling Poison']={t=3000,icon='Ability_PoisonSting'}, - ['Crypt Scarabs']={t=1500,icon='Spell_Shadow_CarrionSwarm'}, - ['Crystal Flash']={t=2000,icon='Spell_Shadow_Teleport'}, - ['Crystal Gaze']={t=2000,icon='Ability_GolemThunderClap'}, - ['Crystalline Slumber']={t=2000,icon='Spell_Nature_Sleep'}, - ['Cupid\'s Arrow']={t=1000,icon='Temp'}, - ['Cure Disease']={t=2500,icon='Spell_Holy_NullifyDisease'}, - ['Curse of Blood']={t=2000,icon='Spell_Shadow_RitualOfSacrifice'}, - ['Curse of Hakkar']={t=2500,icon='Spell_Shadow_GatherShadows'}, - ['Curse of Mending']={t=1000,icon='Spell_Shadow_AntiShadow'}, - ['Curse of Shahram']={t=1000,icon='Spell_Magic_LesserInvisibilty'}, - ['Curse of Stalvan']={t=1000,icon='Spell_Shadow_ShadowPact'}, - ['Curse of Thorns']={t=2000,icon='Spell_Shadow_AntiShadow'}, - ['Curse of Thule']={t=2000,icon='Spell_Shadow_ShadowPact'}, - ['Curse of Timmy']={t=1000,icon='Spell_Shadow_ShadowPact'}, - ['Curse of Weakness']={t=1000,icon='Spell_Shadow_CurseOfMannoroth'}, - ['Curse of the Darkmaster']={t=2000,icon='Spell_Shadow_AntiShadow'}, - ['Curse of the Deadwood']={t=2000,icon='Spell_Shadow_GatherShadows'}, - ['Curse of the Fallen Magram']={t=2000,icon='Spell_Shadow_UnholyFrenzy'}, - ['Curse of the Firebrand']={t=2000,icon='Ability_Creature_Cursed_03'}, - ['Curse of the Plague Rat']={t=1500,icon='Spell_Shadow_UnholyFrenzy'}, - ['Curse of the Tribes']={t=2000,icon='Spell_Shadow_CurseOfMannoroth'}, - ['Dalaran Wizard Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Damage Car']={t=2000,icon='Spell_Fire_Fire'}, - ['Dark Desire']={t=1000,icon='INV_ValentinesChocolate04'}, - ['Dark Iron Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Dark Iron Dwarf Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Dark Iron Land Mine']={t=1000,icon='Spell_Shadow_Metamorphosis'}, - ['Dark Mending']={t=3500,icon='Spell_Shadow_ChillTouch'}, - ['Dark Restore']={t=2000,icon='Ability_Hunter_MendPet'}, - ['Dark Sludge']={t=5000,icon='Spell_Shadow_CreepingPlague'}, - ['Dark Whispers']={t=3000,icon='Spell_Shadow_Haunting'}, - ['Darken Vision']={t=2000,icon='Spell_Shadow_Fumble'}, - ['Daunting Growl']={t=1000,icon='Ability_Racial_Cannibalize'}, - ['Dawn\'s Gambit']={t=1500,icon='Temp'}, - ['Deadly Poison']={t=3000,icon='Ability_Rogue_DualWeild'}, - ['Deadly Poison II']={t=3000,icon='Ability_Rogue_DualWeild'}, - ['Deadly Poison III']={t=3000,icon='Ability_Rogue_DualWeild'}, - ['Deadly Poison IV']={t=3000,icon='Ability_Rogue_DualWeild'}, - ['Deadly Poison V']={t=3000,icon='Ability_Rogue_DualWeild'}, - ['Deadmines Dynamite']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Death & Decay']={t=2000,icon='Spell_Shadow_DeathAndDecay'}, - ['Death Bed']={t=2000,icon='Spell_Shadow_Twilight'}, - ['Deathcharger']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Decayed Agility']={t=2000,icon='Spell_Holy_HarmUndeadAura'}, - ['Decayed Strength']={t=2000,icon='Spell_Holy_HarmUndeadAura'}, - ['Deep Slumber']={t=1000,icon='Spell_Shadow_Cripple'}, - ['Defias Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Defibrillate']={t=4000,icon='Spell_Nature_Purge'}, - ['Delete Me']={t=4000,icon='INV_Scroll_02'}, - ['Demon Pick']={t=5000,icon='Temp'}, - ['Demon Portal']={t=500,icon='Spell_Arcane_TeleportOrgrimmar'}, - ['Demon Summoning Torch']={t=3000,icon='Temp'}, - ['Dense Dynamite']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Destroy Egg']={t=3000,icon='INV_Misc_MonsterClaw_02'}, - ['Destroy Ghost Magnet']={t=10000,icon='Temp'}, - ['Destroy Tent']={t=2500,icon='Temp'}, - ['Detonate']={t=2000,icon='Temp'}, - ['Detonation']={t=5000,icon='Spell_Fire_SelfDestruct'}, - ['Digging for Cobalt']={t=1500,icon='Temp'}, - ['Dimensional Portal']={t=2000,icon='Temp'}, - ['Dire Growl']={t=1000,icon='Ability_Racial_Cannibalize'}, - ['Dire Wolf']={t=3000,icon='Ability_Mount_WhiteDireWolf'}, - ['Disarm Trap']={t=2000,icon='Spell_Shadow_GrimWard'}, - ['Disease Buffet']={t=1500,icon='Spell_Nature_EarthBind'}, - ['Diseased Shot']={t=2000,icon='Spell_Shadow_CallofBone'}, - ['Diseased Slime']={t=2000,icon='Spell_Shadow_CreepingPlague'}, - ['Diseased Spit']={t=3000,icon='Spell_Shadow_CreepingPlague'}, - ['Disenchant']={t=3000,icon='Spell_Holy_RemoveCurse'}, - ['Dismiss Pet']={t=5000,icon='Spell_Nature_SpiritWolf'}, - ['Dispel']={t=1000,icon='Spell_Holy_DispelMagic'}, - ['Dispel Poison']={t=2000,icon='Spell_Holy_Purify'}, - ['Displacing Temporal Rift']={t=5000,icon='Temp'}, - ['Disruption']={t=3000,icon='Temp'}, - ['Disturb Rookery Egg']={t=1000,icon='Temp'}, - ['Divining Trance']={t=5000,icon='Temp'}, - ['Dominate Mind']={t=2000,icon='Spell_Shadow_ShadowWordDominate'}, - ['Domination']={t=1000,icon='Spell_Shadow_ShadowWordDominate'}, - ['Dominion of Soul']={t=3000,icon='Spell_Shadow_ShadowWordDominate'}, - ['Dousing']={t=5000,icon='Temp'}, - ['Dowse Eternal Flame']={t=1000,icon='Temp'}, - ['Draco-Incarcinatrix 900']={t=2000,icon='Temp'}, - ['Draw Ancient Glyphs']={t=10000,icon='Temp'}, - ['Draw of Thistlenettle']={t=2000,icon='Spell_Shadow_Haunting'}, - ['Drawing Kit']={t=7000,icon='Temp'}, - ['Drink Minor Potion']={t=3000,icon='Spell_Holy_Heal'}, - ['Drink Potion']={t=3000,icon='Spell_Holy_Heal'}, - ['Drive Nimboya\'s Laden Pike']={t=2000,icon='Temp'}, - ['Druid\'s Slumber']={t=2500,icon='Spell_Nature_Sleep'}, - ['Drunken Pit Crew']={t=2000,icon='Temp'}, - ['Dummy NPC Summon']={t=20000,icon='Spell_Shadow_UnsummonBuilding'}, - ['Dummy Nuke']={t=2000,icon='Temp'}, - ['Dust Cloud']={t=1500,icon='Ability_Hibernation'}, - ['Dynamite']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Earth Elemental']={t=3000,icon='Ability_Temp'}, - ['Earthgrab Totem']={t=500,icon='Spell_Nature_NatureTouchDecay'}, - ['Editor Test Spell']={t=250,icon='Temp'}, - ['Elderberry Pie']={t=1000,icon='INV_Misc_Food_10'}, - ['Electrified Net']={t=500,icon='Ability_Ensnare'}, - ['Elemental Armor']={t=1000,icon='Spell_Frost_Frost'}, - ['Elemental Fire']={t=500,icon='Spell_Fire_Fireball02'}, - ['Elemental Fury']={t=1000,icon='Spell_Fire_FireArmor'}, - ['Elune\'s Candle']={t=500,icon='Temp'}, - ['Emberseer Start']={t=5000,icon='Spell_Nature_EarthBindTotem'}, - ['Emerald Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Empower Pet']={t=500,icon='Spell_Shadow_DeathPact'}, - ['Empty Festive Mug']={t=2000,icon='Temp'}, - ['Encage']={t=2000,icon='Spell_Shadow_Teleport'}, - ['Encasing Webs']={t=2000,icon='Spell_Nature_EarthBind'}, - ['Enchant Annals of Darrowshire']={t=3500,icon='Temp'}, - ['Enchanted Gaea Seed']={t=5000,icon='Temp'}, - ['Enchanted Quickness']={t=3000,icon='Spell_Nature_Invisibilty'}, - ['Enchanted Resonite Crystal']={t=5000,icon='Temp'}, - ['Enchanting Lullaby']={t=1000,icon='Spell_Shadow_SoothingKiss'}, - ['Energy Siphon']={t=1500,icon='Spell_Shadow_Cripple'}, - ['Enervate']={t=1500,icon='Ability_Creature_Poison_03'}, - ['Enfeeble']={t=2000,icon='Spell_Shadow_CurseOfMannoroth'}, - ['Engulfing Shadows']={t=1500,icon='Spell_Shadow_LifeDrain02'}, - ['Enhance Blunt Weapon']={t=3000,icon='Temp'}, - ['Enhance Blunt Weapon II']={t=3000,icon='Temp'}, - ['Enhance Blunt Weapon III']={t=3000,icon='Temp'}, - ['Enhance Blunt Weapon IV']={t=3000,icon='Temp'}, - ['Enhance Blunt Weapon V']={t=3000,icon='Temp'}, - ['Enlarge']={t=2000,icon='Spell_Nature_Strength'}, - ['Enlightenment']={t=2000,icon='Spell_Shadow_Fumble'}, - ['Enslave Demon']={t=3000,icon='Spell_Shadow_EnslaveDemon'}, - ['Entangling Roots']={t=1500,icon='Spell_Nature_StrangleVines'}, - ['Enveloping Winds']={t=2000,icon='Spell_Nature_Cyclone'}, - ['Escape Artist']={t=500,icon='Ability_Rogue_Trip'}, - ['Everlook Transporter']={t=10000,icon='Spell_Fire_SelfDestruct'}, - ['Evil Eye']={t=1500,icon='Spell_Shadow_Charm'}, - ['Evil God Counterspell']={t=300000,icon='Temp'}, - ['Exorcise Atiesh']={t=20000,icon='Temp'}, - ['Exorcise Spirits']={t=4000,icon='Temp'}, - ['Exploding Shot']={t=1000,icon='Spell_Fire_Fireball02'}, - ['Explosive Sheep']={t=2000,icon='Ability_Repair'}, - ['Explosive Shot']={t=1000,icon='Spell_Fire_Fireball02'}, - ['Extinguish']={t=2000,icon='Temp'}, - ['Eye Beam']={t=2000,icon='Spell_Nature_CallStorm'}, - ['Eye of Immol\'thar']={t=2000,icon='Spell_Shadow_AntiMagicShell'}, - ['Eye of Kilrogg']={t=5000,icon='Spell_Shadow_EvilEye'}, - ['Eye of Yesmur (PT)']={t=2000,icon='Temp'}, - ['Eyes of the Beast']={t=2000,icon='Ability_EyeOfTheOwl'}, - ['Ez-Thro Dynamite']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Faerie Fire']={t=2000,icon='Spell_Nature_FaerieFire'}, - ['Fake Shot']={t=2000,icon='Ability_Marksmanship'}, - ['Fanatic Blade']={t=1000,icon='Spell_Fire_Immolation'}, - ['Far Sight']={t=2000,icon='Spell_Nature_FarSight'}, - ['Far Sight (PT)']={t=2000,icon='Temp'}, - ['Fear']={t=1500,icon='Spell_Shadow_Possession'}, - ['Fear (NYI)']={t=3000,icon='Spell_Shadow_Possession'}, - ['Feeblemind']={t=1000,icon='Spell_Shadow_MindSteal'}, - ['Feeblemind II']={t=1000,icon='Spell_Shadow_MindSteal'}, - ['Feeblemind III']={t=1000,icon='Spell_Shadow_MindSteal'}, - ['Fel Curse']={t=4000,icon='Temp'}, - ['Felstrom Resurrection']={t=3000,icon='Spell_Totem_WardOfDraining'}, - ['Feral Spirit']={t=3000,icon='Spell_Nature_SpiritWolf'}, - ['Feral Spirit II']={t=3000,icon='Spell_Nature_SpiritWolf'}, - ['Fevered Fatigue']={t=3000,icon='Spell_Nature_NullifyDisease'}, - ['Fevered Plague']={t=4500,icon='Spell_Nature_NullifyDisease'}, - ['Fiery Blaze']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Fiery Burst']={t=1500,icon='Spell_Fire_FireBolt'}, - ['Fill Phial']={t=5000,icon='Temp'}, - ['Filling']={t=3000,icon='Temp'}, - ['Find Relic Fragment']={t=5000,icon='Temp'}, - ['Fire Cannon']={t=2000,icon='Temp'}, - ['Fire Elemental']={t=3000,icon='Ability_Temp'}, - ['Fire Shield']={t=1000,icon='Spell_Fire_Immolation'}, - ['Fire Shield II']={t=1000,icon='Spell_Fire_Immolation'}, - ['Fire Shield III']={t=1000,icon='Spell_Fire_Immolation'}, - ['Fire Shield IV']={t=1000,icon='Spell_Fire_Immolation'}, - ['Fire Storm']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Fire Weakness']={t=5000,icon='INV_Misc_QirajiCrystal_02'}, - ['Fire-toasted Bun']={t=1000,icon='INV_Misc_Food_11'}, - ['Fireball']={t=3500,icon='Spell_Fire_FlameBolt'}, - ['Fireball Volley']={t=3000,icon='Spell_Fire_FlameBolt'}, - ['Firebolt']={t=2000,icon='Spell_Fire_FireBolt'}, - ['Firebolt II']={t=3000,icon='Spell_Fire_FireBolt02'}, - ['Firebolt III']={t=3000,icon='Spell_Fire_FireBolt02'}, - ['Firebolt IV']={t=3000,icon='Spell_Fire_FireBolt02'}, - ['Firework']={t=500,icon='Temp'}, - ['First Aid']={t=3000,icon='Spell_Holy_GreaterHeal'}, - ['Fist of Shahram']={t=1000,icon='Ability_Whirlwind'}, - ['Fix Ritual Bell (DND)']={t=3000,icon='Temp'}, - ['Fix Ritual Candle (DND)']={t=3000,icon='Temp'}, - ['Fix Ritual Node']={t=3000,icon='Temp'}, - ['Flame Blast']={t=7000,icon='Spell_Fire_SelfDestruct'}, - ['Flame Breath']={t=1700,icon='Spell_Fire_Fire'}, - ['Flame Buffet']={t=2200,icon='Spell_Fire_Fireball'}, - ['Flame Burst']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Flame Cannon']={t=1500,icon='Spell_Fire_FlameBolt'}, - ['Flame Lash']={t=1000,icon='Spell_Fire_Fireball'}, - ['Flame Spike']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Flame Spray']={t=1700,icon='Spell_Fire_Fire'}, - ['Flamecrack']={t=2500,icon='Spell_Fire_Fire'}, - ['Flames of Chaos']={t=1000,icon='Spell_Fire_WindsofWoe'}, - ['Flames of Retribution']={t=3000,icon='Temp'}, - ['Flames of Shahram']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Flamespit']={t=3000,icon='Spell_Fire_FlameBolt'}, - ['Flamestrike']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Flash Heal']={t=1500,icon='Spell_Holy_FlashHeal'}, - ['Flash of Light']={t=1500,icon='Spell_Holy_FlashHeal'}, - ['Flesh Eating Worm']={t=5000,icon='INV_Misc_Orb_03'}, - ['Fling Torch']={t=1000,icon='Spell_Fire_Flare'}, - ['Fluorescent Green Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Focusing']={t=5000,icon='Temp'}, - ['Force Punch']={t=1000,icon='INV_Gauntlets_31'}, - ['Forge Verigan\'s Fist']={t=600,icon='Spell_Holy_RighteousFury'}, - ['Forgiveness']={t=1000,icon='Temp'}, - ['Forked Lightning']={t=2000,icon='Spell_Nature_ChainLightning'}, - ['Form of the Moonstalker (no invis)']={t=1000,icon='Ability_Hibernation'}, - ['Forsaken Skills']={t=2500,icon='Spell_Shadow_AntiShadow'}, - ['Freeze']={t=4000,icon='Spell_Frost_Glacier'}, - ['Freeze Rookery Egg']={t=500,icon='Temp'}, - ['Freeze Rookery Egg - Prototype']={t=500,icon='Temp'}, - ['Freeze Solid']={t=2500,icon='Spell_Frost_Glacier'}, - ['Frightalon']={t=1000,icon='Spell_Shadow_ShadowPact'}, - ['Frost Breath']={t=250,icon='Spell_Frost_FrostNova'}, - ['Frost Burn']={t=2000,icon='Spell_Frost_ChillingBlast'}, - ['Frost Oil']={t=3000,icon='Temp'}, - ['Frost Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Frost Resistance']={t=1000,icon='Spell_Frost_WizardMark'}, - ['Frost Weakness']={t=5000,icon='INV_Misc_QirajiCrystal_04'}, - ['Frostbolt']={t=3000,icon='Spell_Frost_FrostBolt02'}, - ['Frostbolt Volley']={t=2000,icon='Spell_Frost_FrostBolt02'}, - ['Frostmane Strength']={t=1000,icon='Spell_Nature_UndyingStrength'}, - ['Frostsaber']={t=3000,icon='Ability_Mount_WhiteTiger'}, - ['Frostwolf Howler']={t=3000,icon='Ability_Mount_WhiteDireWolf'}, - ['Full Heal']={t=1000,icon='Temp'}, - ['Fumble']={t=1000,icon='Spell_Shadow_Fumble'}, - ['Fumble II']={t=1000,icon='Spell_Shadow_Fumble'}, - ['Fumble III']={t=1000,icon='Spell_Shadow_Fumble'}, - ['Furbolg Form']={t=2000,icon='INV_Misc_MonsterClaw_04'}, - ['Gargoyle Strike']={t=1500,icon='Spell_Shadow_ShadowBolt'}, - ['Gas Bomb']={t=1000,icon='INV_Misc_Ammo_Bullet_01'}, - ['Gem of the Serpent']={t=5000,icon='Temp'}, - ['Ghost Wolf']={t=3000,icon='Spell_Nature_SpiritWolf'}, - ['Gift of the Xavian']={t=5000,icon='Spell_Holy_FlashHeal'}, - ['Glacial Roar']={t=1000,icon='Spell_Frost_FrostNova'}, - ['Glare']={t=500,icon='Temp'}, - ['Gnome Camera Connection']={t=3000,icon='Temp'}, - ['Gnomish Transporter']={t=10000,icon='Temp'}, - ['Goblin Camera Connection']={t=3000,icon='Temp'}, - ['Goblin Land Mine']={t=1000,icon='Spell_Shadow_Metamorphosis'}, - ['Goblin Mortar']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Golden Sabercat']={t=3000,icon='Ability_Mount_JungleTiger'}, - ['Golden Skeleton Key']={t=5000,icon='Temp'}, - ['Goldshire Portal']={t=5000,icon='Temp'}, - ['Gong']={t=500,icon='Temp'}, - ['Gong Zul\'Farrak Gong']={t=500,icon='Temp'}, - ['Gordok Green Grog']={t=1000,icon='INV_Drink_03'}, - ['Grasping Vines']={t=1000,icon='Spell_Nature_Earthquake'}, - ['Gray Kodo']={t=3000,icon='Ability_Mount_Kodo_01'}, - ['Gray Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Gray Wolf']={t=3000,icon='Ability_Mount_WhiteDireWolf'}, - ['Great Brown Kodo']={t=3000,icon='Ability_Mount_Kodo_03'}, - ['Great Gray Kodo']={t=3000,icon='Ability_Mount_Kodo_01'}, - ['Great Heal']={t=4000,icon='Spell_Holy_Heal'}, - ['Great White Kodo']={t=3000,icon='Ability_Mount_Kodo_01'}, - ['Greater Dispel']={t=4000,icon='Spell_Arcane_StarFire'}, - ['Greater Heal']={t=3000,icon='Spell_Holy_GreaterHeal'}, - ['Greater Invisibility']={t=3000,icon='Spell_Nature_Invisibilty'}, - ['Green Dragon Transform DND']={t=1000,icon='Temp'}, - ['Green Kodo']={t=3000,icon='Ability_Mount_Kodo_02'}, - ['Green Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Green Skeletal Warhorse']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Grom\'s Tribute']={t=2000,icon='Temp'}, - ['Guile of the Raptor']={t=3000,icon='INV_Misc_MonsterClaw_02'}, - ['Gust of Wind']={t=2000,icon='Spell_Nature_EarthBind'}, - ['Hammer of Wrath']={t=1000,icon='Ability_ThunderClap'}, - ['Hand of Iruxos']={t=5000,icon='Temp'}, - ['Harvest Silithid Egg']={t=5000,icon='Temp'}, - ['Harvest Swarm']={t=3000,icon='Spell_Holy_Dizzy'}, - ['Haunting Phantoms']={t=2000,icon='Spell_Shadow_BlackPlague'}, - ['Haunting Spirits']={t=2000,icon='Spell_Shadow_BlackPlague'}, - ['Heal']={t=3000,icon='Spell_Holy_Heal02'}, - ['Heal Visual (DND)']={t=3500,icon='Spell_Holy_Heal'}, - ['Heal Vylestem Vine']={t=500,icon='Temp'}, - ['Healing Circle']={t=3000,icon='Spell_Holy_BlessingOfProtection'}, - ['Healing Tongue']={t=1000,icon='Spell_Holy_Heal'}, - ['Healing Tongue II']={t=1000,icon='Spell_Holy_Heal'}, - ['Healing Touch']={t=3500,icon='Spell_Nature_HealingTouch'}, - ['Healing Ward']={t=2000,icon='Spell_Holy_LayOnHands'}, - ['Healing Wave']={t=3000,icon='Spell_Nature_MagicImmunity'}, - ['Healing Wave of Antu\'sul']={t=1000,icon='Spell_Holy_Heal02'}, - ['Heart of Hakkar - Molthor chucks the heart']={t=2000,icon='Temp'}, - ['Hearthstone']={t=10000,icon='INV_Misc_Rune_01'}, - ['Heavy Dynamite']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Heightened Senses']={t=1000,icon='Temp'}, - ['Hellfire']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Hellfire II']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Hellfire III']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Herb Gathering']={t=5000,icon='Spell_Nature_NatureTouchGrow'}, - ['Hex']={t=2000,icon='Spell_Nature_Polymorph'}, - ['Hex of Ravenclaw']={t=2000,icon='Spell_Shadow_Charm'}, - ['Hi-Explosive Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Hibernate']={t=1500,icon='Spell_Nature_Sleep'}, - ['Holy Fire']={t=3500,icon='Spell_Holy_SearingLight'}, - ['Holy Light']={t=2500,icon='Spell_Holy_HolyBolt'}, - ['Holy Smite']={t=2500,icon='Spell_Holy_HolySmite'}, - ['Holy Wrath']={t=2000,icon='Spell_Holy_Excorcism'}, - ['Honor Points +138']={t=1000,icon='Temp'}, - ['Honor Points +228']={t=1000,icon='Temp'}, - ['Honor Points +2388']={t=1000,icon='INV_BannerPVP_02'}, - ['Honor Points +378']={t=1000,icon='Temp'}, - ['Honor Points +398']={t=1000,icon='Temp'}, - ['Honor Points +50']={t=1000,icon='Temp'}, - ['Honor Points +82']={t=1000,icon='Temp'}, - ['Hooked Net']={t=500,icon='Ability_Ensnare'}, - ['Howl of Terror']={t=2000,icon='Spell_Shadow_DeathScream'}, - ['Howling Rage']={t=5000,icon='Ability_BullRush'}, - ['Ice Tomb']={t=1500,icon='Spell_Frost_Glacier'}, - ['Icebolt']={t=2000,icon='Spell_Frost_FrostBolt02'}, - ['Icicle']={t=1500,icon='Spell_Frost_FrostBolt02'}, - ['Icy Blue Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Identify Brood']={t=10000,icon='Spell_Lightning_LightningBolt01'}, - ['Ignite Flesh']={t=2000,icon='Spell_Fire_Fire'}, - ['Igniting Kroshius']={t=3000,icon='Temp'}, - ['Ilkrud\'s Guardians']={t=5000,icon='Spell_Shadow_SummonVoidWalker'}, - ['Imbue Chest - Absorb']={t=5000,icon='Spell_Holy_GreaterHeal'}, - ['Imbue Chest - Lesser Absorb']={t=5000,icon='Spell_Holy_GreaterHeal'}, - ['Imbue Chest - Lesser Spirit']={t=5000,icon='Spell_Holy_GreaterHeal'}, - ['Imbue Chest - Minor Spirit']={t=5000,icon='Spell_Holy_GreaterHeal'}, - ['Imbue Cloak - Lesser Protection']={t=5000,icon='Spell_Holy_GreaterHeal'}, - ['Imbue Cloak - Minor Resistance']={t=5000,icon='Spell_Holy_GreaterHeal'}, - ['Imbue Weapon - Beastslayer']={t=5000,icon='Spell_Holy_GreaterHeal'}, - ['Immolate']={t=2000,icon='Spell_Fire_Immolation'}, - ['Implosion']={t=10000,icon='Spell_Fire_SelfDestruct'}, - ['Incendia Powder']={t=5000,icon='Temp'}, - ['Incinerate']={t=2000,icon='Spell_Shadow_ChillTouch'}, - ['Incineration']={t=5000,icon='Temp'}, - ['Increase Reputation']={t=1000,icon='Temp'}, - ['Inducing Vision']={t=30000,icon='Spell_Shadow_LifeDrain'}, - ['Inferno']={t=2000,icon='Spell_Shadow_SummonInfernal'}, - ['Inferno Shell']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Ink Spray']={t=1000,icon='Spell_Nature_Sleep'}, - ['Instant Poison']={t=3000,icon='Ability_Poisons'}, - ['Instant Poison II']={t=3000,icon='Ability_Poisons'}, - ['Instant Poison III']={t=3000,icon='Ability_Poisons'}, - ['Instant Poison IV']={t=3000,icon='Ability_Poisons'}, - ['Instant Poison V']={t=3000,icon='Ability_Poisons'}, - ['Instant Poison VI']={t=3000,icon='Ability_Poisons'}, - ['Instant Toxin']={t=3000,icon='INV_Potion_19'}, - ['Instill Lord Valthalak\'s Spirit DND']={t=5000,icon='Temp'}, - ['Intense Pain']={t=1000,icon='Spell_Shadow_ShadowWordPain'}, - ['Intimidating Growl']={t=1000,icon='Ability_Racial_Cannibalize'}, - ['Invis Placing Bear Trap']={t=2000,icon='Temp'}, - ['Invisibility']={t=3000,icon='Spell_Nature_Invisibilty'}, - ['Iron Grenade']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Ivory Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Ivus Teleport Visual DND']={t=1000,icon='Temp'}, - ['J\'eevee summons object']={t=2000,icon='Temp'}, - ['Jarkal\'s Translation']={t=4500,icon='Spell_Holy_Restoration'}, - ['Julie\'s Blessing']={t=2000,icon='Spell_Holy_Renew'}, - ['Jumping Lightning']={t=3000,icon='Spell_Nature_Lightning'}, - ['Kadrak\'s Flag']={t=2000,icon='INV_Banner_03'}, - ['Kalaran Conjures Torch']={t=1000,icon='Temp'}, - ['Kev']={t=3000,icon='Spell_Fire_FireBolt'}, - ['Khadgar\'s Unlocking']={t=10000,icon='INV_Misc_Key_14'}, - ['King of the Gordok']={t=1000,icon='INV_Crown_02'}, - ['Kodo Kombobulator']={t=5000,icon='Trade_Fishing'}, - ['Kreeg\'s Stout Beatdown']={t=1000,icon='INV_Drink_05'}, - ['Large Copper Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Large Seaforium Charge']={t=5000,icon='Temp'}, - ['Large Timber Wolf']={t=3000,icon='Ability_Mount_BlackDireWolf'}, - ['Leopard']={t=3000,icon='Ability_Mount_JungleTiger'}, - ['Leper Cure!']={t=2000,icon='Spell_Holy_FlashHeal'}, - ['Lesser Heal']={t=2500,icon='Spell_Holy_LesserHeal'}, - ['Lesser Healing Wave']={t=1500,icon='Spell_Nature_HealingWaveLesser'}, - ['Lesser Invisibility']={t=3000,icon='Spell_Magic_LesserInvisibilty'}, - ['Lesser Mana Oil']={t=3000,icon='Temp'}, - ['Lesser Wizard Oil']={t=3000,icon='Temp'}, - ['Lethal Toxin']={t=3000,icon='Spell_Nature_CorrosiveBreath'}, - ['Life Harvest']={t=1000,icon='Spell_Shadow_Requiem'}, - ['Life Steal']={t=1500,icon='Spell_Shadow_LifeDrain02'}, - ['Lift Seal']={t=2000,icon='Temp'}, - ['Lightning Blast']={t=3200,icon='Spell_Nature_Lightning'}, - ['Lightning Bolt']={t=3000,icon='Spell_Nature_Lightning'}, - ['Lightning Breath']={t=2000,icon='Spell_Nature_Lightning'}, - ['Lightning Cloud']={t=3000,icon='Spell_Nature_CallStorm'}, - ['Lightning Totem']={t=500,icon='Spell_Nature_Lightning'}, - ['Lightwell']={t=1500,icon='Spell_Holy_SummonLightwell'}, - ['Linken\'s Boomerang']={t=500,icon='INV_Weapon_ShortBlade_10'}, - ['Lizard Bolt']={t=2000,icon='Spell_Nature_Lightning'}, - ['Locust Swarm']={t=3000,icon='Spell_Nature_InsectSwarm'}, - ['Longshot II']={t=4000,icon='Ability_Marksmanship'}, - ['Longshot III']={t=4000,icon='Ability_Marksmanship'}, - ['Longsight']={t=1000,icon='Ability_TownWatch'}, - ['Lunar Invititation']={t=5000,icon='Spell_Arcane_TeleportMoonglade'}, - ['Machine Gun']={t=500,icon='Ability_Marksmanship'}, - ['Magatha Incendia Powder']={t=1300,icon='Temp'}, - ['Mage Sight']={t=3000,icon='Temp'}, - ['Magma Blast']={t=1000,icon='Spell_Fire_FlameShock'}, - ['Majordomo Teleport Visual']={t=1000,icon='Spell_Arcane_Blink'}, - ['Mana Burn']={t=3000,icon='Spell_Shadow_ManaBurn'}, - ['Manastorm']={t=2000,icon='Spell_Frost_IceStorm'}, - ['Manifest Spirit']={t=5000,icon='Spell_Totem_WardOfDraining'}, - ['Manifestation Cleansing']={t=4000,icon='Temp'}, - ['Mark of Flames']={t=1000,icon='Spell_Fire_Fireball'}, - ['Marksman Hit']={t=2000,icon='Ability_Marksmanship'}, - ['Mass Dispell']={t=1000,icon='Spell_Shadow_Teleport'}, - ['Mass Healing']={t=1000,icon='Spell_Holy_GreaterHeal'}, - ['Massive Geyser']={t=1500,icon='Spell_Frost_SummonWaterElemental'}, - ['Massive Mortar']={t=3000,icon='Temp'}, - ['Maypole']={t=10000,icon='Spell_Shadow_Twilight'}, - ['Mebok Smart Drink']={t=3000,icon='Temp'}, - ['Mechanical Patch Kit']={t=2000,icon='INV_Gizmo_03'}, - ['Mechanical Squirrel']={t=1000,icon='Spell_Shadow_Metamorphosis'}, - ['Megavolt']={t=2000,icon='Spell_Nature_ChainLightning'}, - ['Melodious Rapture']={t=1000,icon='Temp'}, - ['Melt Ore']={t=1500,icon='Spell_Fire_SelfDestruct'}, - ['Merging Oozes']={t=500,icon='INV_Potion_12'}, - ['Merithra\'s Wake']={t=2000,icon='Temp'}, - ['Miblon\'s Bait']={t=2000,icon='INV_Misc_Food_50'}, - ['Midsummer Sausage']={t=1000,icon='INV_Misc_Food_53'}, - ['Might of Ragnaros']={t=500,icon='Spell_Fire_SelfDestruct'}, - ['Might of Shahram']={t=1000,icon='Spell_Nature_WispSplode'}, - ['Mind Blast']={t=1500,icon='Spell_Shadow_UnholyFrenzy'}, - ['Mind Control']={t=3000,icon='Spell_Shadow_ShadowWordDominate'}, - ['Mind Rot']={t=2000,icon='Spell_Shadow_MindRot'}, - ['Mind Tremor']={t=2000,icon='Spell_Nature_Earthquake'}, - ['Mind-numbing Poison']={t=3000,icon='Spell_Nature_NullifyDisease'}, - ['Mind-numbing Poison II']={t=3000,icon='Spell_Nature_NullifyDisease'}, - ['Mind-numbing Poison III']={t=3000,icon='Spell_Nature_NullifyDisease'}, - ['Minigun']={t=100,icon='INV_Musket_04'}, - ['Mining']={t=3200,icon='Trade_Mining'}, - ['Minion of Morganth']={t=2500,icon='Spell_Totem_WardOfDraining'}, - ['Minions of Malathrom']={t=1000,icon='Spell_Shadow_CorpseExplode'}, - ['Minor Mana Oil']={t=3000,icon='Temp'}, - ['Minor Wizard Oil']={t=3000,icon='Temp'}, - ['Miring Mud']={t=1000,icon='Spell_Nature_StrangleVines'}, - ['Mirkfallon Fungus']={t=3000,icon='Spell_Holy_HarmUndeadAura'}, - ['Mistletoe']={t=1000,icon='INV_Misc_Branch_01'}, - ['Mithril Frag Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Molten Blast']={t=2000,icon='Spell_Fire_Fire'}, - ['Molten Metal']={t=2000,icon='Spell_Fire_Fireball'}, - ['Molten Rain']={t=2000,icon='Temp'}, - ['Mor\'rogal Enchant']={t=1300,icon='Temp'}, - ['Mottled Red Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Nagmara\'s Love Potion']={t=1000,icon='Temp'}, - ['Narain!']={t=3000,icon='INV_Misc_Head_Gnome_01'}, - ['Naralex\'s Nightmare']={t=2000,icon='Spell_Nature_Sleep'}, - ['Nature Weakness']={t=5000,icon='INV_Misc_QirajiCrystal_03'}, - ['Nefarius Attack 001']={t=1000,icon='Temp'}, - ['Nether Gem']={t=3000,icon='Temp'}, - ['New Magic Missile (Test)']={t=2000,icon='Temp'}, - ['Nightcrawlers']={t=5000,icon='Trade_Fishing'}, - ['Nightsaber']={t=3000,icon='Ability_Mount_BlackPanther'}, - ['Nostalgia']={t=4000,icon='Spell_Shadow_LifeDrain'}, - ['Nullify Mana']={t=2000,icon='Spell_Shadow_DarkRitual'}, - ['Numbing Pain']={t=1500,icon='Spell_Nature_CorrosiveBreath'}, - ['Obsidian Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Opening']={t=5000,icon='Temp'}, - ['Opening - No Text']={t=5000,icon='Temp'}, - ['Opening Bar Door']={t=5000,icon='Temp'}, - ['Opening Benedict\'s Chest']={t=5000,icon='Temp'}, - ['Opening Booty Chest']={t=5000,icon='Temp'}, - ['Opening Cage']={t=5000,icon='Temp'}, - ['Opening Chest']={t=5000,icon='Temp'}, - ['Opening Dark Coffer']={t=5000,icon='Temp'}, - ['Opening Greater Scarab Coffer']={t=5000,icon='Temp'}, - ['Opening Relic Coffer']={t=5000,icon='Temp'}, - ['Opening Safe']={t=5000,icon='Temp'}, - ['Opening Scarab Coffer']={t=5000,icon='Temp'}, - ['Opening Secret Safe']={t=5000,icon='Temp'}, - ['Opening Secure Safe']={t=5000,icon='Temp'}, - ['Opening Stratholme Postbox']={t=5000,icon='Temp'}, - ['Opening Strongbox']={t=5000,icon='Temp'}, - ['Opening Termite Barrel']={t=5000,icon='Temp'}, - ['Ouro Submerge Visual']={t=1500,icon='Spell_Fire_Volcano'}, - ['Owl Form']={t=5000,icon='Spell_Nature_RavenForm'}, - ['Ozzie Explodes']={t=2000,icon='Temp'}, - ['Palamino Stallion']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Palomino Stallion']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Panther']={t=3000,icon='Ability_Mount_BlackPanther'}, - ['Panther Cage Key']={t=5000,icon='Temp'}, - ['Parasite']={t=2000,icon='Ability_Poisons'}, - ['Party Fever']={t=1000,icon='Spell_Shadow_DarkSummoning'}, - ['Peasant Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Peon Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Perm. Illusion Bishop Tyriona']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Perm. Illusion Tyrion']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Pick Lock']={t=5000,icon='Spell_Nature_MoonKey'}, - ['Pickpocket (PT)']={t=5000,icon='Temp'}, - ['Piercing Shadow']={t=2000,icon='Spell_Shadow_ChillTouch'}, - ['Pillar Delving']={t=5000,icon='Temp'}, - ['Pinto Horse']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Place Arcanite Buoy']={t=2000,icon='Temp'}, - ['Place Ghost Magnet']={t=1500,icon='Temp'}, - ['Place Lion Carcass']={t=2500,icon='INV_Misc_Bowl_01'}, - ['Place Scryer']={t=3000,icon='Temp'}, - ['Place Threshadon Carcass']={t=2500,icon='INV_Misc_Bowl_01'}, - ['Place Toxic Fogger']={t=3000,icon='INV_Cask_01'}, - ['Place Unfired Blade']={t=1000,icon='Temp'}, - ['Place Unforged Seal']={t=500,icon='Temp'}, - ['Placing Beacon Torch']={t=2300,icon='Temp'}, - ['Placing Bear Trap']={t=2000,icon='Temp'}, - ['Placing Pendant']={t=5000,icon='Temp'}, - ['Placing Smokey\'s Explosives']={t=3000,icon='Temp'}, - ['Plague Cloud']={t=2000,icon='Spell_Shadow_CallofBone'}, - ['Plague Mind']={t=4000,icon='Spell_Shadow_CallofBone'}, - ['Plant Gor\'tesh Head']={t=5000,icon='Temp'}, - ['Plant Magic Beans']={t=2000,icon='INV_Misc_Food_Wheat_02'}, - ['Plant Seeds']={t=5000,icon='Temp'}, - ['Planting Banner']={t=2300,icon='Temp'}, - ['Planting Guse\'s Beacon']={t=5000,icon='Temp'}, - ['Planting Ichman\'s Beacon']={t=5000,icon='Temp'}, - ['Planting Jeztor\'s Beacon']={t=5000,icon='Temp'}, - ['Planting Mulverick\'s Beacon']={t=5000,icon='Temp'}, - ['Planting Ryson\'s Beacon']={t=5000,icon='Temp'}, - ['Planting Slidore\'s Beacon']={t=5000,icon='Temp'}, - ['Planting Vipore\'s Beacon']={t=5000,icon='Temp'}, - ['Pointy Spike']={t=500,icon='Ability_ImpalingBolt'}, - ['Poison Bolt']={t=2500,icon='Spell_Nature_CorrosiveBreath'}, - ['Poison Cloud']={t=1000,icon='Spell_Nature_Regenerate'}, - ['Poison Stinger']={t=2000,icon='INV_Misc_MonsterTail_03'}, - ['Poisoned Harpoon']={t=2000,icon='Ability_Poisons'}, - ['Poisoned Shot']={t=2000,icon='Ability_Poisons'}, - ['Poisonous Spit']={t=2000,icon='Spell_Nature_CorrosiveBreath'}, - ['Polarity Shift']={t=3000,icon='Spell_Nature_Lightning'}, - ['Polymorph']={t=1500,icon='Spell_Nature_Polymorph'}, - ['Polymorph: Cow']={t=1500,icon='Spell_Nature_Polymorph_Cow'}, - ['Polymorph: Pig']={t=1500,icon='Spell_Magic_PolymorphPig'}, - ['Polymorph: Turtle']={t=1500,icon='Ability_Hunter_Pet_Turtle'}, - ['Portal: Darnassus']={t=10000,icon='Spell_Arcane_PortalDarnassus'}, - ['Portal: Ironforge']={t=10000,icon='Spell_Arcane_PortalIronForge'}, - ['Portal: Karazhan']={t=10000,icon='Spell_Arcane_PortalUnderCity'}, - ['Portal: Orgrimmar']={t=10000,icon='Spell_Arcane_PortalOrgrimmar'}, - ['Portal: Stormwind']={t=10000,icon='Spell_Arcane_PortalStormWind'}, - ['Portal: Thunder Bluff']={t=10000,icon='Spell_Arcane_PortalThunderBluff'}, - ['Portal: Undercity']={t=10000,icon='Spell_Arcane_PortalUnderCity'}, - ['Potion Toss']={t=2000,icon='Spell_Misc_Drink'}, - ['Powerful Seaforium Charge']={t=5000,icon='Temp'}, - ['Powerful Smelling Salts']={t=2000,icon='INV_Misc_Ammo_Gunpowder_01'}, - ['Prayer of Elune']={t=1000,icon='Spell_Holy_Resurrection'}, - ['Prayer of Healing']={t=3000,icon='Spell_Holy_PrayerOfHealing02'}, - ['Presence of Death']={t=1000,icon='Spell_Shadow_ShadeTrueSight'}, - ['Primal Leopard']={t=3000,icon='Ability_Mount_JungleTiger'}, - ['Princess Summons Portal']={t=10000,icon='Spell_Arcane_PortalIronForge'}, - ['Proudmoore\'s Defense']={t=3000,icon='Spell_Holy_BlessingOfProtection'}, - ['Psychometry']={t=5000,icon='Spell_Holy_Restoration'}, - ['Purify and Place Food']={t=5000,icon='INV_Misc_Bowl_01'}, - ['Purple Hands']={t=4000,icon='Spell_Shadow_SiphonMana'}, - ['Purple Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Purple Skeletal Warhorse']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Pyroblast']={t=6000,icon='Spell_Fire_Fireball02'}, - ['Quest - Sergra Darkthorn Spell']={t=3000,icon='Temp'}, - ['Quest - Summon Treant']={t=3000,icon='Spell_Nature_NatureTouchGrow'}, - ['Quest - Teleport Spawn-out']={t=1000,icon='Temp'}, - ['Quest - Troll Hero Summon Visual']={t=30000,icon='Temp'}, - ['Quick Bloodlust']={t=2000,icon='Spell_Nature_BloodLust'}, - ['Quick Flame Ward']={t=1500,icon='Spell_Fire_SealOfFire'}, - ['Quick Frost Ward']={t=1500,icon='Spell_Fire_SealOfFire'}, - ['Radiation Bolt']={t=3000,icon='Spell_Shadow_CorpseExplode'}, - ['Rage of Thule']={t=1500,icon='Spell_Shadow_UnholyFrenzy'}, - ['Ragnaros Emerge']={t=2900,icon='Spell_Fire_LavaSpawn'}, - ['Rain of Fire']={t=3000,icon='Spell_Shadow_RainOfFire'}, - ['Raise Dead']={t=1000,icon='Spell_Shadow_RaiseDead'}, - ['Raise Undead Scarab']={t=1000,icon='Spell_Shadow_Contagion'}, - ['Raptor Feather']={t=5000,icon='Temp'}, - ['Razor Mane']={t=1000,icon='Spell_Nature_Thorns'}, - ['Rebirth']={t=2000,icon='Spell_Nature_Reincarnation'}, - ['Recall']={t=10000,icon='Temp'}, - ['Recite Words of Celebras']={t=3000,icon='Temp'}, - ['Reconstruction']={t=3000,icon='Temp'}, - ['Red & Blue Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Red Dragon Transform DND']={t=1000,icon='Temp'}, - ['Red Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Red Skeletal Horse']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Red Skeletal Warhorse']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Red Wolf']={t=3000,icon='Ability_Mount_BlackDireWolf'}, - ['Redemption']={t=10000,icon='Spell_Holy_Resurrection'}, - ['Regrowth']={t=2000,icon='Spell_Nature_ResistNature'}, - ['Reindeer Dust Effect']={t=8000,icon='Temp'}, - ['Release Imp']={t=2000,icon='Temp'}, - ['Release J\'eevee']={t=2000,icon='Temp'}, - ['Release Rageclaw']={t=10000,icon='Temp'}, - ['Release Umi\'s Yeti']={t=2000,icon='Temp'}, - ['Release Winna\'s Kitten']={t=1000,icon='Ability_Seal'}, - ['Release the Hounds']={t=2000,icon='Temp'}, - ['Releasing Corrupt Ooze']={t=5000,icon='INV_Potion_19'}, - ['Remote Detonate']={t=1000,icon='INV_Misc_StoneTablet_04'}, - ['Remove Insignia']={t=1000,icon='Temp'}, - ['Renew']={t=2000,icon='Spell_Holy_Renew'}, - ['Replenish Spirit']={t=3000,icon='Spell_Nature_MoonGlow'}, - ['Replenish Spirit II']={t=3000,icon='Spell_Nature_MoonGlow'}, - ['Reputation - Ahn\'Qiraj Temple Boss']={t=1000,icon='Temp'}, - ['Reputation - Booty Bay +500']={t=1000,icon='Temp'}, - ['Reputation - Everlook +500']={t=1000,icon='Temp'}, - ['Reputation - Gadgetzan +500']={t=1000,icon='Temp'}, - ['Reputation - Ratchet +500']={t=1000,icon='Temp'}, - ['Resupply']={t=2000,icon='Spell_Misc_Drink'}, - ['Resurrection']={t=10000,icon='Spell_Holy_Resurrection'}, - ['Retching Plague']={t=2000,icon='Spell_Shadow_CallofBone'}, - ['Revive Dig Rat']={t=3000,icon='Spell_Holy_Resurrection'}, - ['Revive Pet']={t=10000,icon='Ability_Hunter_BeastSoothe'}, - ['Revive Ringo']={t=2500,icon='Temp'}, - ['Riding Kodo']={t=3000,icon='INV_Misc_Head_Tauren_02'}, - ['Riding Turtle']={t=3000,icon='Ability_Hunter_Pet_Turtle'}, - ['Rift Beacon']={t=2000,icon='Spell_Nature_AbolishMagic'}, - ['Righteous Flame On']={t=4000,icon='Spell_Holy_InnerFire'}, - ['Rimblat Grows Flower DND']={t=2000,icon='Temp'}, - ['Ritual of Doom']={t=10000,icon='Spell_Shadow_AntiMagicShell'}, - ['Ritual of Doom Effect']={t=10000,icon='Spell_Arcane_PortalDarnassus'}, - ['Ritual of Summoning']={t=5000,icon='Spell_Shadow_Twilight'}, - ['Ritual of Summoning Effect']={t=5000,icon='Temp'}, - ['Rocket Blast']={t=3000,icon='Temp'}, - ['Rookery Whelp Spawn-in Spell']={t=500,icon='Temp'}, - ['Rotate Trigger']={t=3000,icon='Temp'}, - ['Rough Copper Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Rough Dynamite']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Rumsey Rum']={t=1000,icon='INV_Drink_03'}, - ['Rumsey Rum Black Label']={t=1000,icon='INV_Drink_04'}, - ['Rumsey Rum Dark']={t=1000,icon='INV_Drink_04'}, - ['Rumsey Rum Light']={t=1000,icon='INV_Drink_08'}, - ['Rune of Opening']={t=5000,icon='Temp'}, - ['Ruul Snowhoof Shapechange (DND)']={t=1000,icon='Temp'}, - ['Ryson\'s All Seeing Eye']={t=1000,icon='Ability_Ambush'}, - ['Ryson\'s Eye in the Sky']={t=1000,icon='Ability_Hunter_EagleEye'}, - ['Sacrifice']={t=1000,icon='Spell_Holy_DivineIntervention'}, - ['Sacrifice Spinneret']={t=10000,icon='Temp'}, - ['Sand Blast']={t=2000,icon='Spell_Nature_Cyclone'}, - ['Sand Breath']={t=2000,icon='Spell_Fire_WindsofWoe'}, - ['Sapper Explode']={t=5000,icon='Spell_Fire_SelfDestruct'}, - ['Sapphiron DND']={t=20000,icon='Temp'}, - ['Sarilus\'s Elementals']={t=3000,icon='Spell_Shadow_RaiseDead'}, - ['Scare Beast']={t=1500,icon='Ability_Druid_Cower'}, - ['Scarlet Resurrection']={t=2000,icon='Spell_Holy_Resurrection'}, - ['Scarshield Portal']={t=1500,icon='Spell_Arcane_TeleportOrgrimmar'}, - ['Scorch']={t=1500,icon='Spell_Fire_SoulBurn'}, - ['Searing Flames']={t=2000,icon='Spell_Fire_Immolation'}, - ['Searing Pain']={t=1500,icon='Spell_Fire_SoulBurn'}, - ['Seduction']={t=1500,icon='Spell_Shadow_MindSteal'}, - ['Seeping Willow']={t=1000,icon='Spell_Nature_CorrosiveBreath'}, - ['Segra Darkthorn Effect']={t=3000,icon='Temp'}, - ['Self Destruct']={t=7000,icon='Spell_Fire_SelfDestruct'}, - ['Self Detonation']={t=7000,icon='Spell_Fire_SelfDestruct'}, - ['Self Resurrection']={t=5000,icon='Temp'}, - ['Serpentine Cleansing']={t=30000,icon='Spell_Shadow_LifeDrain'}, - ['Set NG-5 Charge (Blue)']={t=5000,icon='INV_Misc_Bomb_05'}, - ['Set NG-5 Charge (Red)']={t=5000,icon='INV_Misc_Bomb_05'}, - ['Shackle Undead']={t=1500,icon='Spell_Nature_Slow'}, - ['Shadow Bolt']={t=3000,icon='Spell_Shadow_ShadowBolt'}, - ['Shadow Bolt Misfire']={t=2000,icon='Spell_Shadow_ShadowBolt'}, - ['Shadow Bolt Volley']={t=3000,icon='Spell_Shadow_ShadowBolt'}, - ['Shadow Flame']={t=2000,icon='Spell_Fire_Incinerate'}, - ['Shadow Nova II']={t=3000,icon='Spell_Shadow_ShadeTrueSight'}, - ['Shadow Oil']={t=3000,icon='Temp'}, - ['Shadow Port']={t=250,icon='Spell_Shadow_AntiShadow'}, - ['Shadow Portal']={t=1000,icon='Spell_Shadow_SealOfKings'}, - ['Shadow Resistance']={t=1000,icon='Spell_Frost_WizardMark'}, - ['Shadow Shell']={t=1000,icon='Spell_Shadow_AntiShadow'}, - ['Shadow Weakness']={t=5000,icon='INV_Misc_QirajiCrystal_05'}, - ['Shadowblink']={t=500,icon='Spell_Shadow_DetectLesserInvisibility'}, - ['Shared Bonds']={t=1500,icon='Spell_Shadow_UnsummonBuilding'}, - ['Sharpen Blade']={t=3000,icon='Temp'}, - ['Sharpen Blade II']={t=3000,icon='Temp'}, - ['Sharpen Blade III']={t=3000,icon='Temp'}, - ['Sharpen Blade IV']={t=3000,icon='Temp'}, - ['Sharpen Blade V']={t=3000,icon='Temp'}, - ['Sharpen Weapon - Critical']={t=3000,icon='Temp'}, - ['Shay\'s Bell']={t=4500,icon='Temp'}, - ['Shield of Reflection']={t=1000,icon='Spell_Shadow_Teleport'}, - ['Shiny Bauble']={t=5000,icon='INV_Misc_Orb_03'}, - ['Shock']={t=1000,icon='Temp'}, - ['Shockwave']={t=2000,icon='Ability_Whirlwind'}, - ['Shoot Bow']={t=1000,icon='Ability_Marksmanship'}, - ['Shoot Crossbow']={t=1000,icon='Ability_Marksmanship'}, - ['Shoot Gun']={t=1000,icon='Ability_Marksmanship'}, - ['Shoot Missile']={t=3000,icon='INV_Ammo_Bullet_03'}, - ['Shoot Rocket']={t=3000,icon='INV_Ammo_Bullet_03'}, - ['Shredder Armor Melt']={t=2000,icon='Spell_Fire_Incinerate'}, - ['Shrink']={t=3000,icon='Spell_Shadow_AntiShadow'}, - ['Silence']={t=1500,icon='Spell_Holy_Silence'}, - ['Silithid Pox']={t=2000,icon='Spell_Nature_NullifyDisease'}, - ['Silver Skeleton Key']={t=5000,icon='Temp'}, - ['Simple Teleport']={t=1000,icon='Spell_Magic_LesserInvisibilty'}, - ['Simple Teleport Group']={t=2000,icon='Spell_Magic_LesserInvisibilty'}, - ['Simple Teleport Other']={t=1000,icon='Spell_Magic_LesserInvisibilty'}, - ['Skeletal Horse']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Skeletal Miner Explode']={t=5000,icon='Spell_Fire_SelfDestruct'}, - ['Skeletal Steed']={t=3000,icon='Ability_Mount_Undeadhorse'}, - ['Skinning']={t=2000,icon='INV_Misc_Pelt_Wolf_01'}, - ['Slam']={t=1500,icon='Ability_Warrior_DecisiveStrike'}, - ['Slave Drain']={t=1000,icon='Spell_Shadow_ChillTouch'}, - ['Sleep']={t=1500,icon='Spell_Nature_Sleep'}, - ['Slime Bolt']={t=2500,icon='Spell_Nature_CorrosiveBreath'}, - ['Sling Dirt']={t=1000,icon='Spell_Nature_Sleep'}, - ['Sling Mud']={t=1000,icon='Spell_Nature_Sleep'}, - ['Slow Poison']={t=1000,icon='Spell_Nature_SlowPoison'}, - ['Slow Poison II']={t=1000,icon='Spell_Nature_SlowPoison'}, - ['Slowing Poison']={t=1000,icon='Spell_Nature_SlowPoison'}, - ['Sludge Toxin']={t=2000,icon='Spell_Nature_Regenerate'}, - ['Small Bronze Bomb']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Small Seaforium Charge']={t=5000,icon='Temp'}, - ['Smite']={t=2500,icon='Spell_Holy_HolySmite'}, - ['Smitten']={t=1000,icon='INV_Ammo_Arrow_02'}, - ['Snowman']={t=1500,icon='INV_Ammo_Snowball'}, - ['Snufflenose Command']={t=1500,icon='Spell_Shadow_LifeDrain'}, - ['Sol H']={t=3000,icon='Spell_Frost_ManaRecharge'}, - ['Sol L']={t=3000,icon='Spell_Frost_ManaRecharge'}, - ['Sol M']={t=3000,icon='Spell_Frost_ManaRecharge'}, - ['Sol U']={t=3000,icon='Spell_Frost_ManaRecharge'}, - ['Solid Dynamite']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Soothe Animal']={t=1500,icon='Ability_Hunter_BeastSoothe'}, - ['Soul Bite']={t=2000,icon='Spell_Shadow_SiphonMana'}, - ['Soul Breaker']={t=2000,icon='Spell_Shadow_Haunting'}, - ['Soul Consumption']={t=4000,icon='Ability_Racial_Cannibalize'}, - ['Soul Drain']={t=2000,icon='Spell_Shadow_LifeDrain02'}, - ['Soul Fire']={t=6000,icon='Spell_Fire_Fireball02'}, - ['Soul Shatter']={t=2000,icon='Spell_Fire_Fire'}, - ['Soulstone Resurrection']={t=3000,icon='Spell_Shadow_SoulGem'}, - ['South Seas Pirate Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Southsea Cannon Fire']={t=5000,icon='Spell_Fire_FireBolt02'}, - ['Spark']={t=2000,icon='Spell_Nature_Lightning'}, - ['Spawn Challenge to Urok']={t=2000,icon='Temp'}, - ['Speak with Heads']={t=5000,icon='Spell_Shadow_LifeDrain'}, - ['Spell Deflection (NYI)']={t=1000,icon='Temp'}, - ['Spice Mortar']={t=500,icon='Spell_Fire_Fireball02'}, - ['Spike Volley']={t=500,icon='Ability_ImpalingBolt'}, - ['Spirit Decay']={t=2000,icon='Spell_Holy_HarmUndeadAura'}, - ['Spirit Spawn-out']={t=500,icon='Temp'}, - ['Spirit Steal']={t=2000,icon='Spell_Shadow_Possession'}, - ['Spotted Frostsaber']={t=3000,icon='Ability_Mount_WhiteTiger'}, - ['Spotted Panther']={t=3000,icon='Ability_Mount_BlackPanther'}, - ['Sprinkling Purified Water']={t=6000,icon='Temp'}, - ['Starfire']={t=3500,icon='Spell_Arcane_StarFire'}, - ['Steel Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Stone Dwarf Awaken Visual']={t=1500,icon='Spell_Nature_Earthquake'}, - ['Stoned - Channel Cast Visual']={t=3000,icon='Spell_Nature_Cyclone'}, - ['Stoneskin']={t=6000,icon='Spell_Nature_EnchantArmor'}, - ['Stonesplinter Trogg Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Storm Bolt']={t=1000,icon='INV_Hammer_01'}, - ['Stormpike Battle Charger']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Strength of Arko\'narin']={t=4000,icon='Spell_Nature_AstralRecal'}, - ['Strength of the Ages']={t=2000,icon='Spell_Shadow_Requiem'}, - ['Strike']={t=2000,icon='Temp'}, - ['Striped Frostsaber']={t=3000,icon='Ability_Mount_WhiteTiger'}, - ['Striped Nightsaber']={t=3000,icon='Ability_Mount_BlackPanther'}, - ['Stuck']={t=10000,icon='Spell_Shadow_Teleport'}, - ['Stun Bomb']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Stun Bomb Attack']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Submerge Visual']={t=1500,icon='Spell_Fire_Volcano'}, - ['Summon']={t=1000,icon='Spell_Arcane_Blink'}, - ['Summon Alarm-o-Bot']={t=250,icon='INV_Gizmo_08'}, - ['Summon Albino Snake']={t=1000,icon='Ability_Seal'}, - ['Summon Albino Snapjaw']={t=1000,icon='Ability_Seal'}, - ['Summon Ancient Spirits']={t=2000,icon='Temp'}, - ['Summon Ancona']={t=1000,icon='Ability_Seal'}, - ['Summon Aquementas']={t=2000,icon='Temp'}, - ['Summon Ar\'lia']={t=2500,icon='Spell_Nature_GroundingTotem'}, - ['Summon Atal\'ai Skeleton']={t=1000,icon='Spell_Shadow_RaiseDead'}, - ['Summon Azure Whelpling']={t=1000,icon='Ability_Seal'}, - ['Summon Baby Shark']={t=1000,icon='Ability_Seal'}, - ['Summon Black Kingsnake']={t=1000,icon='Ability_Seal'}, - ['Summon Black Qiraji Battle Tank']={t=3000,icon='INV_Misc_QirajiCrystal_05'}, - ['Summon Blackhand Dreadweaver']={t=5000,icon='Spell_Nature_Purge'}, - ['Summon Blackhand Veteran']={t=5000,icon='Spell_Nature_Purge'}, - ['Summon Blood Parrot']={t=1000,icon='Ability_Seal'}, - ['Summon Bloodpetal Mini Pests']={t=2000,icon='Spell_Shadow_DarkSummoning'}, - ['Summon Blue Qiraji Battle Tank']={t=3000,icon='INV_Misc_QirajiCrystal_04'}, - ['Summon Blue Racer']={t=1000,icon='Ability_Seal'}, - ['Summon Boar Spirit']={t=1500,icon='Spell_Magic_PolymorphPig'}, - ['Summon Bomb']={t=1000,icon='Ability_Seal'}, - ['Summon Bombay']={t=1000,icon='Ability_Seal'}, - ['Summon Bronze Whelpling']={t=1000,icon='Ability_Seal'}, - ['Summon Brown Snake']={t=1000,icon='Ability_Seal'}, - ['Summon Carrion Scarab']={t=2000,icon='Spell_Shadow_CarrionSwarm'}, - ['Summon Charger']={t=3000,icon='Ability_Mount_Charger'}, - ['Summon Cockatiel']={t=1000,icon='Ability_Seal'}, - ['Summon Cockatoo']={t=1000,icon='Ability_Seal'}, - ['Summon Cockroach']={t=1000,icon='Ability_Seal'}, - ['Summon Common Kitten']={t=2000,icon='INV_Box_PetCarrier_01'}, - ['Summon Cornish Rex']={t=1000,icon='Ability_Seal'}, - ['Summon Corrupted Kitten']={t=1000,icon='Ability_Seal'}, - ['Summon Cottontail Rabbit']={t=1000,icon='Ability_Seal'}, - ['Summon Crimson Snake']={t=1000,icon='Ability_Seal'}, - ['Summon Crimson Whelpling']={t=1000,icon='Ability_Seal'}, - ['Summon Cyclonian']={t=10000,icon='Spell_Nature_EarthBind'}, - ['Summon Dagun']={t=5000,icon='Temp'}, - ['Summon Dark Whelpling']={t=1000,icon='Ability_Seal'}, - ['Summon Dart Frog']={t=1000,icon='Ability_Seal'}, - ['Summon Demon of the Orb']={t=5000,icon='Temp'}, - ['Summon Diablo']={t=1000,icon='Ability_Seal'}, - ['Summon Disgusting Oozeling']={t=1000,icon='Ability_Seal'}, - ['Summon Dreadsteed']={t=3000,icon='Ability_Mount_Dreadsteed'}, - ['Summon Eagle Owl']={t=1000,icon='Ability_Seal'}, - ['Summon Echeyakee']={t=500,icon='Spell_Shadow_LifeDrain'}, - ['Summon Edana Hatetalon']={t=4000,icon='Temp'}, - ['Summon Effect']={t=5000,icon='Spell_Shadow_AnimateDead'}, - ['Summon Elven Wisp']={t=1000,icon='Ability_Seal'}, - ['Summon Embers']={t=2000,icon='Spell_Fire_Fire'}, - ['Summon Emerald Whelpling']={t=1000,icon='Ability_Seal'}, - ['Summon Faeling']={t=1000,icon='Ability_Seal'}, - ['Summon Farm Chicken']={t=1000,icon='Ability_Seal'}, - ['Summon Felhunter']={t=10000,icon='Spell_Shadow_SummonFelHunter'}, - ['Summon Felsteed']={t=3000,icon='Spell_Nature_Swiftness'}, - ['Summon Frail Skeleton']={t=10000,icon='Spell_Shadow_RaiseDead'}, - ['Summon Gnashjaw']={t=3000,icon='Temp'}, - ['Summon Goblin Bomb']={t=250,icon='Ability_Repair'}, - ['Summon Great Horned Owl']={t=1000,icon='Ability_Seal'}, - ['Summon Green Qiraji Battle Tank']={t=3000,icon='INV_Misc_QirajiCrystal_03'}, - ['Summon Green Water Snake']={t=1000,icon='Ability_Seal'}, - ['Summon Green Wing Macaw']={t=1000,icon='Ability_Seal'}, - ['Summon Gunther\'s Visage']={t=2000,icon='Temp'}, - ['Summon Gurky']={t=1000,icon='Ability_Seal'}, - ['Summon Hawk Owl']={t=1000,icon='Ability_Seal'}, - ['Summon Hawksbill Snapjaw']={t=1000,icon='Ability_Seal'}, - ['Summon Helcular\'s Puppets']={t=3000,icon='Spell_Shadow_Haunting'}, - ['Summon Hippogryph Hatchling']={t=1000,icon='Ability_Seal'}, - ['Summon Hyacinth Macaw']={t=1000,icon='Ability_Seal'}, - ['Summon Illusionary Dreamwatchers']={t=1000,icon='Spell_Shadow_Teleport'}, - ['Summon Illusionary Nightmare']={t=2000,icon='Spell_Fire_SealOfFire'}, - ['Summon Illusionary Phantasm']={t=2000,icon='Spell_Fire_SealOfFire'}, - ['Summon Illusory Wraith']={t=1000,icon='Spell_Shadow_Teleport'}, - ['Summon Imp']={t=10000,icon='Spell_Shadow_SummonImp'}, - ['Summon Infernal Servant']={t=2000,icon='Spell_Shadow_SummonInfernal'}, - ['Summon Ishamuhale']={t=2000,icon='Spell_Shadow_LifeDrain'}, - ['Summon Island Frog']={t=1000,icon='Ability_Seal'}, - ['Summon Jubling']={t=1000,icon='Ability_Seal'}, - ['Summon Karang\'s Banner']={t=500,icon='Temp'}, - ['Summon Leatherback Snapjaw']={t=1000,icon='Ability_Seal'}, - ['Summon Lifelike Toad']={t=1000,icon='Ability_Seal'}, - ['Summon Living Flame']={t=2000,icon='Spell_Fire_Fire'}, - ['Summon Loggerhead Snapjaw']={t=1000,icon='Ability_Seal'}, - ['Summon Lunaclaw']={t=3000,icon='Temp'}, - ['Summon Lupine Delusions']={t=1000,icon='Spell_Shadow_Teleport'}, - ['Summon Magic Staff']={t=2000,icon='INV_Staff_26'}, - ['Summon Magram Ravager']={t=4000,icon='Spell_Shadow_RaiseDead'}, - ['Summon Maine Coon']={t=1000,icon='Ability_Seal'}, - ['Summon Mechanical Chicken']={t=1000,icon='Ability_Seal'}, - ['Summon Minion']={t=500,icon='Temp'}, - ['Summon Mr. Wiggles']={t=1000,icon='Ability_Seal'}, - ['Summon Murki']={t=1000,icon='Ability_Seal'}, - ['Summon Murky']={t=1000,icon='Ability_Seal'}, - ['Summon Myzrael']={t=10000,icon='Spell_Shadow_LifeDrain'}, - ['Summon Netherwalker']={t=4000,icon='Spell_Shadow_GatherShadows'}, - ['Summon Olive Snapjaw']={t=1000,icon='Ability_Seal'}, - ['Summon Onyxia Whelp']={t=2000,icon='Temp'}, - ['Summon Orange Tabby']={t=1000,icon='Ability_Seal'}, - ['Summon Orphan']={t=1000,icon='Ability_Seal'}, - ['Summon Panda']={t=1000,icon='Ability_Seal'}, - ['Summon Poley']={t=1000,icon='Ability_Seal'}, - ['Summon Prairie Chicken']={t=1000,icon='Ability_Seal'}, - ['Summon Prairie Dog']={t=1000,icon='Ability_Seal'}, - ['Summon Ragnaros']={t=10000,icon='Spell_Fire_LavaSpawn'}, - ['Summon Razelikh']={t=10000,icon='Temp'}, - ['Summon Red Qiraji Battle Tank']={t=3000,icon='INV_Misc_QirajiCrystal_02'}, - ['Summon Remote-Controlled Golem']={t=3000,icon='Ability_Repair'}, - ['Summon Ribbon Snake']={t=1000,icon='Ability_Seal'}, - ['Summon Riding Gryphon']={t=3000,icon='Ability_BullRush'}, - ['Summon Risen Lackey']={t=2000,icon='Spell_Shadow_RaiseDead'}, - ['Summon Robot']={t=1000,icon='Ability_Seal'}, - ['Summon Rockwing Gargoyles']={t=10000,icon='Spell_Shadow_UnsummonBuilding'}, - ['Summon Rookery Whelp']={t=2000,icon='Temp'}, - ['Summon Scarlet Snake']={t=1000,icon='Ability_Seal'}, - ['Summon Screecher Spirit']={t=2000,icon='Temp'}, - ['Summon Senegal']={t=1000,icon='Ability_Seal'}, - ['Summon Shadowcaster']={t=5000,icon='Spell_Shadow_RaiseDead'}, - ['Summon Shadowstrike']={t=10000,icon='Temp'}, - ['Summon Shield Guard']={t=5000,icon='Spell_Nature_Purge'}, - ['Summon Shy-Rotam']={t=2500,icon='Temp'}, - ['Summon Siamese']={t=1000,icon='Ability_Seal'}, - ['Summon Silver Tabby']={t=1000,icon='Ability_Seal'}, - ['Summon Skeleton']={t=2000,icon='Spell_Shadow_RaiseDead'}, - ['Summon Smolderweb']={t=1000,icon='Ability_Seal'}, - ['Summon Snowshoe Rabbit']={t=1000,icon='Ability_Seal'}, - ['Summon Snowy Owl']={t=1000,icon='Ability_Seal'}, - ['Summon Snufflenose']={t=2000,icon='Spell_Nature_ProtectionformNature'}, - ['Summon Spawn of Bael\'Gar']={t=4000,icon='Spell_Fire_LavaSpawn'}, - ['Summon Speedy']={t=1000,icon='Ability_Seal'}, - ['Summon Spell Guard']={t=5000,icon='Spell_Nature_Purge'}, - ['Summon Spider God']={t=10000,icon='Spell_Shadow_LifeDrain'}, - ['Summon Spirit of Old']={t=4000,icon='Spell_Nature_Purge'}, - ['Summon Spotted Rabbit']={t=1000,icon='Ability_Seal'}, - ['Summon Sprite Darter Hatchling']={t=1000,icon='Ability_Seal'}, - ['Summon Succubus']={t=10000,icon='Spell_Shadow_SummonSuccubus'}, - ['Summon Swamp Ooze']={t=2500,icon='Spell_Shadow_BlackPlague'}, - ['Summon Swamp Spirit']={t=1500,icon='Spell_Nature_AbolishMagic'}, - ['Summon Syndicate Spectre']={t=1000,icon='Spell_Shadow_Twilight'}, - ['Summon Terky']={t=1000,icon='Ability_Seal'}, - ['Summon Tervosh\'s Minion']={t=4000,icon='Spell_Frost_Wisp'}, - ['Summon Thelrin DND']={t=1000,icon='Temp'}, - ['Summon Theurgist']={t=5000,icon='Spell_Nature_Purge'}, - ['Summon Thunderstrike']={t=10000,icon='Temp'}, - ['Summon Timberling']={t=3000,icon='Spell_Nature_ProtectionformNature'}, - ['Summon Tiny Green Dragon']={t=1000,icon='Ability_Seal'}, - ['Summon Tiny Red Dragon']={t=1000,icon='Ability_Seal'}, - ['Summon Tranquil Mechanical Yeti']={t=1000,icon='Ability_Seal'}, - ['Summon Treant Allies']={t=1500,icon='Spell_Nature_ForceOfNature'}, - ['Summon Treasure Horde']={t=1500,icon='Temp'}, - ['Summon Treasure Horde Visual']={t=1500,icon='Temp'}, - ['Summon Tree Frog']={t=1000,icon='Ability_Seal'}, - ['Summon Viper']={t=1500,icon='Spell_Nature_ResistMagic'}, - ['Summon Voidwalker']={t=10000,icon='Spell_Shadow_SummonVoidWalker'}, - ['Summon Warhorse']={t=3000,icon='Spell_Nature_Swiftness'}, - ['Summon Water Elemental']={t=2000,icon='Spell_Shadow_SealOfKings'}, - ['Summon Whiskers']={t=1000,icon='Ability_Seal'}, - ['Summon White Kitten']={t=1000,icon='Ability_Seal'}, - ['Summon White Plymouth Rock']={t=1000,icon='Ability_Seal'}, - ['Summon White Tiger Cub']={t=1000,icon='Ability_Seal'}, - ['Summon Witherbark Felhunter']={t=3000,icon='Spell_Shadow_SummonFelHunter'}, - ['Summon Wood Frog']={t=1000,icon='Ability_Seal'}, - ['Summon Worg Pup']={t=1000,icon='Ability_Seal'}, - ['Summon Xorothian Dreadsteed']={t=5000,icon='Temp'}, - ['Summon Yellow Qiraji Battle Tank']={t=3000,icon='INV_Misc_QirajiCrystal_01'}, - ['Summon Zergling']={t=1000,icon='Ability_Seal'}, - ['Summon Zombie']={t=2000,icon='Spell_Shadow_RaiseDead'}, - ['Summoned Urok']={t=1000,icon='Temp'}, - ['Super Crystal']={t=6000,icon='Temp'}, - ['Superior Healing Ward']={t=2000,icon='Spell_Holy_LayOnHands'}, - ['Sweep']={t=1500,icon='Spell_Nature_Thorns'}, - ['Sweet Surprise']={t=1000,icon='INV_ValentinesChocolate03'}, - ['Swift Blue Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Swift Brown Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Swift Brown Steed']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Swift Brown Wolf']={t=3000,icon='Ability_Mount_BlackDireWolf'}, - ['Swift Dawnsaber']={t=3000,icon='Ability_Mount_JungleTiger'}, - ['Swift Frostsaber']={t=3000,icon='Ability_Mount_WhiteTiger'}, - ['Swift Gray Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Swift Gray Wolf']={t=3000,icon='Ability_Mount_WhiteDireWolf'}, - ['Swift Green Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Swift Mistsaber']={t=3000,icon='Ability_Mount_BlackPanther'}, - ['Swift Olive Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Swift Orange Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Swift Palomino']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Swift Razzashi Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Swift Stormsaber']={t=3000,icon='Ability_Mount_BlackPanther'}, - ['Swift Timber Wolf']={t=3000,icon='Ability_Mount_WhiteDireWolf'}, - ['Swift White Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Swift White Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['Swift White Steed']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Swift Yellow Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Swift Zulian Tiger']={t=3000,icon='Ability_Mount_JungleTiger'}, - ['Symbol of Life']={t=10000,icon='Spell_Holy_Resurrection'}, - ['Syndicate Bomb']={t=3000,icon='Spell_Shadow_MindBomb'}, - ['Syndicate Disguise']={t=3000,icon='Ability_Rogue_Disguise'}, - ['Syndicate Tracker (MURP) DND']={t=1000,icon='Spell_Arcane_Blink'}, - ['Taelan Death']={t=2000,icon='Temp'}, - ['Talvash\'s Necklace Repair']={t=4500,icon='Spell_Holy_Restoration'}, - ['Tammra Sapling']={t=1300,icon='Temp'}, - ['Tawny Sabercat']={t=3000,icon='Ability_Mount_JungleTiger'}, - ['Teach Bark of Doom']={t=1000,icon='Temp'}, - ['Teal Kodo']={t=3000,icon='Ability_Mount_Kodo_02'}, - ['Teleport']={t=1000,icon='Spell_Magic_LesserInvisibilty'}, - ['Teleport Altar of the Tides']={t=2000,icon='Temp'}, - ['Teleport Anvilmar']={t=2000,icon='Temp'}, - ['Teleport Barracks']={t=2000,icon='Temp'}, - ['Teleport Cemetary']={t=2000,icon='Temp'}, - ['Teleport Darkshire']={t=2000,icon='Temp'}, - ['Teleport Duskwood']={t=2000,icon='Temp'}, - ['Teleport Elwynn']={t=2000,icon='Temp'}, - ['Teleport Goldshire']={t=2000,icon='Temp'}, - ['Teleport Lighthouse']={t=2000,icon='Temp'}, - ['Teleport Monastery']={t=2000,icon='Temp'}, - ['Teleport Moonbrook']={t=2000,icon='Temp'}, - ['Teleport Northshire Abbey']={t=2000,icon='Temp'}, - ['Teleport Treant']={t=2000,icon='Temp'}, - ['Teleport Westfall']={t=2000,icon='Temp'}, - ['Teleport from Azshara Tower']={t=1000,icon='Spell_Nature_EarthBind'}, - ['Teleport to Azshara Tower']={t=1000,icon='Spell_Nature_AstralRecalGroup'}, - ['Teleport to Darnassus - Event']={t=1000,icon='Temp'}, - ['Teleport: Darnassus']={t=10000,icon='Spell_Arcane_TeleportDarnassus'}, - ['Teleport: Ironforge']={t=10000,icon='Spell_Arcane_TeleportIronForge'}, - ['Teleport: Moonglade']={t=10000,icon='Spell_Arcane_TeleportMoonglade'}, - ['Teleport: Orgrimmar']={t=10000,icon='Spell_Arcane_TeleportOrgrimmar'}, - ['Teleport: Stormwind']={t=10000,icon='Spell_Arcane_TeleportStormWind'}, - ['Teleport: Thunder Bluff']={t=10000,icon='Spell_Arcane_TeleportThunderBluff'}, - ['Teleport: Undercity']={t=10000,icon='Spell_Arcane_TeleportUnderCity'}, - ['Tell Joke']={t=2000,icon='Spell_Shadow_LifeDrain'}, - ['Temperature Reading']={t=2000,icon='Temp'}, - ['Test Sharpen Blade']={t=3000,icon='Temp'}, - ['Test of Lore']={t=2000,icon='Spell_Nature_EarthBind'}, - ['Tharnariun Cure 1']={t=750,icon='Spell_Nature_RemoveDisease'}, - ['Tharnariun\'s Heal']={t=500,icon='Spell_Nature_MagicImmunity'}, - ['The Big One']={t=3000,icon='Spell_Fire_SelfDestruct'}, - ['Thorium Grenade']={t=1000,icon='Spell_Fire_SelfDestruct'}, - ['Thousand Blades']={t=250,icon='INV-Sword_53'}, - ['Threatening Gaze']={t=2000,icon='Spell_Shadow_Charm'}, - ['Threatening Growl']={t=1000,icon='Ability_Racial_Cannibalize'}, - ['Throw Axe']={t=1000,icon='INV_Axe_08'}, - ['Throw Cupid\'s Dart']={t=1000,icon='Temp'}, - ['Throw Dark Iron Ale']={t=500,icon='Temp'}, - ['Throw Dynamite']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Throw Nightmare Object']={t=2000,icon='Spell_Fire_SelfDestruct'}, - ['Throw Rock']={t=3000,icon='Ability_GolemStormBolt'}, - ['Throw Rock II']={t=3000,icon='Ability_GolemStormBolt'}, - ['Tiger']={t=3000,icon='Ability_Mount_JungleTiger'}, - ['Time Lapse']={t=2000,icon='Spell_Arcane_PortalOrgrimmar'}, - ['Time Stop']={t=3000,icon='Temp'}, - ['Time Stop Visual DND']={t=3000,icon='Temp'}, - ['Toasted Smorc']={t=1000,icon='INV_SummerFest_Smorc'}, - ['Torch Combine']={t=5000,icon='Temp'}, - ['Torch Toss']={t=3000,icon='Spell_Fire_Fireball02'}, - ['Toss Fuel on Bonfire']={t=500,icon='Temp'}, - ['Toss Stink Bomb']={t=2000,icon='INV_Misc_Bowl_01'}, - ['Touch of Death']={t=3000,icon='Spell_Shadow_UnsummonBuilding'}, - ['Touch of Ravenclaw']={t=1500,icon='Spell_Shadow_Requiem'}, - ['Toxic Bolt']={t=2500,icon='Spell_Nature_CorrosiveBreath'}, - ['Toxic Saliva']={t=500,icon='Spell_Nature_CorrosiveBreath'}, - ['Toxic Spit']={t=2500,icon='Spell_Nature_CorrosiveBreath'}, - ['Transform Victim']={t=2000,icon='Spell_Magic_LesserInvisibilty'}, - ['Trelane\'s Freezing Touch']={t=3000,icon='Spell_Shadow_UnsummonBuilding'}, - ['Triage']={t=7000,icon='Temp'}, - ['True Fulfillment']={t=500,icon='Spell_Shadow_Charm'}, - ['Truesilver Skeleton Key']={t=5000,icon='Temp'}, - ['Tune Up']={t=7000,icon='INV_Gizmo_02'}, - ['Turn Undead']={t=1500,icon='Spell_Holy_TurnUndead'}, - ['Turquoise Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Uldaman Boss Agro']={t=5000,icon='Spell_Nature_EarthBindTotem'}, - ['Uldaman Key Staff']={t=5000,icon='Temp'}, - ['Uldaman Sub-Boss Agro']={t=5000,icon='Spell_Nature_EarthBindTotem'}, - ['Unholy Curse']={t=1000,icon='Spell_Shadow_CurseOfMannoroth'}, - ['Unlock Maury\'s Foot']={t=5000,icon='Temp'}, - ['Unlocking']={t=5000,icon='Temp'}, - ['Unpainted Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['Unstable Concoction']={t=3000,icon='Spell_Fire_Incinerate'}, - ['Urok Minions Vanish']={t=2000,icon='Temp'}, - ['Use Bauble']={t=3000,icon='Temp'}, - ['Uther\'s Tribute']={t=2000,icon='Temp'}, - ['Veil of Shadow']={t=1500,icon='Spell_Shadow_GatherShadows'}, - ['Venom Spit']={t=2500,icon='Spell_Nature_CorrosiveBreath'}, - ['Venom\'s Bane']={t=2000,icon='Temp'}, - ['Very Berry Cream']={t=1000,icon='INV_ValentinesChocolate02'}, - ['Viewing Room Student Transform - Effect']={t=1000,icon='Temp'}, - ['Violet Raptor']={t=3000,icon='Ability_Mount_Raptor'}, - ['Vipore Cat Form DND']={t=1000,icon='Temp'}, - ['Void Bolt']={t=3000,icon='Spell_Shadow_ShadowBolt'}, - ['Voidwalker Guardian']={t=3000,icon='Spell_Shadow_SummonVoidWalker'}, - ['Volatile Infection']={t=2000,icon='Spell_Holy_HarmUndeadAura'}, - ['Volley']={t=3000,icon='Ability_TheBlackArrow'}, - ['Volley II']={t=3000,icon='Ability_TheBlackArrow'}, - ['Voodoo']={t=1000,icon='Spell_Shadow_AntiShadow'}, - ['Voodoo Hex']={t=1000,icon='Spell_Shadow_CurseOfMannoroth'}, - ['Wail of the Banshee']={t=2000,icon='Spell_Shadow_Possession'}, - ['Wandering Plague']={t=2000,icon='Spell_Shadow_CallofBone'}, - ['War Stomp']={t=500,icon='Ability_WarStomp'}, - ['Warosh\'s Transform']={t=1000,icon='Temp'}, - ['Water Bubble']={t=1000,icon='Spell_Frost_Wisp'}, - ['Weak Frostbolt']={t=2200,icon='Spell_Frost_FrostBolt02'}, - ['Whirling Barrage']={t=1500,icon='INV_Spear_05'}, - ['White Mechanostrider']={t=3000,icon='Ability_Mount_MechaStrider'}, - ['White Ram']={t=3000,icon='Ability_Mount_MountainRam'}, - ['White Stallion']={t=3000,icon='Ability_Mount_RidingHorse'}, - ['Wide Sweep']={t=4000,icon='Ability_Whirlwind'}, - ['Widow\'s Embrace']={t=500,icon='Spell_Arcane_Blink'}, - ['Wild Regeneration']={t=3000,icon='Spell_Nature_Rejuvenation'}, - ['Will of Shahram']={t=1000,icon='Spell_Holy_MindVision'}, - ['Windsor Death DND']={t=1500,icon='Temp'}, - ['Windsor Reading Tablets DND']={t=10000,icon='Temp'}, - ['Wing Buffet']={t=1000,icon='INV_Misc_MonsterScales_14'}, - ['Winter Wolf']={t=3000,icon='Ability_Mount_WhiteDireWolf'}, - ['Winterax Wisdom']={t=1000,icon='Ability_Ambush'}, - ['Winterspring Frostsaber']={t=3000,icon='Ability_Mount_PinkTiger'}, - ['Wither']={t=1500,icon='Spell_Nature_NullifyDisease'}, - ['Wither Touch']={t=2000,icon='Spell_Nature_Drowsy'}, - ['Wizard Oil']={t=3000,icon='Temp'}, - ['Word of Thawing']={t=5000,icon='Temp'}, - ['Worm Sweep']={t=1000,icon='INV_Misc_MonsterScales_05'}, - ['Wound Poison']={t=3000,icon='INV_Misc_Herb_16'}, - ['Wrath']={t=2000,icon='Spell_Nature_AbolishMagic'}, - ['Yenniku\'s Release']={t=4000,icon='Spell_Shadow_LifeDrain'}, - ['[PH] Buttress Activator']={t=5000,icon='Temp'}, - ['[PH] Cystal Bazooka']={t=1000,icon='Temp'}, - ['[PH] Teleport to Auberdine']={t=2000,icon='Temp'}, - ['[PH] Teleport to Balthule']={t=2000,icon='Temp'}, - ['[PH] Teleport to Booty Bay']={t=2000,icon='Temp'}, - ['[PH] Teleport to Felwood']={t=2000,icon='Temp'}, - ['[PH] Teleport to Grom\'Gol']={t=2000,icon='Temp'}, - ['[PH] Teleport to Menethil Harbor']={t=2000,icon='Temp'}, - ['[PH] Teleport to Orgrimmar']={t=2000,icon='Temp'}, - ['[PH] Teleport to Ratchet']={t=2000,icon='Temp'}, - ['[PH] Teleport to Theramore']={t=2000,icon='Temp'}, - ['[PH] Teleport to Undercity']={t=2000,icon='Temp'}, -} - -ShaguTweaks_locale["enUS"]["debuffs"] = { - ['AE Charm']={[0]=300.0,}, - ['Abomination Spit']={[0]=10.0,}, - ['Acid Slime']={[0]=30.0,}, - ['Acid Spit']={[0]=30.0,}, - ['Acid Splash']={[0]=30.0,}, - ['Acid Spray']={[0]=10.0,}, - ['Acid Volley']={[0]=25.0,}, - ['Acid of Hakkar']={[0]=60.0,}, - ['Advanced Target Dummy Spawn Effect']={[0]=10.0,}, - ['Aftermath']={[0]=5.0,}, - ['Agility VIII']={[0]=3600.0,}, - ['Agonizing Pain']={[0]=15.0,}, - ['Air Bubbles']={[0]=10.0,}, - ['Amplify Damage']={[0]=10.0,}, - ['Amplify Flames']={[0]=30.0,}, - ['Ancient Despair']={[0]=5.0,}, - ['Ancient Dread']={[0]=900.0,}, - ['Ancient Hysteria']={[0]=900.0,}, - ['Anub\'Rekhan\'s Aura']={[0]=5.0,}, - ['Aquatic Miasma']={[0]=3600.0,}, - ['Arantir\'s Rage']={[0]=6.0,}, - ['Arcane Burst']={[0]=8.0,}, - ['Arcane Intellect']={[0]=1800.0,}, - ['Arcane Missiles']={[1]=3.0,[2]=4.0,[3]=5.0,[4]=5.0,[5]=5.0,[6]=5.0,[7]=5.0,[8]=5.0,[0]=5.0,}, - ['Arcane Weakness']={[0]=45.0,}, - ['Armor IV']={[0]=3600.0,}, - ['Armor Shatter']={[0]=45.0,}, - ['Arugal\'s Curse']={[0]=10.0,}, - ['Arugal\'s Gift']={[0]=300.0,}, - ['Aspect of Arlokk']={[0]=2.0,}, - ['Aspect of Jeklik']={[0]=5.0,}, - ['Aspect of Mar\'li']={[0]=6.0,}, - ['Aspect of Venoxis']={[0]=10.0,}, - ['Atal\'ai Poison']={[0]=30.0,}, - ['Attack Order']={[0]=10.0,}, - ['Attract Rager']={[0]=2.0,}, - ['Aura Leech (PT)']={[0]=15.0,}, - ['Aura of Agony']={[0]=8.0,}, - ['Aura of Battle']={[0]=10.0,}, - ['Aura of Command']={[0]=30.0,}, - ['Aura of Fear']={[0]=3.0,}, - ['Aural Shock']={[0]=300.0,}, - ['Awaken Earthen Guardians']={[0]=5.0,}, - ['Axe Flurry']={[0]=2.0,}, - ['Axe Toss']={[0]=3.0,}, - ['Azrethoc\'s Stomp']={[0]=5.0,}, - ['Backhand']={[0]=2.0,}, - ['Balnazzar Transform Stun']={[0]=5.0,}, - ['Baneful Poison']={[0]=120.0,}, - ['Banish']={[1]=20.0,[2]=30.0,[0]=30.0,}, - ['Banshee Curse']={[0]=12.0,}, - ['Banshee Shriek']={[0]=5.0,}, - ['Barbed Sting']={[0]=300.0,}, - ['Barrel Explode']={[0]=3.0,}, - ['Bash']={[1]=2.0,[2]=3.0,[3]=4.0,[0]=4.0,}, - ['Battle Command']={[0]=6.0,}, - ['Battle Net']={[0]=10.0,}, - ['Battle Shout']={[0]=900.0,}, - ['Battle Standard']={[0]=3.0,}, - ['Battlegear of Might']={[0]=6.0,}, - ['Befuddlement']={[0]=15.0,}, - ['Bellowing Roar']={[0]=3.0,}, - ['Berserk']={[0]=30.0,}, - ['Biletoad Infection']={[0]=180.0,}, - ['Black Arrow']={[0]=30.0,}, - ['Black March Blessing']={[0]=6.0,}, - ['Black Rot']={[0]=1800.0,}, - ['Black Sludge']={[0]=120.0,}, - ['Blackout']={[0]=3.0,}, - ['Blast Wave']={[0]=6.0,}, - ['Blaze']={[0]=30.0,}, - ['Bleakwood Curse']={[0]=60.0,}, - ['Blessing of Nordrassil']={[0]=10.0,}, - ['Blight']={[0]=60.0,}, - ['Blind']={[0]=10.0,}, - ['Blink']={[0]=1.0,}, - ['Blood Howl']={[0]=15.0,}, - ['Blood Siphon']={[0]=8.0,}, - ['Bloodfang']={[0]=6.0,}, - ['Bloodlord\'s Aura']={[0]=5.0,}, - ['Bloodlust']={[0]=15.0,}, - ['Bloodpetal Poison']={[0]=30.0,}, - ['Boar Charge']={[0]=1.0,}, - ['Bone Smelt']={[0]=20.0,}, - ['Bottle of Poison']={[0]=30.0,}, - ['Boulder']={[0]=10.0,}, - ['Brain Damage']={[0]=30.0,}, - ['Brain Hacker']={[0]=30.0,}, - ['Breath of Sargeras']={[0]=90.0,}, - ['Brood Affliction: Black']={[0]=600.0,}, - ['Brood Affliction: Blue']={[0]=600.0,}, - ['Brood Affliction: Bronze']={[0]=600.0,}, - ['Brood Affliction: Green']={[0]=600.0,}, - ['Brood Affliction: Red']={[0]=600.0,}, - ['Brood Power: Blue']={[0]=6.0,}, - ['Brood Power: Bronze']={[0]=5.0,}, - ['Brood Power: Green']={[0]=6.0,}, - ['Brood Power: Red']={[0]=5.0,}, - ['Bruising Blow']={[0]=5.0,}, - ['Burning Adrenaline']={[0]=20.0,}, - ['Burning Winds']={[0]=8.0,}, - ['Cadaver Worms']={[0]=600.0,}, - ['Call of the Grave']={[0]=60.0,}, - ['Cantation of Manifestation']={[0]=600.0,}, - ['Capture Felhound Spirit']={[0]=9.0,}, - ['Capture Infernal Spirit']={[0]=9.0,}, - ['Capture Spirit']={[0]=9.0,}, - ['Capture Treant']={[0]=5.0,}, - ['Cause Insanity']={[0]=6.0,}, - ['Cauterizing Flames']={[0]=900.0,}, - ['Chains of Ice']={[1]=15.0,[2]=20.0,[0]=20.0,}, - ['Chains of Kel\'Thuzad']={[0]=20.0,}, - ['Challenging Roar']={[0]=6.0,}, - ['Challenging Shout']={[0]=6.0,}, - ['Charge']={[0]=4.0,}, - ['Charge Stun']={[0]=1.0,}, - ['Cheap Shot']={[0]=4.0,}, - ['Chemical Peel']={[0]=10.0,}, - ['Chill']={[0]=30.0,}, - ['Chill Nova']={[0]=10.0,}, - ['Chilled']={[0]=1.5,}, - ['Chilling Breath']={[0]=12.0,}, - ['Chilling Touch']={[0]=8.0,}, - ['Chromatic Mutation']={[0]=300.0,}, - ['Claw Grasp']={[0]=4.0,}, - ['Clear All Shackels']={[0]=1.0,}, - ['Cleave Armor']={[0]=15.0,}, - ['Clenched Pinchers']={[0]=15.0,}, - ['Cold Eye']={[0]=15.0,}, - ['Concussion Blow']={[0]=5.0,}, - ['Concussive Shot']={[0]=4.0,}, - ['Conflagration']={[0]=10.0,}, - ['Consume']={[0]=15.0,}, - ['Contagion of Rot']={[0]=240.0,}, - ['Control Machine']={[0]=60.0,}, - ['Corrosive Acid']={[0]=300.0,}, - ['Corrosive Acid Spit']={[0]=10.0,}, - ['Corrosive Ooze']={[0]=60.0,}, - ['Corrosive Poison']={[0]=30.0,}, - ['Corrosive Venom Spit']={[0]=10.0,}, - ['Corrupted Agility']={[0]=4.0,}, - ['Corrupted Fear']={[0]=2.0,}, - ['Corrupted Healing']={[0]=30.0,}, - ['Corrupted Intellect']={[0]=4.0,}, - ['Corrupted Stamina']={[0]=4.0,}, - ['Corrupted Strength']={[0]=4.0,}, - ['Corrupted Totems']={[0]=30.0,}, - ['Corruption']={[1]=12.0,[2]=15.0,[3]=18.0,[4]=18.0,[5]=18.0,[6]=18.0,[7]=18.0,[0]=18.0,}, - ['Corruption of the Earth']={[0]=10.0,}, - ['Counterattack']={[0]=5.0,}, - ['Counterspell']={[0]=10.0,}, - ['Counterspell - Silenced']={[0]=4.0,}, - ['Cowering Roar']={[0]=5.0,}, - ['Cozy Fire']={[0]=60.0,}, - ['Crash of Waves']={[0]=10.0,}, - ['Create Heart of Hakkar Explosion']={[0]=30.0,}, - ['Create Heart of Hakkar Rift']={[0]=30.0,}, - ['Create Heart of Hakkar Summon Circle']={[0]=30.0,}, - ['Create Zul\'s Aura']={[0]=30.0,}, - ['Creature of Nightmare']={[0]=30.0,}, - ['Creeper Venom']={[0]=300.0,}, - ['Creeping Mold']={[0]=60.0,}, - ['Creeping Plague']={[0]=20.0,}, - ['Cripple']={[0]=20.0,}, - ['Crippling Poison']={[0]=12.0,}, - ['Crowd Pummel']={[0]=5.0,}, - ['Crusader Strike']={[0]=30.0,}, - ['Crusader\'s Hammer']={[0]=4.0,}, - ['Crush Armor']={[0]=30.0,}, - ['Crystal Gaze']={[0]=6.0,}, - ['Crystal Yield']={[0]=120.0,}, - ['Crystalline Slumber']={[0]=15.0,}, - ['Curse of Agony']={[0]=24.0,}, - ['Curse of Blood']={[0]=600.0,}, - ['Curse of Doom']={[0]=60.0,}, - ['Curse of Exhaustion']={[0]=12.0,}, - ['Curse of Hakkar']={[0]=120.0,}, - ['Curse of Idiocy']={[0]=120.0,}, - ['Curse of Impotence']={[0]=120.0,}, - ['Curse of Mending']={[0]=180.0,}, - ['Curse of Recklessness']={[0]=120.0,}, - ['Curse of Shadow']={[0]=300.0,}, - ['Curse of Stalvan']={[0]=600.0,}, - ['Curse of Thorns']={[0]=180.0,}, - ['Curse of Thule']={[0]=240.0,}, - ['Curse of Timmy']={[0]=60.0,}, - ['Curse of Tongues']={[0]=30.0,}, - ['Curse of Tuten\'kash']={[0]=900.0,}, - ['Curse of Vengeance']={[0]=900.0,}, - ['Curse of Weakness']={[0]=120.0,}, - ['Curse of the Bleakheart']={[0]=180.0,}, - ['Curse of the Darkmaster']={[0]=60.0,}, - ['Curse of the Deadwood']={[0]=120.0,}, - ['Curse of the Dreadmaul']={[0]=60.0,}, - ['Curse of the Elemental Lord']={[0]=60.0,}, - ['Curse of the Elements']={[0]=300.0,}, - ['Curse of the Eye']={[0]=120.0,}, - ['Curse of the Fallen Magram']={[0]=900.0,}, - ['Curse of the Firebrand']={[0]=300.0,}, - ['Curse of the Plague Rat']={[0]=14.0,}, - ['Curse of the Plaguebringer']={[0]=10.0,}, - ['Curse of the Shadowhorn']={[0]=300.0,}, - ['Curse of the Tribes']={[0]=1800.0,}, - ['Cursed Blade']={[0]=20.0,}, - ['Cursed Blood']={[0]=600.0,}, - ['Damage Shield']={[0]=10.0,}, - ['Dark Energy']={[0]=300.0,}, - ['Dark Iron Taskmaster Death']={[0]=6.0,}, - ['Dark Plague']={[0]=90.0,}, - ['Dark Sludge']={[0]=300.0,}, - ['Darken Vision']={[0]=12.0,}, - ['Daunting Growl']={[0]=30.0,}, - ['Dazed']={[0]=4.0,}, - ['Deadly Leech Poison']={[0]=45.0,}, - ['Deadly Poison']={[0]=12.0,}, - ['Deadly Poison II']={[0]=12.0,}, - ['Deadly Poison III']={[0]=12.0,}, - ['Deadly Poison IV']={[0]=12.0,}, - ['Deadly Poison V']={[0]=12.0,}, - ['Deadly Toxin']={[0]=12.0,}, - ['Deadly Toxin II']={[0]=12.0,}, - ['Deadly Toxin III']={[0]=12.0,}, - ['Deadly Toxin IV']={[0]=12.0,}, - ['Deafening Screech']={[0]=8.0,}, - ['Death Bed']={[0]=10.0,}, - ['Death Coil']={[0]=3.0,}, - ['Debilitate']={[0]=15.0,}, - ['Debilitating Charge']={[0]=8.0,}, - ['Debilitating Touch']={[0]=120.0,}, - ['Decayed Agility']={[0]=300.0,}, - ['Decayed Strength']={[0]=300.0,}, - ['Decimate']={[0]=3.0,}, - ['Decrepit Fever']={[0]=21.0,}, - ['Deep Sleep']={[0]=10.0,}, - ['Deep Slumber']={[0]=15.0,}, - ['Deep Wound']={[0]=12.0,}, - ['Defile']={[0]=10.0,}, - ['Defiling Aura']={[0]=5.0,}, - ['Delusions of Jin\'do']={[0]=20.0,}, - ['Demonfork']={[0]=25.0,}, - ['Demoralize']={[0]=30.0,}, - ['Demoralizing Roar']={[0]=30.0,}, - ['Demoralizing Shout']={[0]=30.0,}, - ['Detect Greater Invisibility']={[0]=600.0,}, - ['Detect Magic']={[0]=120.0,}, - ['Detonate Mana']={[0]=5.0,}, - ['Devilsaur Barb']={[0]=10.0,}, - ['Devouring Plague']={[0]=24.0,}, - ['Dire Growl']={[0]=15.0,}, - ['Dirk\'s Rememberin\' Poison']={[0]=30.0,}, - ['Disarm']={[0]=10.0,}, - ['Discombobulate']={[0]=12.0,}, - ['Disease Buffet']={[0]=20.0,}, - ['Diseased Shot']={[0]=300.0,}, - ['Diseased Slime']={[0]=120.0,}, - ['Diseased Spit']={[0]=10.0,}, - ['Disjunction']={[0]=300.0,}, - ['Dismember']={[0]=10.0,}, - ['Dismounting Shot']={[0]=2.0,}, - ['Dissolve Armor']={[0]=20.0,}, - ['Distract Move']={[0]=7.0,}, - ['Distracting Pain']={[0]=15.0,}, - ['Distracting Spit']={[0]=15.0,}, - ['Divine Spirit']={[0]=1800.0,}, - ['Dominate Mind']={[0]=10.0,}, - ['Domination']={[0]=15.0,}, - ['Dominion of Soul']={[0]=60.0,}, - ['Drain Life']={[0]=5.0,}, - ['Drain Mana']={[0]=5.0,}, - ['Drain Soul']={[0]=15.0,}, - ['Draw Spirit']={[0]=5.0,}, - ['Draw of Thistlenettle']={[0]=8.0,}, - ['Dreadful Fright']={[0]=5.0,}, - ['Dredge Sickness']={[0]=300.0,}, - ['Drowning Death']={[0]=300.0,}, - ['Druid\'s Slumber']={[0]=15.0,}, - ['Dust Cloud']={[0]=12.0,}, - ['Eagle Claw']={[0]=15.0,}, - ['Earth Shock']={[0]=2.0,}, - ['Earthbind']={[0]=5.0,}, - ['Earthborer Acid']={[0]=30.0,}, - ['Earthgrab']={[0]=4.0,}, - ['Echoing Roar']={[0]=20.0,}, - ['Ectoplasmic Distiller']={[0]=3.0,}, - ['Egg Explosion']={[0]=3.0,}, - ['Electrified Net']={[0]=10.0,}, - ['Elemental Fire']={[0]=8.0,}, - ['Elemental Vulnerability']={[0]=30.0,}, - ['Elixir of the Giants']={[0]=1200.0,}, - ['Emeriss Aura']={[0]=10.0,}, - ['Encage']={[0]=30.0,}, - ['Encasing Webs']={[0]=6.0,}, - ['Enchanting Lullaby']={[0]=10.0,}, - ['Encouragement']={[0]=1800.0,}, - ['Enervate']={[0]=10.0,}, - ['Enfeeble']={[0]=120.0,}, - ['Engulfing Flames']={[0]=6.0,}, - ['Enraging Bite']={[0]=6.0,}, - ['Enslave']={[0]=15.0,}, - ['Enslave Demon']={[0]=300.0,}, - ['Entangle']={[0]=8.0,}, - ['Entangling Roots']={[1]=12.0,[2]=15.0,[3]=18.0,[4]=21.0,[5]=24.0,[6]=27.0,[0]=27.0,}, - ['Entrapment']={[0]=5.0,}, - ['Enveloping Web']={[0]=8.0,}, - ['Enveloping Webs']={[0]=8.0,}, - ['Enveloping Winds']={[0]=10.0,}, - ['Eskhandar\'s Rake']={[0]=30.0,}, - ['Essence of the Red']={[0]=180.0,}, - ['Explode']={[0]=2.5,}, - ['Explode Bug']={[0]=4.0,}, - ['Explosive Trap Effect']={[0]=20.0,}, - ['Expose Armor']={[0]=30.0,}, - ['Expose Weakness']={[0]=5.0,}, - ['Extract Essence']={[0]=12.0,}, - ['Eye Peck']={[0]=12.0,}, - ['Eye of Immol\'thar']={[0]=4.0,}, - ['Faerie Fire']={[0]=40.0,}, - ['Faerie Fire (Feral)']={[0]=40.0,}, - ['Fang of the Crystal Spider']={[0]=10.0,}, - ['Fatal Sting']={[0]=12.0,}, - ['Fear']={[1]=10.0,[2]=15.0,[3]=20.0,[0]=20.0,}, - ['Fear (NYI)']={[0]=15.0,}, - ['Feeblemind']={[0]=30.0,}, - ['Feeblemind II']={[0]=30.0,}, - ['Feeblemind III']={[0]=30.0,}, - ['Fel Stomp']={[0]=3.0,}, - ['Feral Charge Effect']={[0]=4.0,}, - ['Festering Bites']={[0]=1800.0,}, - ['Festering Rash']={[0]=1800.0,}, - ['Festival Fortune!']={[0]=1800.0,}, - ['Fevered Fatigue']={[0]=1800.0,}, - ['Fevered Plague']={[0]=180.0,}, - ['Fire Festival Fortitude']={[0]=3600.0,}, - ['Fire Festival Fury']={[0]=3600.0,}, - ['Fire Protection']={[0]=3600.0,}, - ['Fire Vulnerability']={[0]=30.0,}, - ['Fire Weakness']={[0]=45.0,}, - ['Fireball']={[1]=4.0,[2]=6.0,[3]=6.0,[4]=8.0,[5]=8.0,[6]=8.0,[7]=8.0,[8]=8.0,[9]=8.0,[10]=8.0,[11]=8.0,[12]=8.0,[0]=8.0,}, - ['Fist of Ragnaros']={[0]=5.0,}, - ['Five Fat Finger Exploding Heart Technique']={[0]=30.0,}, - ['Fixate']={[0]=10.0,}, - ['Flame Buffet']={[0]=45.0,}, - ['Flame Lash']={[0]=45.0,}, - ['Flame Shock']={[0]=12.0,}, - ['Flameshocker\'s Revenge']={[0]=2.0,}, - ['Flameshocker\'s Touch']={[0]=3.0,}, - ['Flash Freeze']={[0]=5.0,}, - ['Flesh Rot']={[0]=10.0,}, - ['Forsaken Skills']={[0]=300.0,}, - ['Foul Chill']={[0]=120.0,}, - ['Frailty']={[0]=60.0,}, - ['Freeze']={[0]=15.0,}, - ['Freeze III PROC']={[0]=5.0,}, - ['Freeze Solid']={[0]=10.0,}, - ['Freezing Claw']={[0]=5.0,}, - ['Freezing Trap Effect']={[1]=10.0,[2]=15.0,[3]=20.0,[0]=20.0,}, - ['Frenzied Command']={[0]=10.0,}, - ['Frenzied Dive']={[0]=2.0,}, - ['Frightalon']={[0]=60.0,}, - ['Frightening Shriek']={[0]=6.0,}, - ['Frost']={[0]=10.0,}, - ['Frost Aura']={[0]=5.0,}, - ['Frost Burn']={[0]=15.0,}, - ['Frost Hold']={[0]=10.0,}, - ['Frost Nova']={[0]=8.0,}, - ['Frost Protection ']={[0]=3600.0,}, - ['Frost Shock']={[0]=8.0,}, - ['Frost Shot']={[0]=10.0,}, - ['Frost Weakness']={[0]=45.0,}, - ['Frostbite']={[0]=5.0,}, - ['Frostbolt']={[1]=5.0,[2]=6.0,[3]=6.0,[4]=7.0,[5]=7.0,[6]=8.0,[7]=8.0,[8]=9.0,[9]=9.0,[10]=9.0,[11]=9.0,[0]=9.0,}, - ['Frostbolt Volley']={[0]=8.0,}, - ['Frostbrand Attack']={[0]=8.0,}, - ['Fumble']={[0]=30.0,}, - ['Fumble II']={[0]=30.0,}, - ['Fumble III']={[0]=30.0,}, - ['Fungal Bloom']={[0]=90.0,}, - ['Garrote']={[0]=18.0,}, - ['Gehennas\' Curse']={[0]=300.0,}, - ['Geyser']={[0]=5.0,}, - ['Ghostly Strike']={[0]=7.0,}, - ['Ghoul Plague']={[0]=1800.0,}, - ['Ghoul Rot']={[0]=600.0,}, - ['Gift of Arthas']={[0]=180.0,}, - ['Gizlock\'s Dummy Taunt Effect']={[0]=5.0,}, - ['Glacial Roar']={[0]=3.0,}, - ['Glimpse of Madness']={[0]=3.0,}, - ['Gnarlpine Vengeance']={[0]=6.0,}, - ['Gnomish Death Ray']={[0]=4.0,}, - ['Gnomish Mind Control Cap']={[0]=20.0,}, - ['Golemagg\'s Trust']={[0]=2.0,}, - ['Gore']={[0]=15.0,}, - ['Gouge']={[0]=4.0,}, - ['Grab Weapon']={[0]=15.0,}, - ['Grasping Vines']={[0]=10.0,}, - ['Greater Arcane Elixir']={[0]=1800.0,}, - ['Greater Polymorph']={[0]=20.0,}, - ['Grip of Command']={[0]=10.0,}, - ['Ground Smash']={[0]=3.0,}, - ['Ground Stomp']={[0]=5.0,}, - ['Ground Tremor']={[0]=2.0,}, - ['Growl']={[0]=3.0,}, - ['Gust of Wind']={[0]=4.0,}, - ['Gutgore Ripper']={[0]=30.0,}, - ['Hail Storm']={[0]=3.0,}, - ['Hallow\'s End Fright']={[0]=6.0,}, - ['Hammer of Justice']={[1]=3.0,[2]=4.0,[3]=5.0,[4]=6.0,[0]=6.0,}, - ['Hamstring']={[0]=15.0,}, - ['Hand Snap']={[0]=8.0,}, - ['Hand of Ragnaros']={[0]=2.0,}, - ['Hand of Thaurissan']={[0]=5.0,}, - ['Harass']={[1]=10.0,[2]=20.0,[3]=30.0,[0]=30.0,}, - ['Harsh Winds']={[0]=1.0,}, - ['Harvest Soul']={[0]=60.0,}, - ['Hate to Zero']={[0]=1.7,}, - ['Haunting Phantoms']={[0]=300.0,}, - ['Haunting Spirits']={[0]=300.0,}, - ['Head Butt']={[0]=2.0,}, - ['Head Crack']={[0]=20.0,}, - ['Head Smash']={[0]=2.0,}, - ['Health II']={[0]=3600.0,}, - ['Hemorrhage']={[0]=15.0,}, - ['Hex']={[0]=10.0,}, - ['Hex of Jammal\'an']={[0]=10.0,}, - ['Hex of Ravenclaw']={[0]=30.0,}, - ['Hex of Weakness']={[0]=120.0,}, - ['Hibernate']={[1]=20.0,[2]=30.0,[3]=40.0,[0]=40.0,}, - ['Highlord\'s Justice']={[0]=5.0,}, - ['Hive\'Zara Catalyst']={[0]=30.0,}, - ['Holy Blast']={[0]=4.0,}, - ['Holy Fire']={[0]=10.0,}, - ['Holy Protection ']={[0]=3600.0,}, - ['Holy Sunder']={[0]=60.0,}, - ['Holy Word: Fortitude']={[0]=1800.0,}, - ['Hooked Net']={[0]=10.0,}, - ['Howl of Terror']={[1]=10.0,[2]=15.0,[0]=15.0,}, - ['Howling Blade']={[0]=30.0,}, - ['Howling Rage']={[0]=300.0,}, - ['Hunter Epic Anti-Cheat DND']={[0]=60.0,}, - ['Hunter\'s Mark']={[0]=120.0,}, - ['Ice Blast']={[0]=10.0,}, - ['Ice Bolt Prot']={[0]=5.0,}, - ['Ice Claw']={[0]=6.0,}, - ['Ice Nova']={[0]=2.0,}, - ['Ice Tomb']={[0]=10.0,}, - ['Icebolt']={[0]=2.0,}, - ['Icicle']={[0]=10.0,}, - ['Icy Grasp']={[0]=5.0,}, - ['Ignite']={[0]=4.0,}, - ['Ignite Flesh']={[0]=60.0,}, - ['Ignite Mana']={[0]=300.0,}, - ['Immolate']={[0]=15.0,}, - ['Immolation Trap Effect']={[0]=15.0,}, - ['Immunity']={[0]=1800.0,}, - ['Impact']={[0]=2.0,}, - ['Impale']={[0]=9.0,}, - ['Impending Doom']={[0]=10.0,}, - ['Improved Concussive Shot']={[0]=3.0,}, - ['Improved Hamstring']={[0]=5.0,}, - ['Improved Scorpid Sting']={[0]=20.0,}, - ['Improved Wing Clip']={[0]=5.0,}, - ['Incapacitating Shout']={[0]=60.0,}, - ['Incinerate']={[0]=60.0,}, - ['Incite Flames']={[0]=60.0,}, - ['Inevitable Doom']={[0]=10.0,}, - ['Infected Bite']={[0]=180.0,}, - ['Infected Spine']={[0]=300.0,}, - ['Infected Wound']={[0]=300.0,}, - ['Inferno Effect']={[0]=2.0,}, - ['Inferno Shell']={[0]=10.0,}, - ['Ink Spray']={[0]=15.0,}, - ['Insect Swarm']={[0]=12.0,}, - ['Intellect IX']={[0]=3600.0,}, - ['Intercept Stun']={[0]=3.0,}, - ['Interrupt (PT)']={[0]=30.0,}, - ['Intimidating Growl']={[0]=5.0,}, - ['Intimidating Roar']={[0]=8.0,}, - ['Intimidating Shout']={[0]=8.0,}, - ['Intimidation']={[0]=3.0,}, - ['Intoxicating Venom']={[0]=120.0,}, - ['Involuntary Transformation']={[0]=30.0,}, - ['Irradiated']={[0]=60.0,}, - ['Itch']={[0]=8.0,}, - ['Jeff Dummy 1']={[0]=30.0,}, - ['Jeff Dummy 2']={[0]=30.0,}, - ['Judge\'s Gavel']={[0]=10.0,}, - ['Judgement of Justice']={[0]=10.0,}, - ['Judgement of Light']={[0]=10.0,}, - ['Judgement of Wisdom']={[0]=10.0,}, - ['Judgement of the Crusader']={[0]=10.0,}, - ['Keg Trap']={[0]=2.0,}, - ['Kick']={[0]=5.0,}, - ['Kick - Silenced']={[0]=2.0,}, - ['Kidney Shot']={[0]=1.0,}, - ['Knockdown']={[0]=2.0,}, - ['Knockout']={[0]=6.0,}, - ['Kodo Stomp']={[0]=3.0,}, - ['Lacerate']={[0]=8.0,}, - ['Lacerations']={[0]=60.0,}, - ['Lag']={[0]=10.0,}, - ['Larva Goo']={[0]=6.0,}, - ['Lash']={[0]=2.0,}, - ['Leech Poison']={[0]=40.0,}, - ['Lethal Toxin']={[0]=180.0,}, - ['Life Drain']={[0]=12.0,}, - ['Lightwell']={[0]=180.0,}, - ['Living Bomb']={[0]=8.0,}, - ['Localized Toxin']={[0]=60.0,}, - ['Locust Swarm']={[0]=6.0,}, - ['Long Daze']={[0]=6.0,}, - ['Low Swipe']={[0]=12.0,}, - ['Lucifron\'s Curse']={[0]=300.0,}, - ['Lunaclaw Spirit']={[0]=120.0,}, - ['Lunar Fortune']={[0]=1800.0,}, - ['Mace Stun Effect']={[0]=3.0,}, - ['Magenta Cap Sickness']={[0]=1200.0,}, - ['Maggot Goo']={[0]=6.0,}, - ['Maggot Slime']={[0]=1800.0,}, - ['Magic Reflection']={[0]=10.0,}, - ['Magma Shackles']={[0]=15.0,}, - ['Magma Spit']={[0]=30.0,}, - ['Magma Splash']={[0]=30.0,}, - ['Malown\'s Slam']={[0]=2.0,}, - ['Mana Burn']={[0]=8.0,}, - ['Mangle']={[0]=2.0,}, - ['Marduk\'s Curse']={[0]=5.0,}, - ['Mark of Arlokk']={[0]=120.0,}, - ['Mark of Blaumeux']={[0]=75.0,}, - ['Mark of Detonation']={[0]=30.0,}, - ['Mark of Flames']={[0]=120.0,}, - ['Mark of Frost']={[0]=12.0,}, - ['Mark of Kazzak']={[0]=60.0,}, - ['Mark of Korth\'azz']={[0]=75.0,}, - ['Mark of Mograine']={[0]=75.0,}, - ['Mark of Nature']={[0]=12.0,}, - ['Mark of Zeliek']={[0]=75.0,}, - ['Mark of the Wild']={[0]=1800.0,}, - ['Mass Healing']={[0]=12.0,}, - ['Massive Tremor']={[0]=2.0,}, - ['Maul']={[0]=2.0,}, - ['Melt Armor']={[0]=60.0,}, - ['Melt Ore']={[0]=20.0,}, - ['Mental Domination']={[0]=120.0,}, - ['Might of Shahram']={[0]=5.0,}, - ['Mind Control']={[0]=60.0,}, - ['Mind Flay']={[0]=3.0,}, - ['Mind Rot']={[0]=30.0,}, - ['Mind Shatter']={[0]=3.0,}, - ['Mind Soothe']={[0]=15.0,}, - ['Mind Tremor']={[0]=600.0,}, - ['Mind-numbing Poison']={[0]=10.0,}, - ['Mind-numbing Poison II']={[0]=12.0,}, - ['Mind-numbing Poison III']={[0]=14.0,}, - ['Minor Scorpion Venom Effect']={[0]=60.0,}, - ['Mirefin Fungus']={[0]=8.0,}, - ['Miring Mud']={[0]=5.0,}, - ['Mirkfallon Fungus']={[0]=2700.0,}, - ['Mobility Malfunction']={[0]=20.0,}, - ['Mocking Blow']={[0]=6.0,}, - ['Molten Metal']={[0]=15.0,}, - ['Moonfire']={[1]=9.0,[2]=12.0,[3]=12.0,[4]=12.0,[5]=12.0,[6]=12.0,[7]=12.0,[8]=12.0,[9]=12.0,[10]=12.0,[0]=12.0,}, - ['Mortal Cleave']={[0]=5.0,}, - ['Mortal Strike']={[0]=10.0,}, - ['Mortal Wound']={[0]=15.0,}, - ['Moss Covered Feet']={[0]=180.0,}, - ['Moss Covered Hands']={[0]=180.0,}, - ['Muculent Fever']={[0]=600.0,}, - ['Muscle Tear']={[0]=5.0,}, - ['Mutate Bug']={[0]=240.0,}, - ['Mutating Injection']={[0]=10.0,}, - ['Naralex\'s Nightmare']={[0]=15.0,}, - ['Naraxis Web']={[0]=30.0,}, - ['Nature Protection ']={[0]=3600.0,}, - ['Nature Weakness']={[0]=45.0,}, - ['Necrotic Poison']={[0]=30.0,}, - ['Net']={[0]=10.0,}, - ['Net Guard']={[0]=20.0,}, - ['Net-o-Matic']={[0]=10.0,}, - ['Noxious Catalyst']={[0]=120.0,}, - ['Nullify']={[0]=8.0,}, - ['Numbing Pain']={[0]=10.0,}, - ['Oops!']={[0]=10.0,}, - ['Open Wound Effect']={[0]=8.0,}, - ['Overseer\'s Poison']={[0]=60.0,}, - ['Overwhelming Stench']={[0]=6.0,}, - ['Pacify']={[0]=10.0,}, - ['Panic']={[0]=8.0,}, - ['Paralyze']={[0]=30.0,}, - ['Paralyzing Poison']={[0]=8.0,}, - ['Parasite']={[0]=75.0,}, - ['Parasitic Serpent']={[0]=10.0,}, - ['Pester Effect']={[0]=3.0,}, - ['Petrify']={[0]=8.0,}, - ['Phantom Strike']={[0]=20.0,}, - ['Pierce Ankle']={[0]=3.0,}, - ['Pierce Armor']={[0]=20.0,}, - ['Piercing Howl']={[0]=6.0,}, - ['Piercing Shadow']={[0]=1800.0,}, - ['Piercing Shot']={[0]=15.0,}, - ['Piercing Shriek']={[0]=6.0,}, - ['Plague']={[0]=40.0,}, - ['Plague Cloud']={[0]=240.0,}, - ['Plague Mind']={[0]=600.0,}, - ['Plague Mist']={[0]=8.0,}, - ['Planted']={[0]=3.0,}, - ['Poison']={[0]=30.0,}, - ['Poison Aura']={[0]=12.0,}, - ['Poison Bolt']={[0]=10.0,}, - ['Poison Bolt Volley']={[0]=10.0,}, - ['Poison Charge']={[0]=9.0,}, - ['Poison Cloud']={[0]=45.0,}, - ['Poison Mind']={[0]=15.0,}, - ['Poison Stinger']={[0]=10.0,}, - ['Poisoned Harpoon']={[0]=60.0,}, - ['Poisoned Shot']={[0]=75.0,}, - ['Poisonous Spit']={[0]=15.0,}, - ['Poisonous Stab']={[0]=15.0,}, - ['Polymorph']={[1]=20.0,[2]=30.0,[3]=40.0,[4]=50.0,[0]=50.0,}, - ['Polymorph Backfire']={[0]=8.0,}, - ['Polymorph: Chicken']={[0]=10.0,}, - ['Polymorph: Cow']={[0]=50.0,}, - ['Polymorph: Pig']={[0]=50.0,}, - ['Polymorph: Sheep']={[0]=10.0,}, - ['Polymorph: Turtle']={[0]=50.0,}, - ['Polymorphic Ray']={[0]=4.0,}, - ['Possess']={[0]=120.0,}, - ['Pounce']={[0]=2.0,}, - ['Pounce Bleed']={[0]=18.0,}, - ['Premeditation']={[0]=10.0,}, - ['Psychic Scream']={[0]=8.0,}, - ['Pummel']={[0]=4.0,}, - ['Puncture']={[0]=10.0,}, - ['Puncture Armor']={[0]=30.0,}, - ['Purge']={[0]=2.0,}, - ['Putrid Bile']={[0]=45.0,}, - ['Putrid Bite']={[0]=30.0,}, - ['Putrid Breath']={[0]=30.0,}, - ['Putrid Enzyme']={[0]=300.0,}, - ['Putrid Stench']={[0]=10.0,}, - ['Pyroblast']={[0]=12.0,}, - ['Pyroclasm']={[0]=3.0,}, - ['Rabid Maw']={[0]=30.0,}, - ['Rabies']={[0]=600.0,}, - ['Radiation Poisoning']={[0]=25.0,}, - ['Rage of Thule']={[0]=120.0,}, - ['Rake']={[0]=9.0,}, - ['Rallying Cry of the Dragonslayer']={[0]=7200.0,}, - ['Rampage']={[0]=2.5,}, - ['Rat Nova']={[0]=10.0,}, - ['Ravage']={[0]=2.0,}, - ['Reckless Charge']={[0]=30.0,}, - ['Reflection Field']={[0]=5.0,}, - ['Regeneration IV']={[0]=3600.0,}, - ['Rend']={[1]=9.0,[2]=12.0,[3]=15.0,[4]=18.0,[5]=21.0,[6]=21.0,[7]=21.0,[0]=21.0,}, - ['Rend Flesh']={[0]=12.0,}, - ['Repentance']={[0]=6.0,}, - ['Repulsive Gaze']={[0]=8.0,}, - ['Retching Plague']={[0]=300.0,}, - ['Revenge Stun']={[0]=3.0,}, - ['Rhahk\'Zor Slam']={[0]=3.0,}, - ['Rift Beacon']={[0]=60.0,}, - ['Righteous Fire']={[0]=8.0,}, - ['Rip']={[0]=12.0,}, - ['Riposte']={[0]=6.0,}, - ['Riptide']={[0]=4.0,}, - ['Ritual Candle Aura']={[0]=6.0,}, - ['Rupture']={[0]=6.0,}, - ['Sacrifice']={[0]=8.0,}, - ['Sap']={[1]=25.0,[2]=35.0,[3]=45.0,[0]=45.0,}, - ['Sap Might']={[0]=300.0,}, - ['Savage Assault']={[0]=30.0,}, - ['Savage Assault II']={[0]=30.0,}, - ['Savage Assault III']={[0]=30.0,}, - ['Savage Assault IV']={[0]=30.0,}, - ['Savage Assault V']={[0]=30.0,}, - ['Savage Pummel']={[0]=5.0,}, - ['Scald']={[0]=4.0,}, - ['Scare Beast']={[1]=10.0,[2]=15.0,[3]=20.0,[0]=20.0,}, - ['Scatter Shot']={[0]=4.0,}, - ['Scorpid Poison']={[0]=10.0,}, - ['Scorpid Sting']={[0]=20.0,}, - ['Screams of the Past']={[0]=5.0,}, - ['Screech']={[0]=4.0,}, - ['Searing Blast']={[0]=30.0,}, - ['Searing Flames']={[0]=9.0,}, - ['Seduction']={[0]=15.0,}, - ['Seeping Willow']={[0]=30.0,}, - ['Serious Wound']={[0]=10.0,}, - ['Serpent Sting']={[0]=15.0,}, - ['Serrated Bite']={[0]=30.0,}, - ['Shackle Undead']={[1]=30.0,[2]=40.0,[3]=50.0,[0]=50.0,}, - ['Shadow Bolt']={[0]=6.0,}, - ['Shadow Command']={[0]=15.0,}, - ['Shadow Flame']={[0]=10.0,}, - ['Shadow Mark']={[0]=15.0,}, - ['Shadow Protection ']={[0]=3600.0,}, - ['Shadow Vulnerability']={[0]=15.0,}, - ['Shadow Weakness']={[0]=45.0,}, - ['Shadow Word: Pain']={[0]=18.0,}, - ['Shadow of Ebonroc']={[0]=8.0,}, - ['Shadowburn']={[0]=5.0,}, - ['Shadowstalker Slash']={[0]=5.0,}, - ['Shadowstalker Stab']={[0]=5.0,}, - ['Shared Bonds']={[0]=4.0,}, - ['Shazzrah\'s Curse']={[0]=300.0,}, - ['Shield Bash']={[0]=6.0,}, - ['Shield Bash - Silenced']={[0]=3.0,}, - ['Shield Slam']={[0]=2.0,}, - ['Shockwave']={[0]=2.0,}, - ['Shred']={[0]=12.0,}, - ['Shrink']={[0]=120.0,}, - ['Shrink Ray']={[0]=20.0,}, - ['Silence']={[0]=5.0,}, - ['Silithid Pox']={[0]=1800.0,}, - ['Siphon Blessing']={[0]=30.0,}, - ['Siphon Health']={[0]=15.0,}, - ['Siphon Life']={[0]=30.0,}, - ['Skull Crack']={[0]=2.0,}, - ['Skullforge Brand']={[0]=30.0,}, - ['Slam']={[0]=2.0,}, - ['Slavedriver\'s Cane']={[0]=30.0,}, - ['Sleep']={[1]=20.0,[2]=30.0,[0]=30.0,}, - ['Sleepwalk']={[0]=10.0,}, - ['Slice and Dice']={[0]=6.0,}, - ['Slime Bolt']={[0]=6.0,}, - ['Slime Burst']={[0]=5.0,}, - ['Slime Dysentery']={[0]=1800.0,}, - ['Slime Stream']={[0]=3.0,}, - ['Sling Dirt']={[0]=10.0,}, - ['Sling Mud']={[0]=15.0,}, - ['Sloth Effect']={[0]=3.0,}, - ['Slow']={[1]=10.0,[2]=15.0,[0]=15.0,}, - ['Slow Poison']={[0]=30.0,}, - ['Slow Poison II']={[0]=30.0,}, - ['Slowing Ooze']={[0]=10.0,}, - ['Slowing Poison']={[0]=25.0,}, - ['Sludge']={[0]=3.0,}, - ['Sludge Toxin']={[0]=45.0,}, - ['Smite Demon']={[0]=5.0,}, - ['Smite Slam']={[0]=3.0,}, - ['Smite Stomp']={[0]=10.0,}, - ['Smoke Bomb']={[0]=4.0,}, - ['Smoke Cloud']={[0]=3.0,}, - ['Smother']={[0]=6.0,}, - ['Snap Kick']={[0]=2.0,}, - ['Sol L']={[0]=5.0,}, - ['Sonic Burst']={[0]=10.0,}, - ['Soot Covering']={[0]=10.0,}, - ['Soothe Animal']={[0]=15.0,}, - ['Soul Breaker']={[0]=30.0,}, - ['Soul Burn']={[0]=16.0,}, - ['Soul Corruption']={[0]=15.0,}, - ['Soul Drain']={[0]=10.0,}, - ['Soul Revival']={[0]=1800.0,}, - ['Soul Siphon']={[0]=10.0,}, - ['Soul Tap']={[0]=12.0,}, - ['Spell Lock']={[1]=6.0,[2]=8.0,[0]=8.0,}, - ['Spell Vulnerability']={[0]=5.0,}, - ['Spider Poison']={[0]=30.0,}, - ['Spider Web']={[0]=1.0,}, - ['Spider\'s Kiss']={[0]=10.0,}, - ['Spirit Decay']={[0]=1200.0,}, - ['Spirit of Zandalar']={[0]=7200.0,}, - ['Spitelash']={[0]=20.0,}, - ['Starfire Stun']={[0]=3.0,}, - ['Starshards']={[0]=6.0,}, - ['Stasis Effect']={[0]=15.0,}, - ['Stasis Test']={[0]=5.0,}, - ['Static Conduit']={[0]=15.0,}, - ['Steam Jet']={[0]=10.0,}, - ['Sticky Tar']={[0]=4.0,}, - ['Stink Trap']={[0]=120.0,}, - ['Stomp']={[0]=10.0,}, - ['Storm Bolt']={[0]=8.0,}, - ['Stormbolt']={[0]=5.0,}, - ['Stormstrike']={[0]=12.0,}, - ['Strong Cleave']={[0]=10.0,}, - ['Stun']={[0]=2.0,}, - ['Stunning Blast']={[1]=3.0,[2]=5.0,[0]=5.0,}, - ['Stunning Blow']={[1]=5.0,[2]=8.0,[0]=8.0,}, - ['Stunning Strike']={[0]=4.0,}, - ['Stygian Grasp']={[0]=5.0,}, - ['Submersion']={[0]=60.0,}, - ['Sul\'thraze']={[0]=15.0,}, - ['Summon Darrowshire Spirit']={[0]=60.0,}, - ['Summon Finkle Generator']={[0]=5.0,}, - ['Summon Freed Soul']={[0]=60.0,}, - ['Summon Healed Celebrian Vine']={[0]=604800.0,}, - ['Summon Isalien DND']={[0]=600.0,}, - ['Summon Jarien and Sothos DND']={[0]=600.0,}, - ['Summon Knight']={[0]=180.0,}, - ['Summon Kormok DND']={[0]=600.0,}, - ['Summon Lord Valthalak DND']={[0]=600.0,}, - ['Summon Marduk the Black']={[0]=300.0,}, - ['Summon Mor Grayhoof DND']={[0]=600.0,}, - ['Summon Mounted Knight']={[0]=120.0,}, - ['Summon Screecher Spirit']={[0]=120.0,}, - ['Summon Thelrin DND']={[0]=600.0,}, - ['Summon Trainee']={[0]=230.0,}, - ['Summon Various DND']={[0]=600.0,}, - ['Sunder Armor']={[0]=30.0,}, - ['Sundering Cleave']={[0]=30.0,}, - ['Super Shrink Ray']={[0]=20.0,}, - ['Surprise Attack']={[0]=2.5,}, - ['Survival Instinct']={[0]=2.0,}, - ['TWEEP']={[0]=11.0,}, - ['Taelan\'s Suffering Effect']={[0]=2.0,}, - ['Tail Lash']={[0]=2.0,}, - ['Tainted Blood Effect']={[0]=10.0,}, - ['Tainted Mind']={[0]=600.0,}, - ['Tame Adult Plainstrider']={[0]=900.0,}, - ['Tame Armored Scorpid']={[0]=900.0,}, - ['Tame Beast']={[0]=20.0,}, - ['Tame Dire Mottled Boar']={[0]=900.0,}, - ['Tame Ice Claw Bear']={[0]=20.0,}, - ['Tame Large Crag Boar']={[0]=20.0,}, - ['Tame Nightsaber Stalker']={[0]=900.0,}, - ['Tame Prairie Stalker']={[0]=900.0,}, - ['Tame Snow Leopard']={[0]=900.0,}, - ['Tame Strigid Screecher']={[0]=900.0,}, - ['Tame Surf Crawler']={[0]=900.0,}, - ['Tame Swoop']={[0]=900.0,}, - ['Tame Webwood Lurker']={[0]=900.0,}, - ['Target Dummy Spawn Effect']={[0]=5.0,}, - ['Taunt']={[0]=3.0,}, - ['Tendon Rip']={[0]=8.0,}, - ['Tendon Slice']={[0]=8.0,}, - ['Tendrils of Air']={[0]=2.0,}, - ['Terrify']={[0]=4.0,}, - ['Terrifying Howl']={[0]=3.0,}, - ['Terrifying Roar']={[0]=5.0,}, - ['Terrifying Screech']={[0]=4.0,}, - ['Test Curse of Agony']={[0]=24.0,}, - ['Test Rip']={[0]=12.0,}, - ['Test Strike W35']={[0]=10.0,}, - ['Test Strike W50']={[0]=10.0,}, - ['Tetanus']={[0]=1200.0,}, - ['Thorn Volley']={[0]=2.0,}, - ['Thorns']={[0]=600.0,}, - ['Threatening Gaze']={[0]=6.0,}, - ['Threatening Growl']={[0]=30.0,}, - ['Throw Axe']={[0]=2.0,}, - ['Thunder Clap']={[1]=10.0,[2]=14.0,[3]=18.0,[4]=22.0,[5]=26.0,[6]=30.0,[0]=30.0,}, - ['Thunderclap']={[0]=10.0,}, - ['Thundercrack']={[0]=2.5,}, - ['Thunderfury']={[0]=12.0,}, - ['Thundershock']={[0]=5.0,}, - ['Tidal Charm']={[0]=3.0,}, - ['Tight Pinch']={[0]=5.0,}, - ['Time Lapse']={[0]=8.0,}, - ['Torch Burst']={[0]=30.0,}, - ['Torch Toss']={[0]=30.0,}, - ['Tornado']={[0]=4.0,}, - ['Touch of Ravenclaw']={[0]=5.0,}, - ['Touch of Weakness']={[0]=120.0,}, - ['Toxic Contagion']={[0]=60.0,}, - ['Toxic Saliva']={[0]=120.0,}, - ['Toxic Volley']={[0]=15.0,}, - ['Tranquilizing Poison']={[0]=8.0,}, - ['Trap']={[0]=10.0,}, - ['Trelane\'s Freezing Touch']={[0]=12.0,}, - ['Trip']={[0]=3.0,}, - ['True Fulfillment']={[0]=20.0,}, - ['Tunneler Acid']={[0]=30.0,}, - ['Turn Undead']={[1]=10.0,[2]=15.0,[3]=20.0,[0]=20.0,}, - ['Twisted Reflection']={[0]=45.0,}, - ['Unbalancing Strike']={[0]=6.0,}, - ['Unholy Curse']={[0]=12.0,}, - ['Vampiric Embrace']={[0]=60.0,}, - ['Vanish']={[0]=20.0,}, - ['Veil of Darkness']={[0]=7.0,}, - ['Veil of Shadow']={[0]=15.0,}, - ['Vekniss Catalyst']={[0]=30.0,}, - ['Vengeance']={[0]=600.0,}, - ['Venom Spit']={[0]=10.0,}, - ['Venom Sting']={[0]=45.0,}, - ['Venomhide Poison']={[0]=30.0,}, - ['Vicious Rend']={[0]=15.0,}, - ['Vindication']={[0]=10.0,}, - ['Viper Sting']={[0]=8.0,}, - ['Virulent Poison']={[0]=30.0,}, - ['Vital Wound']={[0]=8.0,}, - ['Void Bolt']={[0]=10.0,}, - ['Volatile Infection']={[0]=120.0,}, - ['Voodoo Hex']={[0]=120.0,}, - ['Vulnerable']={[0]=3.0,}, - ['Wail of Nightlash']={[0]=15.0,}, - ['Wail of the Banshee']={[0]=12.0,}, - ['Wailing Dead']={[0]=6.0,}, - ['Wandering Plague']={[0]=300.0,}, - ['War Stomp']={[0]=2.0,}, - ['Warchief\'s Blessing']={[0]=3600.0,}, - ['Ward of Laze effect']={[0]=3.0,}, - ['Warlock Terror']={[0]=2.0,}, - ['Water Breathing']={[0]=600.0,}, - ['Wavering Will']={[0]=60.0,}, - ['Weak Poison']={[0]=12.0,}, - ['Weakening Disease']={[0]=30.0,}, - ['Web']={[0]=8.0,}, - ['Web Explosion']={[0]=10.0,}, - ['Web II']={[0]=10.0,}, - ['Web III']={[0]=12.0,}, - ['Web Spin']={[0]=7.0,}, - ['Web Spray']={[0]=10.0,}, - ['Whipweed Entangle']={[0]=18.0,}, - ['Whipweed Roots']={[0]=15.0,}, - ['Whirling Trip']={[0]=2.0,}, - ['Whirlwind']={[0]=2.0,}, - ['Whisperings of C\'Thun']={[0]=20.0,}, - ['Widow Bite']={[0]=4.0,}, - ['Widow\'s Embrace']={[0]=30.0,}, - ['Wild Magic']={[0]=30.0,}, - ['Wild Polymorph']={[0]=20.0,}, - ['Will of Hakkar']={[0]=20.0,}, - ['Windreaper']={[0]=20.0,}, - ['Windsor Dismisses Horse DND']={[0]=55.0,}, - ['Wing Clip']={[0]=10.0,}, - ['Wings of Despair']={[0]=6.0,}, - ['Winter\'s Chill']={[0]=15.0,}, - ['Wither']={[0]=21.0,}, - ['Wither Strike']={[0]=8.0,}, - ['Wither Touch']={[0]=120.0,}, - ['Withered Touch']={[0]=180.0,}, - ['Withering Heat']={[0]=900.0,}, - ['Withering Poison']={[0]=180.0,}, - ['Wound']={[0]=25.0,}, - ['Wound Poison']={[0]=15.0,}, - ['Wracking Pains']={[0]=180.0,}, - ['Wrath of the Plaguebringer']={[0]=10.0,}, - ['Wyvern Sting']={[0]=12.0,}, - ['[PH] Crystal Corpse Timer']={[0]=7200.0,}, - ['scaler test']={[0]=10.0,}, -} - --- custom entries not detected by DBC extractor -ShaguTweaks_locale["enUS"]["debuffs"]['Cone of Cold']={[0]=8.0,} -- Cone of Cold - -ShaguTweaks_locale["enUS"]["totems"] = { - ["Disease Cleansing Totem"] = "spell_nature_diseasecleansingtotem", - ["Earth Elemental Totem"] = "spell_nature_earthelemental_totem", - ["Earthbind Totem"] = "spell_nature_strengthofearthtotem02", - ["Fire Elemental Totem"] = "spell_fire_elemental_totem", - ["Fire Nova Totem"] = "spell_fire_sealoffire", - ["Fire Resistance Totem"] = "spell_fireresistancetotem_01", - ["Flametongue Totem"] = "spell_nature_guardianward", - ["Frost Resistance Totem"] = "spell_frostresistancetotem_01", - ["Grace of Air Totem"] = "spell_nature_invisibilitytotem", - ["Grounding Totem"] = "spell_nature_groundingtotem", - ["Healing Stream Totem"] = "Inv_spear_04", - ["Magma Totem"] = "spell_fire_selfdestruct", - ["Mana Spring Totem"] = "spell_nature_manaregentotem", - ["Mana Tide Totem"] = "spell_frost_summonwaterelemental", - ["Nature Resistance Totem"] = "spell_nature_natureresistancetotem", - ["Poison Cleansing Totem"] = "spell_nature_poisoncleansingtotem", - ["Searing Totem"] = "spell_fire_searingtotem", - ["Sentry Totem"] = "spell_nature_removecurse", - ["Stoneclaw Totem"] = "spell_nature_stoneclawtotem", - ["Stoneskin Totem"] = "spell_nature_stoneskintotem", - ["Strength of Earth Totem"] = "spell_nature_earthbindtotem", - ["Totem of Wrath"] = "spell_fire_totemofwrath", - ["Tremor Totem"] = "spell_nature_tremortotem", - ["Windfury Totem"] = "spell_nature_windfury", - ["Windwall Totem"] = "spell_nature_earthbind", - ["Wrath of Air Totem"] = "spell_nature_slowingtotem", -} diff --git a/main.lua b/main.lua index 70e9d0d..5793816 100644 --- a/main.lua +++ b/main.lua @@ -74,6 +74,8 @@ ShaguTweaks:SetScript("OnEvent", function() -- Retire the removed duplicate toggle; ClassicAPI owns these preferences. ShaguTweaks_config[ShaguTweaks.T["Auto Dismount"]] = nil + -- Area looting is delegated to a dedicated addon from 3.8.6 onward. + ShaguTweaks_config[ShaguTweaks.T["AoE Loot"]] = nil -- Gold numbers belong to Real Health Numbers; retire the duplicate toggle. ShaguTweaks_config["Unit Frame Health Colors"] = nil diff --git a/mods/actionbar-reagents.lua b/mods/actionbar-reagents.lua index 743dfdf..6fcf63e 100755 --- a/mods/actionbar-reagents.lua +++ b/mods/actionbar-reagents.lua @@ -12,24 +12,54 @@ local module = ShaguTweaks:register({ }) module.enable = function(self) + if self.counter then return end local reagent_slots = { } local reagent_counts = { } + local macro_slots = { } local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight" } local reagentcounter = CreateFrame("Frame", "ShaguTweaksReagentCount", UIParent) + self.counter = reagentcounter reagentcounter:RegisterEvent("PLAYER_ENTERING_WORLD") reagentcounter:RegisterEvent("ACTIONBAR_SLOT_CHANGED") reagentcounter:RegisterEvent("ACTIONBAR_PAGE_CHANGED") reagentcounter:RegisterEvent("UPDATE_BONUS_ACTIONBAR") reagentcounter:RegisterEvent("UPDATE_MACROS") + reagentcounter:RegisterEvent("SPELLS_CHANGED") reagentcounter:RegisterEvent("BAG_UPDATE") + local macroEvents = { "MODIFIER_STATE_CHANGED", "PLAYER_TARGET_CHANGED", + "UPDATE_MOUSEOVER_UNIT", "PLAYER_REGEN_DISABLED", "PLAYER_REGEN_ENABLED", + "UPDATE_SHAPESHIFT_FORM", "UPDATE_SHAPESHIFT_FORMS", "SPELL_UPDATE_COOLDOWN", + "UNIT_AURA", "UNIT_DISPLAYPOWER" } + local macroEventSet = {} + local watchingMacros = false + local function WatchMacros() + local needed = next(macro_slots) ~= nil + if needed == watchingMacros then return end + watchingMacros = needed + for _, name in ipairs(macroEvents) do + if needed and API.eventutils and _G.C_EventUtils.IsEventValid(name) then + macroEventSet[name] = true + if name == "UNIT_AURA" or name == "UNIT_DISPLAYPOWER" then + API.RegisterUnitEvent(reagentcounter, name, "player") + else reagentcounter:RegisterEvent(name) end + elseif not needed then + reagentcounter:UnregisterEvent(name) + macroEventSet[name] = nil + end + end + end + -- Hidden frames still receive events, but do not execute OnUpdate. -- Wake for one frame only when work is pending. reagentcounter:Hide() reagentcounter:SetScript("OnEvent", function() + if (event == "UNIT_AURA" or event == "UNIT_DISPLAYPOWER") and arg1 ~= "player" then return end this.pending = true - if event ~= "BAG_UPDATE" then + if macroEventSet[event] then + this.macros = true + elseif event ~= "BAG_UPDATE" then this.rescan = true end this:Show() @@ -45,16 +75,24 @@ module.enable = function(self) -- changes. ClassicAPI resolves the spell/macro directly, so no tooltip -- scan or localized reagent-name parsing is required. if this.rescan then - reagent_counts = {} for slot = 1, 120 do reagentcounter.ScanSlot(slot) end + WatchMacros() + elseif this.macros then + -- A native macro can resolve to another spell without being edited. + -- Re-evaluate only known macro actions, not all 120 action slots. + for slot in pairs(macro_slots) do reagentcounter.ScanSlot(slot) end + WatchMacros() end -- Count only reagents referenced by active actions. ClassicAPI reads the -- inventory directly, avoiding a Lua bag scan for every reagent. - for itemID in pairs(reagent_counts) do - reagent_counts[itemID] = API.GetItemCount(itemID, false, false) or 0 + for itemID in pairs(reagent_counts) do reagent_counts[itemID] = nil end + for _, itemID in pairs(reagent_slots) do + if reagent_counts[itemID] == nil then + reagent_counts[itemID] = API.GetItemCount(itemID, false, false) or 0 + end end -- update all actionbar buttons @@ -79,11 +117,13 @@ module.enable = function(self) this.pending = nil this.rescan = nil + this.macros = nil this:Hide() end) reagentcounter.ScanSlot = function(slot) reagent_slots[slot] = nil + macro_slots[slot] = nil if not HasAction(slot) then return end local actionType, actionID = API.GetActionInfo(slot) @@ -92,6 +132,7 @@ module.enable = function(self) if actionType == "spell" then spellID = actionID elseif actionType == "macro" and actionID then + macro_slots[slot] = actionID local _, _, macroSpellID = API.GetMacroSpell(actionID) spellID = macroSpellID end @@ -105,7 +146,6 @@ module.enable = function(self) -- The action button only has one count field. Preserve the historical -- behavior by showing the first reagent's owned stack count. reagent_slots[slot] = reagent.itemID - reagent_counts[reagent.itemID] = reagent_counts[reagent.itemID] or 0 end -- The stock UI clears the Count fontstring on non-consumable actions whenever diff --git a/mods/aoe-loot.lua b/mods/aoe-loot.lua deleted file mode 100644 index 5549622..0000000 --- a/mods/aoe-loot.lua +++ /dev/null @@ -1,121 +0,0 @@ --- Adapted from AoELoot by Sandrea / ChatGPT for ShaguTweaks-ClassicAPI. - -local T = ShaguTweaks.T -local API = ShaguTweaks.API - -local module = ShaguTweaks:register({ - title = T["AoE Loot"], - description = T["Automatically loots all nearby corpses using ClassicAPI without confirmation prompts. Leaves Master Loot untouched."], - expansions = { ["vanilla"] = true }, - category = T["Loot"], - enabled = nil, -}) - -module.enable = function(self) - -- AoE Loot relies entirely on ClassicAPI's native corpse walker, - -- container classification and timer. Do not keep partial legacy behavior - -- when those capabilities are unavailable. - if not API or not API.aoeloot or not API.containeropenable or not API.timer then return end - - local frame = CreateFrame("Frame", "ShaguTweaksAoELoot") - local pending = false - local containerLootDeadline = 0 - local takeoverGeneration = 0 - local TAKEOVER_DELAY = 0.3 - - local function CancelTakeover() - takeoverGeneration = takeoverGeneration + 1 - pending = false - end - - local function TrackContainerUse(bag, slot) - local isOpenable, canOpen = API.IsContainerItemOpenable(bag, slot) - if isOpenable and canOpen then - -- LOOT_OPENED is also fired by clams, lockboxes and similar bag items. - -- Mark that session before UseContainerItem runs so it remains under the - -- normal client handling and the consumed container can leave the bag. - containerLootDeadline = GetTime() + 3 - end - end - - -- Use the ShaguTweaks hook explicitly because its prepend mode records the - -- bag item before UseContainerItem can synchronously fire LOOT_OPENED. - ShaguTweaks.hooksecurefunc("UseContainerItem", TrackContainerUse, true) - - local function IsMasterLootActive() - if not GetLootMethod then return false end - return GetLootMethod() == "master" - end - - local function CanStartAoELoot() - if IsMasterLootActive() then return false end - if API.IsLootScanInProgress() then return false end - return true - end - - local function HasNearbyLootableCorpse() - local units = API.GetNearbyLootableUnits() - return type(units) == "table" and table.getn(units) > 0 - end - - local function StartAoELoot(generation) - if generation ~= takeoverGeneration or not pending then return end - - pending = false - if CanStartAoELoot() then - API.LootAllCorpses() - end - end - - local function TakeOverLootSession() - -- AoE Loot owns corpse sessions while enabled. Close the normal client - -- window immediately so Vanilla autoloot, SuperAPI SetAutoloot modes and - -- quickloot-patched clients cannot keep control of the session. - takeoverGeneration = takeoverGeneration + 1 - local generation = takeoverGeneration - pending = true - CloseLoot() - - -- Native autoloot may already have emitted loot packets before LOOT_OPENED - -- reaches Lua. ClassicAPI's own loot test uses the same 0.3s settling - -- window between loot operations; after it expires the native corpse walker - -- becomes the sole owner of all remaining nearby corpse loot. - C_Timer.After(TAKEOVER_DELAY, function() - StartAoELoot(generation) - end) - end - - frame:RegisterEvent("LOOT_OPENED") - - frame:SetScript("OnEvent", function() - if event ~= "LOOT_OPENED" then return end - - -- Inventory containers use the same loot event as corpses. Let the normal - -- client finish them instead of treating them as an AoE-loot trigger. - if containerLootDeadline > 0 then - local isContainerLoot = GetTime() <= containerLootDeadline - containerLootDeadline = 0 - if isContainerLoot then - CancelTakeover() - return - end - end - - -- The master looter must keep the normal window to inspect and assign loot. - if IsMasterLootActive() then - CancelTakeover() - return - end - - -- Chests, fishing nodes and other non-corpse sources also emit LOOT_OPENED. - -- Only take over when ClassicAPI can see at least one nearby lootable unit. - if not HasNearbyLootableCorpse() then return end - - -- A queued takeover already owns this interaction. ClassicAPI suppresses - -- its own LOOT_OPENED/LOOT_CLOSED events while walking corpses, so no - -- additional session needs to be stacked here. - if pending then return end - - TakeOverLootSession() - end) -end diff --git a/mods/buy-em-all.lua b/mods/buy-em-all.lua index 4b2568c..2043d5a 100755 --- a/mods/buy-em-all.lua +++ b/mods/buy-em-all.lua @@ -484,10 +484,8 @@ function Buy:HandleMerchantClick(anchor, button, ignoreModifiers) if self.max <= 0 then return true end if self.max == 1 then - if self.originalMerchantItemButton_OnClick then - self.originalMerchantItemButton_OnClick("LeftButton", 1) - end - return true + -- Let this invocation's predecessor buy one without opening StackSplit. + return false, true end self.defaultStack = quantity @@ -636,21 +634,30 @@ module.enable = function(self) Buy:StopPurchaseQueue() end) - local function MerchantItemButton_OnClick_Wrapper(button, ignoreModifiers) - local anchor = this - if Buy:HandleMerchantClick(anchor, button, ignoreModifiers) then return end - if Buy.originalMerchantItemButton_OnClick then - return Buy.originalMerchantItemButton_OnClick(button, ignoreModifiers) - end + local installed, dispatching + local function DispatchClick(original, button, ignoreModifiers) + local handled, single = Buy:HandleMerchantClick(this, button, ignoreModifiers) + if handled then return end + return original(single and "LeftButton" or button, single and 1 or ignoreModifiers) end local function InstallHook() local current = _G.MerchantItemButton_OnClick if type(current) ~= "function" then return end - if current == MerchantItemButton_OnClick_Wrapper then return end + if current == installed then return end - Buy.originalMerchantItemButton_OnClick = current - _G.MerchantItemButton_OnClick = MerchantItemButton_OnClick_Wrapper + -- A later addon may wrap our previous hook. Each installation must retain + -- its own predecessor; a mutable shared fallback would form a cycle. + local original = current + installed = function(button, ignoreModifiers) + if dispatching then return original(button, ignoreModifiers) end + dispatching = true + local ok, r1, r2, r3, r4 = pcall(DispatchClick, original, button, ignoreModifiers) + dispatching = nil + if not ok then error(r1) end + return r1, r2, r3, r4 + end + _G.MerchantItemButton_OnClick = installed end InstallHook() diff --git a/mods/castbar-move.lua b/mods/castbar-move.lua index 1a068de..5c70d3e 100644 --- a/mods/castbar-move.lua +++ b/mods/castbar-move.lua @@ -41,6 +41,9 @@ local function SetPreview(on) if S.preview == on then return end S.preview = on if on then + local tint = ShaguTweaks.UITint + S.mover.border:SetVertexColor(tint and tint.r or 1, tint and tint.g or 1, + tint and tint.b or 1, tint and tint.a or 1) S.mover:Show() else CastingBarFrame:StopMovingOrSizing() @@ -65,7 +68,8 @@ local function CreateMover() m.bg:SetAllPoints(m) m.bg:SetTexture(0, 0, 0, .5) - m.border = m:CreateTexture(nil, "ARTWORK") + -- Keep the rim above the StatusBar's ARTWORK fill during preview/dragging. + m.border = m:CreateTexture(nil, "OVERLAY") m.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border") m.border:SetWidth(256) m.border:SetHeight(64) diff --git a/mods/chat-spam-filter.lua b/mods/chat-spam-filter.lua index 67ef3b0..626adea 100644 --- a/mods/chat-spam-filter.lua +++ b/mods/chat-spam-filter.lua @@ -16,11 +16,11 @@ local COOLDOWN = 70 -- how often (in seconds) stale entries may be pruned when chat traffic arrives local CLEAN_INTERVAL = 30 --- double-buffered cache of [frame] = {message = last_seen_time}, keyed per +-- Cache of [frame] = {message = last_seen_time}, keyed per -- chat frame so that a message shown in one tab (e.g. "General") doesn't -- get treated as a duplicate when it also needs to show in another tab -- (e.g. a custom "World" tab) -local cache = { {}, {}, INDEX = 1 } +local cache = {} local last_cleanup = 0 local canFilterEmotes = API and API.guildmembership and API.friendmembership @@ -37,21 +37,12 @@ local function PruneCache(now) if now - last_cleanup < CLEAN_INTERVAL then return end last_cleanup = now - local index = cache.INDEX - local newindex = (index == 1) and 2 or 1 - cache[newindex] = {} - - for frame, frameCache in pairs(cache[index]) do + for frame, frameCache in pairs(cache) do for msg, seen_at in pairs(frameCache) do - if (seen_at + COOLDOWN) > now then - cache[newindex][frame] = cache[newindex][frame] or {} - cache[newindex][frame][msg] = seen_at - end + if (seen_at + COOLDOWN) <= now then frameCache[msg] = nil end end + if not next(frameCache) then cache[frame] = nil end end - - cache[index] = {} - cache.INDEX = newindex end -- returns true (and records the message) if this exact text was already seen @@ -60,11 +51,10 @@ local function IsDuplicate(frame, msg) local now = GetTime() PruneCache(now) - local index = cache.INDEX - local frameCache = cache[index][frame] + local frameCache = cache[frame] if not frameCache then frameCache = {} - cache[index][frame] = frameCache + cache[frame] = frameCache end local seen_at = frameCache[msg] diff --git a/mods/cooldown-numbers.lua b/mods/cooldown-numbers.lua index 28ca42f..7b60be4 100644 --- a/mods/cooldown-numbers.lua +++ b/mods/cooldown-numbers.lua @@ -18,6 +18,90 @@ local module = ShaguTweaks:register({ -- by one complete clock cycle. local TIMER_WRAP_SECONDS = (2 ^ 32) / 1000 +local formatter +local nativeCooldowns = setmetatable({}, { __mode = "k" }) +local cvarWatcher +local SetCooldown + +local function FontSize(parent) + local size = parent:GetHeight() or 0 + size = size > 0 and size * .64 or 12 + return size > 14 and 14 or size +end + +local function NativeNumbersEnabled() + return type(_G.GetCVar) == "function" and _G.GetCVar("countdownForCooldowns") == "1" +end + +local function CanUseNative(cooldown) + return type(cooldown.GetCountdownFontString) == "function" + and type(cooldown.SetCountdownFormatter) == "function" + and type(cooldown.SetMinimumCountdownDuration) == "function" + and type(cooldown.SetHideCountdownNumbers) == "function" + and _G.C_StringUtil and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function" + and _G.Enum and _G.Enum.NumericRuleFormatRounding +end + +local function NativeTarget(cooldown, parent) + if CanUseNative(cooldown) then return cooldown end + if cooldown.stCountdownNativeFrame ~= nil then return cooldown.stCountdownNativeFrame or nil end + if not ShaguTweaks.API.nativecooldowns or not _G.C_StringUtil + or type(_G.C_StringUtil.CreateNumericRuleFormatter) ~= "function" then return end + -- Stock Vanilla indicators can still be Models. Keep their sweep untouched + -- and use an engine-driven child solely for numbers, as the old overlay did. + local ok, child = pcall(CreateFrame, "Cooldown", nil, cooldown) + if not ok or not child then + cooldown.stCountdownNativeFrame = false + return + end + if not CanUseNative(child) then + child:Hide() + cooldown.stCountdownNativeFrame = false + return + end + child.stCountdownHelper = true + child:SetAllPoints(cooldown) + child:SetFrameLevel(parent:GetFrameLevel() + 1) + child:SetDrawSwipe(false) + child:SetDrawEdge(false) + child:SetDrawBling(false) + cooldown.stCountdownNativeFrame = child + return child +end + +local function GetFormatter() + if formatter then return formatter end + local up = _G.Enum.NumericRuleFormatRounding.Up + formatter = _G.C_StringUtil.CreateNumericRuleFormatter() + -- Match TimeConvert, including its colors before rounding at 5/10 seconds. + formatter:SetBreakpoints({ + { threshold = 0, step = 1, rounding = up, format = "|cffff5555%d" }, + { threshold = 5, step = 1, rounding = up, format = "|cffffff55%d" }, + { threshold = 10, step = 1, rounding = up, format = "|cffffffff%d" }, + { threshold = 60, format = "|cffffffff%dm", components = { { div = 60, step = 1, rounding = up } } }, + { threshold = 3600, format = "|cffffffff%dh", components = { { div = 3600, step = 1, rounding = up } } }, + { threshold = 86400, format = "|cffffffff%dd", components = { { div = 86400, step = 1, rounding = up } } }, + }) + return formatter +end + +local function RememberNative(cooldown, start, duration, enable) + nativeCooldowns[cooldown] = true + cooldown.stCountdownStart, cooldown.stCountdownDuration, cooldown.stCountdownEnable = start, duration, enable + if cvarWatcher then return end + cvarWatcher = CreateFrame("Frame") + cvarWatcher.enabled = NativeNumbersEnabled() + cvarWatcher:RegisterEvent("CVAR_UPDATE") + cvarWatcher:SetScript("OnEvent", function() + local enabled = NativeNumbersEnabled() + if enabled == cvarWatcher.enabled then return end + cvarWatcher.enabled = enabled + for frame in pairs(nativeCooldowns) do + SetCooldown(frame, frame.stCountdownStart, frame.stCountdownDuration, frame.stCountdownEnable) + end + end) +end + local function CooldownOnUpdate() -- hide frames without parent local parent = this:GetParent() @@ -73,9 +157,7 @@ local function CreateCoolDown(cooldown) cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString(nil, "OVERLAY") -- detect dynamic font size - local size = parent:GetHeight() or 0 - size = size > 0 and size * .64 or 12 - size = size > 14 and 14 or size + local size = FontSize(parent) -- set fonts cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE") @@ -83,16 +165,55 @@ local function CreateCoolDown(cooldown) cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate) end -local function SetCooldown(this, start, duration, enable) +SetCooldown = function(this, start, duration, enable) + if this.stCountdownHelper then return end -- Let OmniCC and ShaguPlates own their cooldown text. Also hide an overlay -- that may have been created before either compatibility flag was applied. - if this.noCooldownCount or this.pfCooldownType then + if this.noCooldownCount or this.pfCooldownType or this.readable then if this.cooldowntext then this.cooldowntext:Hide() end + if this.stCountdownNativeFrame then this.stCountdownNativeFrame:Hide() end + if (this.stNativeCountdown or this.noCooldownCount) + and type(this.SetHideCountdownNumbers) == "function" then + this:SetHideCountdownNumbers(true) + end return end + local parent = this:GetParent() + if parent and (CanUseNative(this) or (ShaguTweaks.API.nativecooldowns + and _G.C_StringUtil and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function")) then + RememberNative(this, start, duration, enable) + if NativeNumbersEnabled() then + local target = NativeTarget(this, parent) + local text = target and target:GetCountdownFontString() + if text then + if this.cooldowntext then this.cooldowntext:Hide() end + if target == this then this.stNativeCountdown = true end + target:SetCountdownFormatter(GetFormatter()) + -- Native threshold is strictly greater-than; retain duration >= 2s. + target:SetMinimumCountdownDuration(1999) + local active = start and start > 0 and duration + and duration >= 2 and (not enable or enable > 0) + target:SetHideCountdownNumbers(not active) + if target ~= this then + if active then target:SetCooldown(start, duration) else target:Hide() end + end + text:SetFont(STANDARD_TEXT_FONT, FontSize(parent), "OUTLINE") + text:ClearAllPoints() + text:SetPoint("CENTER", target, "CENTER", 0, 0) + text:SetTextColor(1, 1, 1, 1) + return + end + end + end + + -- Never switch on the global CVar: it also controls unrelated addons. + -- Older builds and CVar-off clients keep the Lua renderer. + if this.stCountdownNativeFrame then this.stCountdownNativeFrame:Hide() end + if type(this.SetHideCountdownNumbers) == "function" then this:SetHideCountdownNumbers(true) end + -- don't draw global cooldowns if not duration or duration < 2 then -- hide if already existing @@ -108,7 +229,7 @@ local function SetCooldown(this, start, duration, enable) end if this.cooldowntext then - if start > 0 and duration > 0 and (not enable or enable > 0) then + if start and start > 0 and duration > 0 and (not enable or enable > 0) then this.cooldowntext.elapsed = 0 this.cooldowntext:Show() else diff --git a/mods/free-slots.lua b/mods/free-slots.lua index 1bebc65..cdd1b6b 100644 --- a/mods/free-slots.lua +++ b/mods/free-slots.lua @@ -1,5 +1,5 @@ local _G = ShaguTweaks.GetGlobalEnv() -local L, T = ShaguTweaks.L, ShaguTweaks.T +local T = ShaguTweaks.T local API = ShaguTweaks.API local module = ShaguTweaks:register({ diff --git a/mods/improved-castbar.lua b/mods/improved-castbar.lua index a556818..30b01b7 100644 --- a/mods/improved-castbar.lua +++ b/mods/improved-castbar.lua @@ -84,6 +84,19 @@ module.enable = function(self) end local timerFormat = "%.1f"..T["s"] + local nativeText = API.durationtimers and type(API.GetCastDuration) == "function" + if nativeText then + local formatter = _G.C_StringUtil.CreateNumericRuleFormatter() + formatter:AddBreakpoint({ threshold = 0, format = timerFormat }) + castbar.timerBinding = _G.C_DurationUtil.CreateDurationTextBinding() + castbar.timerBinding:SetFontString(castbar.timerText) + castbar.timerBinding:SetFormatter(formatter) + castbar.timerBinding:SetExpiredText(string.format(timerFormat, 0)) + ShaguTweaks.HookScript(CastingBarFrame, "OnHide", function() + castbar.timerBinding:Disable() + castbar:Hide() + end) + end local function UpdateTimer() local alpha = CastingBarFrame:GetAlpha() @@ -136,6 +149,7 @@ module.enable = function(self) castbar.startTime = nil castbar.endTime = nil castbar.isChannel = nil + if castbar.timerBinding then castbar.timerBinding:Disable() end if CastingBarFrame:GetAlpha() == 0 then castbar:Hide() end @@ -157,24 +171,35 @@ module.enable = function(self) castbar.elapsed = 0 castbar:Show() - UpdateTimer() + if nativeText then + local duration + duration, castbar.fallbackDuration = API.GetCastDuration("player", isChannel, + castbar.startTime, castbar.endTime, castbar.fallbackDuration) + castbar.timerBinding:SetDuration(duration) + castbar.timerBinding:Enable() + else + UpdateTimer() + end end -- The cast/channel query runs only on ClassicAPI state-change events. - -- While visible, OnUpdate only animates the remaining-time text and alpha. + -- Native text inherits the player's normal fade. Only older builds need + -- an OnUpdate to animate the remaining-time text and alpha. castbar.elapsed = 0 - castbar:SetScript("OnUpdate", function() + if not nativeText then castbar:SetScript("OnUpdate", function() this.elapsed = (this.elapsed or 0) + arg1 if this.elapsed < .05 then return end this.elapsed = 0 UpdateTimer() - end) + end) end + + if nativeText then ShaguTweaks.HookScript(CastingBarFrame, "OnShow", RefreshCast) end local events = CreateFrame("Frame", nil, UIParent) events:RegisterEvent("PLAYER_ENTERING_WORLD") for index = 1, table.getn(classicEvents) do - events:RegisterEvent(classicEvents[index]) + API.RegisterUnitEvent(events, classicEvents[index], "player") end events:SetScript("OnEvent", function() diff --git a/mods/item-colors.lua b/mods/item-colors.lua index 711fee9..c19362f 100644 --- a/mods/item-colors.lua +++ b/mods/item-colors.lua @@ -80,6 +80,68 @@ local function Quality(itemID) return API.GetItemQualityByID(itemID) end +-- Cold metadata needs one retry when ClassicAPI's cache reply arrives. Store +-- identities only: the refresh rereads live slots instead of repainting a +-- recycled button with an old item's reply. No polling while waiting. +local pendingItems = {} +local itemEvents, itemRefreshQueued + +local function StopItemEventsIfIdle() + if itemEvents and not next(pendingItems) then + itemEvents:UnregisterEvent("GET_ITEM_INFO_RECEIVED") + end +end + +local function RetryItemColors() + itemRefreshQueued = nil + if Engine.borders or Engine.glows then + if Engine.RefreshAll then Engine:RefreshAll() end + end +end + +local function TrackColdItem(button, itemID, quality) + if not itemID or quality ~= nil or not (Engine.borders or Engine.glows) + or not button.IsVisible or not button:IsVisible() then + if pendingItems[button] then + pendingItems[button] = nil + StopItemEventsIfIdle() + end + return + end + if not API.eventutils or not _G.C_EventUtils + or not _G.C_EventUtils.IsEventValid("GET_ITEM_INFO_RECEIVED") then return end + + if not itemEvents then + itemEvents = CreateFrame("Frame") + itemEvents:SetScript("OnEvent", function() + local refresh + for slot, id in pairs(pendingItems) do + if not slot:IsVisible() then + pendingItems[slot] = nil + elseif id == arg1 then + pendingItems[slot] = nil + if arg2 and arg2 ~= 0 then refresh = true end + end + end + StopItemEventsIfIdle() + if refresh and not itemRefreshQueued then + itemRefreshQueued = true + if API.timer then API.Defer(RetryItemColors) + else ShaguTweaks.QueueFunction(RetryItemColors) end + end + end) + end + if not button.stItemCacheHideHook then + button.stItemCacheHideHook = true + HookScript(button, "OnHide", function() + pendingItems[button] = nil + StopItemEventsIfIdle() + end) + end + pendingItems[button] = itemID + itemEvents:RegisterEvent("GET_ITEM_INFO_RECEIVED") +end + local function IsQuestItem(itemID) if not itemID then return false end local _, _, _, _, _, classID = API.GetItemInfoInstant(itemID) @@ -120,6 +182,7 @@ local function SetVisualColor(button, itemID, opts, knownQuality) local default = opts.default or defaultColor local quality = knownQuality if quality == nil then quality = Quality(itemID) end + TrackColdItem(button, itemID, quality) local quest = opts.quest ~= false and IsQuestItem(itemID) local r, g, b local hasRarity = false @@ -522,6 +585,7 @@ function Engine:Install() self.RefreshAll = function() RefreshPaperdoll() + if InspectFrame and InspectFrame:IsShown() then RefreshInspect() end RefreshWeapons() RefreshBags() if BankFrame and BankFrame:IsShown() then RefreshBank() end diff --git a/mods/loss-of-control.lua b/mods/loss-of-control.lua index 0333a7b..ef64756 100644 --- a/mods/loss-of-control.lua +++ b/mods/loss-of-control.lua @@ -8,6 +8,7 @@ local _G = ShaguTweaks.GetGlobalEnv() local T = ShaguTweaks.T +local API = ShaguTweaks.API local module = ShaguTweaks:register({ title = T["Loss of Control Alert"], @@ -53,6 +54,13 @@ local DEFAULT_COLOR = COLORS.STUN -- One table for state: Lua 5.0 allows only 32 upvalues per function. local S = { unlocked = false, elapsed = 0 } +local Refresh + +local function StopTiming() + if S.expiry then S.expiry:Cancel(); S.expiry = nil end + if S.schoolWake then S.schoolWake:Cancel(); S.schoolWake = nil end + if S.binding then S.binding:Disable(); S.frame.bar:SetMinMaxValues(0, 1) end +end local function Supported() local c = _G.C_LossOfControl @@ -73,7 +81,8 @@ local function PickEffect() local count = _G.C_LossOfControl.GetActiveLossOfControlDataCount() or 0 for i = 1, count do local data = _G.C_LossOfControl.GetActiveLossOfControlData(i) - if data then + if data and not (data.duration and data.duration > 0 + and data.timeRemaining and data.timeRemaining <= 0) then local rank = RANK[data.locType] or (table.getn(PRIORITY) + 1) local left = data.timeRemaining or 0 if not best or rank < bestRank or (rank == bestRank and left > bestLeft) then @@ -97,24 +106,35 @@ local function ApplyTheme() local color = ShaguTweaks.UIBorder local r, g, b = color and color.r or .6, color and color.g or .6, color and color.b or .6 local a = color and color.a or 1 - f:SetBackdropBorderColor(r, g, b, a) - f.border:SetBackdropBorderColor(r, g, b, a) - f.icon:SetVertexColor(1, 1, 1, 1) + if S.themeR ~= r or S.themeG ~= g or S.themeB ~= b or S.themeA ~= a then + S.themeR, S.themeG, S.themeB, S.themeA = r, g, b, a + f:SetBackdropBorderColor(r, g, b, a) + f.border:SetBackdropBorderColor(r, g, b, a) + f.icon:SetVertexColor(1, 1, 1, 1) + end +end + +local function SetBarColor(r, g, b) + if S.native then S.frame.bar:SetStatusBarColor(r, g, b) + else S.frame.bar:SetVertexColor(r, g, b) end end local function Apply(data) local f = S.frame local color = COLORS[data.locType] or DEFAULT_COLOR - f.icon:SetTexture((data.iconTexture and data.iconTexture ~= "") and data.iconTexture or FALLBACK_ICON) + local icon = (data.iconTexture and data.iconTexture ~= "") and data.iconTexture or FALLBACK_ICON + if S.icon ~= icon then S.icon = icon; f.icon:SetTexture(icon) end ApplyTheme() - f.bar:SetVertexColor(color[1], color[2], color[3]) - f.title:SetText(LABELS[data.locType] or data.displayText or "") + if S.effectColor ~= color then S.effectColor = color; SetBarColor(color[1], color[2], color[3]) end + local title = LABELS[data.locType] or data.displayText or "" + if S.title ~= title then S.title = title; f.title:SetText(title) end -- spell name under the label; school lockouts name the locked school local detail = SchoolName(data) if not detail and data.locType ~= "SCHOOL_INTERRUPT" then detail = data.displayText end - f.detail:SetText(detail or "") + detail = detail or "" + if S.detail ~= detail then S.detail = detail; f.detail:SetText(detail) end f.data = data end @@ -140,18 +160,43 @@ local function UpdateTime() end end --- Pick again and redraw. Called on the events and while the alert is up, --- because re-locking a school extends its lockout without firing an event. -local function Refresh() +-- Re-select on lifecycle events and timed expiry. Native school lockouts also +-- check scalar state for silent extensions; older builds retain their poller. +Refresh = function() local f = S.frame if S.unlocked then return end local data = PickEffect() if data then Apply(data) - UpdateTime() + if S.native then + StopTiming() + local left, total = data.timeRemaining, data.duration + if left and left > 0 then + S.duration:SetTimeFromEnd(GetTime() + left, total and total > 0 and total or left) + S.binding:SetDuration(S.duration) + S.binding:Enable() + S.expiry = _G.C_Timer.NewTimer(left, S.expire) + if total and total > 0 then + f.bar:SetTimerDuration(S.duration, _G.Enum.StatusBarInterpolation.Immediate, + _G.Enum.StatusBarTimerDirection.RemainingTime) + f.bar:Show() + else f.bar:Hide() end + else + f.time:SetText("") + f.bar:Hide() + end + if data.locType == "SCHOOL_INTERRUPT" then + if type(_G.C_LossOfControl.GetSchoolLockout) == "function" then + local mask, remaining = _G.C_LossOfControl.GetSchoolLockout() + S.schoolMask, S.schoolEnd = mask, GetTime() + (remaining or 0) + end + S.schoolWake = _G.C_Timer.NewTimer(.2, S.checkSchool) + end + else UpdateTime() end if not f:IsShown() then f:Show() end - elseif f:IsShown() then + else + StopTiming() f.data = nil f:Hide() end @@ -184,10 +229,13 @@ local function SetUnlocked(on) f:EnableMouse(on) if on then + StopTiming() + S.icon, S.effectColor, S.title, S.detail = nil, nil, nil, nil f.data = nil f.icon:SetTexture(FALLBACK_ICON) ApplyTheme() - f.bar:SetVertexColor(1, .2, .2) + SetBarColor(1, .2, .2) + if S.native then f.bar:SetValue(1) end f.bar:SetWidth(BAR_WIDTH) f.bar:Show() f.title:SetText(T["Stunned"]) @@ -252,8 +300,41 @@ local function CreateAlert() f.time:SetTextColor(1, 1, 1) -- remaining time as a bar under the text - f.bar = f:CreateTexture(nil, "ARTWORK") - f.bar:SetTexture("Interface\\TargetingFrame\\UI-StatusBar") + S.native = API.durationtimers and _G.Enum.StatusBarTimerDirection.RemainingTime ~= nil + if S.native then + f.bar = CreateFrame("StatusBar", nil, f) + if type(f.bar.SetTimerDuration) ~= "function" then S.native = false; f.bar:Hide() end + end + if S.native then + f.bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") + f.bar:SetWidth(BAR_WIDTH) + S.duration = _G.C_DurationUtil.CreateDuration() + local formatter = _G.C_StringUtil.CreateNumericRuleFormatter() + formatter:SetBreakpoints({{ threshold = 0, format = "%.1f" }, + { threshold = 10, step = 1, format = "%d" }}) + S.binding = _G.C_DurationUtil.CreateDurationTextBinding() + S.binding:SetFontString(f.time) + S.binding:SetFormatter(formatter) + S.binding:SetExpiredText("") + S.binding:SetUpdateInterval(.05) + S.expire = function() S.expiry = nil; Refresh() end + -- Re-locking an active school can extend it without any native event. + -- Check cheap scalar state only while a school lockout is displayed. + S.checkSchool = function() + S.schoolWake = nil + if S.unlocked or not f.data then return end + local query = _G.C_LossOfControl.GetSchoolLockout + if type(query) ~= "function" then Refresh(); return end + local mask, remaining = query() + local finish = GetTime() + (remaining or 0) + if mask ~= S.schoolMask or math.abs(finish - S.schoolEnd) > .05 then + Refresh() + else S.schoolWake = _G.C_Timer.NewTimer(.2, S.checkSchool) end + end + else + f.bar = f:CreateTexture(nil, "ARTWORK") + f.bar:SetTexture("Interface\\TargetingFrame\\UI-StatusBar") + end f.bar:SetHeight(4) f.bar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", ICON + 20, 10) @@ -271,8 +352,8 @@ local function CreateAlert() RestorePosition() end) - -- Runs only while the alert is shown (a hidden frame gets no OnUpdate). - f:SetScript("OnUpdate", OnUpdate) + -- Only older builds need a Lua animation/selection loop while shown. + if not S.native then f:SetScript("OnUpdate", OnUpdate) end S.events = CreateFrame("Frame") S.events:SetScript("OnEvent", function() @@ -314,5 +395,6 @@ module.disable = function(self) if not S.frame then return end if S.unlocked then SetUnlocked(false) end S.events:UnregisterAllEvents() + StopTiming() S.frame:Hide() end diff --git a/mods/minimap-clean.lua b/mods/minimap-clean.lua index cd4513b..25eb924 100644 --- a/mods/minimap-clean.lua +++ b/mods/minimap-clean.lua @@ -35,7 +35,8 @@ local ignoreList = { "GameTimeFrame", "MinimapToggleButton", "TWMinimapShopFrame", - "LFT_Minimap" + "LFT_Minimap", + "LFTMinimapButton", } local function ShouldIgnore(name) @@ -99,6 +100,7 @@ local function RefreshButtonCache() buttonCache[table.getn(buttonCache) + 1] = child end end + return true end local function SetButtonsAlpha(alpha) @@ -128,15 +130,20 @@ module.enable = function(self) self.frame = self.frame or CreateFrame("Frame") - -- Prime the cache once. Future scans only happen when the minimap child count - -- changes, e.g. when an addon creates a button after login. - RefreshButtonCache() + -- Addons can replace a child without changing the final child count. + self.frame:RegisterEvent("ADDON_LOADED") + self.frame:SetScript("OnEvent", function() cachedChildCount = -1 end) + SetButtonsAlpha(1) self.frame:SetScript("OnUpdate", function() self.elapsed = self.elapsed + arg1 if self.elapsed < THROTTLE then return end self.elapsed = 0 + -- Only the native child count is queried while the cache is unchanged. + -- Newly discovered buttons inherit the current state even while hidden. + if RefreshButtonCache() then SetButtonsAlpha(self.isShown and 1 or 0) end + local isOver = API.IsMouseOver(Minimap, MARGIN, -MARGIN, -MARGIN, MARGIN) if isOver then @@ -161,6 +168,7 @@ end module.disable = function(self) if self.frame then self.frame:SetScript("OnUpdate", nil) + self.frame:UnregisterEvent("ADDON_LOADED") end -- Restore all detected addon buttons when the module is disabled. diff --git a/mods/minimap-zoom.lua b/mods/minimap-zoom.lua index 74d1b18..a11fa70 100644 --- a/mods/minimap-zoom.lua +++ b/mods/minimap-zoom.lua @@ -1,4 +1,30 @@ local T = ShaguTweaks.T +local _G = ShaguTweaks.GetGlobalEnv() + +local function FixLFTButtonDrag() + local button = _G.LFTMinimapButton + if not button or button.stEnlargedDragFix then return end + local stop = button:GetScript("OnDragStop") + if type(stop) ~= "function" then return end + button.stEnlargedDragFix = true + button:SetScript("OnDragStop", function(...) + local parent = button:GetParent() + local left, top = button:GetLeft(), button:GetTop() + local scale = button:GetScale() + stop(unpack(arg)) + -- Turtle's handler detaches to UIParent without converting coordinates or + -- inherited scale. Keep the native parent/local scale (also across reloads) + -- and preserve the dropped screen position. Native reset remains untouched. + if parent == Minimap and button:GetParent() == UIParent then + button:SetParent(parent) + button:SetScale(scale) + if left and top then + button:ClearAllPoints() + button:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top) + end + end + end) +end local module = ShaguTweaks:register({ title = T["Enlarged Minimap"], @@ -121,6 +147,7 @@ module.enable = function(self) end MinimapCluster:SetScale(scale) + FixLFTButtonDrag() ShaguTweaks.ScheduleMinimapClamp() -- BuffFrame is anchored to UIParent TOPRIGHT/TOPRIGHT x=-205 y=-13 by vanilla. diff --git a/mods/sell-junk.lua b/mods/sell-junk.lua index cfbbf2b..cb5f35a 100644 --- a/mods/sell-junk.lua +++ b/mods/sell-junk.lua @@ -35,7 +35,10 @@ module.enable = function(self) autovendor:SetScript("OnShow", function() this.startMoney = GetMoney() - this.started = HasGreyItems() + this.lastCount = API.GetNumJunkItems() + this.started = this.lastCount > 0 + this.elapsed = 0 + this.progressUntil = GetTime() + 5 if not this.started then this:Hide() @@ -45,6 +48,7 @@ module.enable = function(self) -- ClassicAPI performs a real merchant sale and drains the queue one item -- per frame, without abusing UseContainerItem or a manual throttle. API.SellAllJunkItems() + this.button:Update() end) autovendor:SetScript("OnHide", function() @@ -57,15 +61,22 @@ module.enable = function(self) this.started = nil this.startMoney = nil + this.lastCount, this.elapsed, this.progressUntil = nil, nil, nil if this.button then this.button:Update() end end) - -- This frame is visible only while ClassicAPI's short sell queue is active. - -- Polling a single engine count here replaces repeated full bag scans. + -- Native code owns sale pacing. Check completion ten times per second and + -- stop waiting after five seconds without progress; never resubmit the sale. autovendor:SetScript("OnUpdate", function() - if this.started and API.GetNumJunkItems() == 0 then - this:Hide() - end + if not this.started then return end + this.elapsed = this.elapsed + arg1 + if this.elapsed < .1 then return end + this.elapsed = 0 + local count = API.GetNumJunkItems() + local now = GetTime() + if count < this.lastCount then this.progressUntil = now + 5 end + this.lastCount = count + if count == 0 or now >= this.progressUntil then this:Hide() end end) autovendor:RegisterEvent("MERCHANT_SHOW") diff --git a/mods/swing-timer.lua b/mods/swing-timer.lua index 244e176..0cf6a41 100644 --- a/mods/swing-timer.lua +++ b/mods/swing-timer.lua @@ -340,6 +340,14 @@ end -- Classic cast bar look: dark background, UI-StatusBar fill, the cast bar -- border and spark, and an icon frame like Improved Castbar's. +local function ApplyBarTheme(bar) + local tint, edge = ShaguTweaks.UITint, ShaguTweaks.UIBorder + bar.border:SetVertexColor(tint and tint.r or 1, tint and tint.g or 1, + tint and tint.b or 1, tint and tint.a or 1) + bar.icon:SetBackdropBorderColor(edge and edge.r or 1, edge and edge.g or 1, + edge and edge.b or 1, edge and edge.a or 1) +end + local function CreateBar(key) local bar = CreateFrame("StatusBar", "ShaguTweaksSwingTimer_" .. key, S.container) bar:SetWidth(WIDTH) @@ -353,7 +361,8 @@ local function CreateBar(key) bar.bg:SetAllPoints(bar) bar.bg:SetTexture(0, 0, 0, .5) - bar.border = bar:CreateTexture(nil, "ARTWORK") + -- The StatusBar fill uses ARTWORK; keep the rim above it at every progress. + bar.border = bar:CreateTexture(nil, "OVERLAY") bar.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border") bar.border:SetWidth(256) bar.border:SetHeight(64) @@ -411,15 +420,16 @@ local function CreateBar(key) tile = true, tileSize = 8, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 } }) - if ShaguTweaks.DarkMode then - bar.icon:SetBackdropBorderColor(.3, .3, .3, .9) - end bar.icon.texture = bar.icon:CreateTexture(nil, "BACKGROUND") bar.icon.texture:SetPoint("CENTER", 0, 0) bar.icon.texture:SetWidth(24) bar.icon.texture:SetHeight(24) bar.icon.texture:SetTexCoord(.08, .92, .08, .92) + ApplyBarTheme(bar) + -- Modules initialize in arbitrary order. Read the final palette when a real + -- timer or mover preview appears, without an idle update or recolor scan. + bar:SetScript("OnShow", function() ApplyBarTheme(this) end) -- Each bar is dragged on its own while the preview is up. bar:SetMovable(true) diff --git a/mods/target-castbar.lua b/mods/target-castbar.lua index 696e608..a9eb9a4 100644 --- a/mods/target-castbar.lua +++ b/mods/target-castbar.lua @@ -159,12 +159,19 @@ local function UpdatePosition() end local function HideCast() + if castbar.expiry then castbar.expiry:Cancel(); castbar.expiry = nil end + if castbar.nativeProgress then castbar:SetMinMaxValues(0, 1) end castbar.startTime = nil castbar.endTime = nil castbar.isChannel = nil castbar:Hide() end +local function ExpireCast() + castbar.expiry = nil + HideCast() +end + local function UpdateProgress() local startTime = castbar.startTime local endTime = castbar.endTime @@ -216,6 +223,7 @@ local function RefreshCast() castbar.startTime = startTime / 1000 castbar.endTime = endTime / 1000 castbar.isChannel = isChannel + if castbar.endTime <= GetTime() then HideCast(); return end local max = castbar.endTime - castbar.startTime castbar:SetMinMaxValues(0, max) @@ -230,7 +238,17 @@ local function RefreshCast() UpdatePosition() castbar:Show() - UpdateProgress() + if castbar.nativeProgress then + if castbar.expiry then castbar.expiry:Cancel() end + local duration + duration, castbar.fallbackDuration = API.GetCastDuration("target", isChannel, + castbar.startTime, castbar.endTime, castbar.fallbackDuration) + castbar:SetTimerDuration(duration, _G.Enum.StatusBarInterpolation.Immediate, + isChannel and _G.Enum.StatusBarTimerDirection.RemainingTime or _G.Enum.StatusBarTimerDirection.ElapsedTime) + castbar.expiry = _G.C_Timer.NewTimer(math.max(0, castbar.endTime - GetTime()), ExpireCast) + else + UpdateProgress() + end end -- Hold Ctrl+Shift+Alt to show a marker where the bar goes and drag it; the @@ -280,7 +298,7 @@ local function CreateMover() mover.bg:SetAllPoints(mover) mover.bg:SetTexture(0, 0, 0, .5) - mover.border = mover:CreateTexture(nil, "ARTWORK") + mover.border = mover:CreateTexture(nil, "OVERLAY") mover.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border") mover.border:SetWidth(256 * f) mover.border:SetHeight(64 * f) @@ -308,9 +326,6 @@ local function CreateMover() tile = true, tileSize = 8, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 } }) - if ShaguTweaks.DarkMode then - mover.icon:SetBackdropBorderColor(.3, .3, .3, .9) - end mover.icon.texture = mover.icon:CreateTexture(nil, "BACKGROUND") mover.icon.texture:SetPoint("CENTER", 0, 0) mover.icon.texture:SetWidth(16) @@ -338,6 +353,11 @@ local function SetPreview(on) if (mover and mover:IsShown() and true or false) == on then return end if on then if not mover then CreateMover() end + local tint, edge = ShaguTweaks.UITint, ShaguTweaks.UIBorder + mover.border:SetVertexColor(tint and tint.r or 1, tint and tint.g or 1, + tint and tint.b or 1, tint and tint.a or 1) + mover.icon:SetBackdropBorderColor(edge and edge.r or 1, edge and edge.g or 1, + edge and edge.b or 1, edge and edge.a or 1) PlaceMover() mover:Show() elseif mover then @@ -374,15 +394,41 @@ module.enable = function(self) end end - -- Animation-only OnUpdate; hidden frames do not receive OnUpdate ticks. - castbar:SetScript("OnUpdate", UpdateProgress) + castbar.nativeProgress = API.durationtimers and type(API.GetCastDuration) == "function" + and type(castbar.SetTimerDuration) == "function" + and _G.Enum.StatusBarTimerDirection.RemainingTime ~= nil + if castbar.nativeProgress then + castbar.spark:ClearAllPoints() + castbar.spark:SetPoint("CENTER", castbar:GetStatusBarTexture(), "RIGHT", 0, 0) + else + -- Older ClassicAPI builds retain the animation-only fallback. + castbar:SetScript("OnUpdate", UpdateProgress) + end local listener = CreateFrame("Frame") listener:RegisterEvent("PLAYER_TARGET_CHANGED") listener:RegisterEvent("MODIFIER_STATE_CHANGED") for index = 1, table.getn(castEvents) do - API.RegisterUnitEvent(listener, castEvents[index], "target") + API.RegisterUnitEvent(listener, castEvents[index], "target", "player") + end + + -- Layout changes are event-driven on the native animation path. Defer the + -- aura pass so native/third-party buff rows finish laying themselves out. + local positionQueued + local function RunPositionUpdate() positionQueued = nil; UpdatePosition() end + local function QueuePositionUpdate() + if positionQueued or not castbar:IsShown() then return end + positionQueued = true + if API.timer then API.Defer(RunPositionUpdate) + else ShaguTweaks.QueueFunction(RunPositionUpdate) end + end + if castbar.nativeProgress then + API.RegisterUnitEvent(listener, "UNIT_AURA", "target") + if TargetofTargetFrame then + ShaguTweaks.HookScript(TargetofTargetFrame, "OnShow", QueuePositionUpdate) + ShaguTweaks.HookScript(TargetofTargetFrame, "OnHide", QueuePositionUpdate) + end end listener:SetScript("OnEvent", function() @@ -395,10 +441,11 @@ module.enable = function(self) RefreshCast() return end + if event == "UNIT_AURA" then QueuePositionUpdate(); return end -- ClassicAPI fans remote cast events out to every unit token currently -- resolving to the caster, including "target" when appropriate. - if arg1 == "target" then + if arg1 == "target" or (arg1 == "player" and UnitIsUnit("target", "player")) then RefreshCast() end end) diff --git a/mods/totem-timers.lua b/mods/totem-timers.lua index 6e20d2f..283a2fb 100644 --- a/mods/totem-timers.lua +++ b/mods/totem-timers.lua @@ -2,11 +2,12 @@ -- -- One icon per active totem (Fire, Earth, Water, Air) with the time left. -- Left-click targets the totem, hovering shows its tooltip. The data comes --- from ClassicAPI's GetTotemInfo / GetTotemTimeLeft, and the display is --- redrawn on PLAYER_TOTEM_UPDATE, with a light timer only while a totem is up. +-- from ClassicAPI's totem APIs. Native duration timers animate where available; +-- older builds use a light update only while a totem is up. local _G = ShaguTweaks.GetGlobalEnv() local T = ShaguTweaks.T +local API = ShaguTweaks.API local module = ShaguTweaks:register({ title = T["Totem Timers"], @@ -50,6 +51,12 @@ local function FormatTime(seconds) return string.format("%d", seconds) end +local function StopButton(b) + if b.expiry then b.expiry:Cancel(); b.expiry = nil end + if b.binding then b.binding:Disable(); b.bar:SetMinMaxValues(0, 1); b.bar:SetValue(1) end + b.active = nil +end + local function Layout() local shown = 0 for slot = 1, 4 do @@ -102,12 +109,24 @@ local function Refresh() for slot = 1, 4 do local b = S.buttons[slot] local have, name, start, duration, icon = _G.GetTotemInfo(slot) - if start and start > 0 then + local nativeDuration = S.native and _G.GetTotemDuration(slot) + if start and start > 0 and (not nativeDuration or nativeDuration:GetRemainingDuration() > 0) then any = true b.duration = duration if icon and icon ~= "" then b.icon:SetTexture(icon) end b:Show() + if S.native then + StopButton(b) + b.active = true + b.bar:SetWidth(SIZE) + b.bar:SetTimerDuration(nativeDuration, _G.Enum.StatusBarInterpolation.Immediate, + _G.Enum.StatusBarTimerDirection.RemainingTime) + b.binding:SetDuration(nativeDuration) + b.binding:Enable() + b.expiry = _G.C_Timer.NewTimer(nativeDuration:GetRemainingDuration(), b.expire) + end else + StopButton(b) b.duration = nil b:Hide() end @@ -115,7 +134,7 @@ local function Refresh() if any then Layout() - UpdateTimes() + if not S.native then UpdateTimes() end if not S.frame:IsShown() then S.frame:Show() end else S.frame:Hide() @@ -150,6 +169,7 @@ local function SetUnlocked(on) if on then for slot = 1, 4 do local b = S.buttons[slot] + StopButton(b) b:EnableMouse(false) -- let the drag reach the container b.icon:SetTexture(PREVIEW_ICONS[slot]) b.time:SetText(FormatTime(60 - slot * 10)) @@ -189,11 +209,29 @@ local function CreateButton(slot) b.time:SetTextColor(1, 1, 1) -- remaining time as a thin bar under the icon - b.bar = b:CreateTexture(nil, "OVERLAY") - b.bar:SetTexture("Interface\\Buttons\\WHITE8X8") + b.bar = CreateFrame("StatusBar", nil, b) + b.bar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8") + b.bar:SetMinMaxValues(0, 1) + b.bar:SetValue(1) + b.bar:SetWidth(SIZE) b.bar:SetHeight(3) b.bar:SetPoint("TOPLEFT", b, "BOTTOMLEFT", 0, -2) - b.bar:SetVertexColor(SLOT_COLORS[slot][1], SLOT_COLORS[slot][2], SLOT_COLORS[slot][3]) + b.bar:SetStatusBarColor(SLOT_COLORS[slot][1], SLOT_COLORS[slot][2], SLOT_COLORS[slot][3]) + if S.native and type(b.bar.SetTimerDuration) ~= "function" then S.native = false end + if S.native then + b.binding = _G.C_DurationUtil.CreateDurationTextBinding() + b.binding:SetFontString(b.time) + b.binding:SetFormatter(S.formatter) + b.binding:SetExpiredText("") + b.binding:SetUpdateInterval(.1) + b.expire = function() + b.expiry = nil + StopButton(b) + b:Hide() + for i = 1, 4 do if S.buttons[i].active then return end end + if not S.unlocked then S.frame:Hide() end + end + end b:RegisterForClicks("LeftButtonUp") b:SetScript("OnClick", function() @@ -226,6 +264,20 @@ local function CreateTimers() f:Hide() S.frame = f + S.native = API.durationtimers and type(_G.GetTotemDuration) == "function" + and _G.Enum.StatusBarTimerDirection.RemainingTime ~= nil + if S.native then + local down = _G.Enum.NumericRuleFormatRounding.Down + S.formatter = _G.C_StringUtil.CreateNumericRuleFormatter() + S.formatter:SetBreakpoints({ + { threshold = 0, step = 1, rounding = down, format = "|cffff3333%d" }, + { threshold = 5.000001, step = 1, rounding = down, format = "|cffffe633%d" }, + { threshold = 10.000001, step = 1, rounding = down, format = "|cffffffff%d" }, + { threshold = 60, format = "|cffffffff%d:%02d", components = { + { div = 60, step = 1, rounding = down }, { mod = 60, step = 1, rounding = down } } }, + }) + end + for slot = 1, 4 do CreateButton(slot) end f:SetScript("OnMouseDown", function() @@ -242,8 +294,8 @@ local function CreateTimers() RestorePosition() end) - -- Runs only while a totem is out (a hidden frame gets no OnUpdate). - f:SetScript("OnUpdate", OnUpdate) + -- Only older builds need a Lua animation loop while a totem is out. + if not S.native then f:SetScript("OnUpdate", OnUpdate) end S.events = CreateFrame("Frame") S.events:SetScript("OnEvent", function() @@ -284,5 +336,6 @@ module.disable = function(self) if not S.frame then return end if S.unlocked then SetUnlocked(false) end S.events:UnregisterAllEvents() + for slot = 1, 4 do StopButton(S.buttons[slot]) end S.frame:Hide() end diff --git a/mods/turtle-wow.lua b/mods/turtle-wow.lua index d247f72..00745a6 100644 --- a/mods/turtle-wow.lua +++ b/mods/turtle-wow.lua @@ -5,7 +5,32 @@ if not TargetHPText or not TargetHPPercText then return end local T = ShaguTweaks.T -local L = ShaguTweaks.L +local _G = ShaguTweaks.GetGlobalEnv() + +local function IsFirstContinent() + local maps = _G.C_Map + local first = GetMapContinents() + if first and maps and type(maps.GetBestMapForUnit) == "function" + and type(maps.GetMapInfo) == "function" then + local id = maps.GetBestMapForUnit("player") + for depth = 1, 8 do + if not id or id == 0 then return false end + local info = maps.GetMapInfo(id) + if not info then break end + if info.name == first then return true end + id = info.parentMapID + end + end + + -- Older ClassicAPI builds lack the map hierarchy. Restore the viewed map + -- immediately instead of leaving it on the player's zone after clock hover. + local continent, zone = GetCurrentMapContinent(), GetCurrentMapZone() + if continent == nil or zone == nil then return end + SetMapToCurrentZone() + local result = GetCurrentMapContinent() == 1 + if continent == 0 then SetMapZoom(0) else SetMapZoom(continent, zone) end + return result +end local module = ShaguTweaks:register({ title = T["Turtle WoW Compatibility"], @@ -21,8 +46,7 @@ module.enable = function(self) local zh, zm = GetGameTime() local sh, sm = zh, zm - SetMapToCurrentZone() - if GetCurrentMapContinent() == 1 then + if IsFirstContinent() then sh = sh + 12 sh = sh >= 24 and sh - 24 or sh end @@ -79,9 +103,4 @@ module.enable = function(self) -- Vendor Values exposes a lazy ClassicAPI-backed SellValueDB compatibility -- table, so do not allocate a second Turtle-specific static price database. - - -- update debuff durations - L["debuffs"]["Hand of Reckoning"] = {[0]=3.0} - L["debuffs"]['Insect Swarm'] = {[0]=18.0} - L["debuffs"]['Moonfire'] = {[1]=9.0,[2]=18.0,[3]=18.0,[4]=18.0,[5]=18.0,[6]=18.0,[7]=18.0,[8]=18.0,[9]=18.0,[10]=18.0,[0]=18.0} end diff --git a/mods/unitframes-classcolor.lua b/mods/unitframes-classcolor.lua index 4662414..67c2add 100644 --- a/mods/unitframes-classcolor.lua +++ b/mods/unitframes-classcolor.lua @@ -121,7 +121,7 @@ local function SetupModernPowerBars() if not state then return end local unit = bar.unit or state.unit if not ShaguTweaks.ModernClassColors or not UnitExists(unit) then - state.background:Hide() + if state.background:IsShown() then state.background:Hide() end return end -- The druid's secondary bar stays mana while the primary bar is rage/energy. @@ -132,24 +132,37 @@ local function SetupModernPowerBars() local suffix = state.family if suffix == "Player" then suffix = "" - local _, class = UnitClass(unit) -- Keep the retail energy variant for combo-point users. - if powerType == 3 and (class == "ROGUE" or class == "DRUID") then - suffix = "Class" + if powerType == 3 then + local _, class = UnitClass(unit) + if class == "ROGUE" or class == "DRUID" then suffix = "Class" end end end -- Vanilla mana slots are rectangular. Retail target/small-frame fills -- have cut-out corners that leave a visible wedge inside those slots. -- Use the native rectangular mana artwork for every mana frame. if powerType == 0 then suffix = "Class" end - bar:SetStatusBarTexture(texture .. suffix) - state.background:Show() + if state.styleTexture ~= texture or state.styleSuffix ~= suffix then + state.styleTexture, state.styleSuffix = texture, suffix + state.wantedTexture = texture .. suffix + end + local wanted = state.wantedTexture + local current = bar:GetStatusBarTexture() + if not current or current:GetTexture() ~= wanted then bar:SetStatusBarTexture(wanted) end + if not state.background:IsShown() then state.background:Show() end -- Call the method captured before our hook to avoid recursive updates. - state.setColor(bar, 1, 1, 1, 1) + local r, g, b, a = bar:GetStatusBarColor() + if not r or math.abs(r - 1) > .0001 or math.abs(g - 1) > .0001 + or math.abs(b - 1) > .0001 or not a or math.abs(a - 1) > .0001 then + state.setColor(bar, 1, 1, 1, 1) + end else -- Unknown/custom power types retain their original texture and tint. - bar:SetStatusBarTexture(state.originalTexture) - state.background:Hide() + local current = bar:GetStatusBarTexture() + if not current or current:GetTexture() ~= state.originalTexture then + bar:SetStatusBarTexture(state.originalTexture) + end + if state.background:IsShown() then state.background:Hide() end end end @@ -190,7 +203,17 @@ local function SetupModernPowerBars() end -- Vanilla refreshes power type here. Appending preserves the normal frame -- updates, including shapeshifts and targets with a different power type. - hooksecurefunc("UnitFrame_UpdateManaType", RefreshPowerBars) + hooksecurefunc("UnitFrame_UpdateManaType", function(frame) + -- Clients use either an explicit unit frame or the legacy `this` context. + if type(frame) ~= "table" then frame = this end + if not frame then return end + if bars[frame] then ApplyPowerStyle(frame); return end + if frame.manabar and bars[frame.manabar] then ApplyPowerStyle(frame.manabar); return end + if not frame.unit then return end + for bar, state in pairs(bars) do + if (bar.unit or state.unit) == frame.unit then ApplyPowerStyle(bar) end + end + end) RefreshPowerBars() return RefreshPowerBars end diff --git a/mods/unitframes-classportrait.lua b/mods/unitframes-classportrait.lua index 24d6b4d..6c426a4 100644 --- a/mods/unitframes-classportrait.lua +++ b/mods/unitframes-classportrait.lua @@ -13,6 +13,8 @@ for _, name in pairs(tocs) do end end +local CLASS_PORTRAIT_TEXTURE = (addonpath or "Interface\\AddOns\\ShaguTweaks") .. "\\img\\UI-Classes-Circles" + local CLASS_ICON_TCOORDS = { ["WARRIOR"] = { 0, 0.25, 0, 0.25 }, ["MAGE"] = { 0.25, 0.49609375, 0, 0.25 }, @@ -46,33 +48,45 @@ local function UpdatePortraits(frame) if class then local iconCoords = CLASS_ICON_TCOORDS[class] if iconCoords then - frame.portrait:SetTexture(addonpath .. "\\img\\UI-Classes-Circles") - frame.portrait:SetTexCoord(unpack(iconCoords)) + local wanted = CLASS_PORTRAIT_TEXTURE + if frame.portrait:GetTexture() ~= wanted then frame.portrait:SetTexture(wanted) end + local l, t, l2, b, r, t2, r2, b2 = frame.portrait:GetTexCoord() + if l ~= iconCoords[1] or t ~= iconCoords[3] or l2 ~= iconCoords[1] + or b ~= iconCoords[4] or r ~= iconCoords[2] or t2 ~= iconCoords[3] + or r2 ~= iconCoords[2] or b2 ~= iconCoords[4] then + frame.portrait:SetTexCoord(unpack(iconCoords)) + end end else - frame.portrait:SetTexCoord(0, 1, 0, 1) + local l, t, l2, b, r, t2, r2, b2 = frame.portrait:GetTexCoord() + if l ~= 0 or t ~= 0 or l2 ~= 0 or b ~= 1 or r ~= 1 or t2 ~= 0 or r2 ~= 1 or b2 ~= 1 then + frame.portrait:SetTexCoord(0, 1, 0, 1) + end end end module.enable = function(self) -- overwrite portraits set by the game - local original = UnitFrame_Update - UnitFrame_Update = function(arg1, arg2, arg3) - original(arg1, arg2, arg3) - UpdatePortraits(this) - end + ShaguTweaks.hooksecurefunc("UnitFrame_Update", function(frame) + UpdatePortraits(type(frame) == "table" and frame.unit and frame or this) + end) -- handle portrait update events local events = CreateFrame("Frame") events:RegisterEvent("PLAYER_ENTERING_WORLD") events:RegisterEvent("UNIT_PORTRAIT_UPDATE") + local function RefreshFrame(frame) + if not frame or not frame.unit then return end + if event == "PLAYER_ENTERING_WORLD" or frame.unit == arg1 + or (arg1 and UnitIsUnit(frame.unit, arg1)) then UpdatePortraits(frame) end + end events:SetScript("OnEvent", function() - UpdatePortraits(PlayerFrame) - UpdatePortraits(TargetFrame) - UpdatePortraits(PartyMemberFrame1) - UpdatePortraits(PartyMemberFrame2) - UpdatePortraits(PartyMemberFrame3) - UpdatePortraits(PartyMemberFrame4) + RefreshFrame(PlayerFrame) + RefreshFrame(TargetFrame) + RefreshFrame(PartyMemberFrame1) + RefreshFrame(PartyMemberFrame2) + RefreshFrame(PartyMemberFrame3) + RefreshFrame(PartyMemberFrame4) end) -- Turtle/Vanilla already runs TargetofTarget_OnUpdate for the target-of-target diff --git a/mods/unitframes-personal-resource-display.lua b/mods/unitframes-personal-resource-display.lua index 8a4ffbf..0958186 100644 --- a/mods/unitframes-personal-resource-display.lua +++ b/mods/unitframes-personal-resource-display.lua @@ -95,18 +95,32 @@ module.enable = function(self) percent = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") percent:SetPoint("LEFT", bar, "LEFT", 4, 0) end - local function Appearance() + local function TextureAppearance() local texture = source:GetStatusBarTexture() - bar:SetStatusBarTexture(texture and texture:GetTexture() or "Interface\\TargetingFrame\\UI-StatusBar") + local path = texture and texture:GetTexture() or "Interface\\TargetingFrame\\UI-StatusBar" + if state.texturePath ~= path then + state.texturePath = path + bar:SetStatusBarTexture(path) + end + end + local function ColorAppearance() local r, g, b = source:GetStatusBarColor() - bar:SetStatusBarColor(r, g, b, 1) + if state.r ~= r or state.g ~= g or state.b ~= b then + state.r, state.g, state.b = r, g, b + bar:SetStatusBarColor(r, g, b, 1) + end end local function Values() local minimum, maximum = source:GetMinMaxValues() local value = source:GetValue() or 0 maximum = maximum or 0 - bar:SetMinMaxValues(minimum or 0, maximum > 0 and maximum or 1) - bar:SetValue(value) + minimum = minimum or 0 + local rangeChanged = state.minimum ~= minimum or state.maximum ~= maximum + if rangeChanged then + state.minimum = minimum + bar:SetMinMaxValues(minimum, maximum > 0 and maximum or 1) + end + if rangeChanged or state.value ~= value then bar:SetValue(value) end if value ~= state.value or maximum ~= state.maximum then state.value, state.maximum = value, maximum text:SetText(ShaguTweaks.Abbreviate(value)) @@ -115,9 +129,11 @@ module.enable = function(self) end ShaguTweaks.hooksecurefunc(source, "SetValue", Values) ShaguTweaks.hooksecurefunc(source, "SetMinMaxValues", Values) - ShaguTweaks.hooksecurefunc(source, "SetStatusBarColor", Appearance) - ShaguTweaks.hooksecurefunc(source, "SetStatusBarTexture", Appearance) - Appearance(); Values() + ShaguTweaks.hooksecurefunc(source, "SetStatusBarColor", ColorAppearance) + ShaguTweaks.hooksecurefunc(source, "SetStatusBarTexture", function() + TextureAppearance(); ColorAppearance() + end) + TextureAppearance(); ColorAppearance(); Values() if attachedTo then frame:SetParent(attachedTo) frame:SetScale(1) diff --git a/mods/worldmap-colors.lua b/mods/worldmap-colors.lua index f1741a5..c54d648 100644 --- a/mods/worldmap-colors.lua +++ b/mods/worldmap-colors.lua @@ -22,6 +22,15 @@ end local partyUnits = { "party1", "party2", "party3", "party4" } local UPDATE_INTERVAL = .1 +local partyNames = {} + +local function RefreshPartyNames() + for name in pairs(partyNames) do partyNames[name] = nil end + for i = 1, 4 do + local name = UnitName(partyUnits[i]) + if name then partyNames[name] = true end + end +end local function SetAllPointsOffset(frame, parent, offset) frame:SetPoint("TOPLEFT", parent, "TOPLEFT", offset, -offset) @@ -61,7 +70,7 @@ local function UpdateIcon(frame, fallback) if not unit and not name then return end local icon = _G[frame:GetName() .. "Icon"] - if icon then icon:SetTexture() end + if icon and icon:GetTexture() then icon:SetTexture() end -- create icon if not yet existing if not frame.texture then @@ -70,13 +79,7 @@ local function UpdateIcon(frame, fallback) end -- check if unit is in same party group - local ingroup - for i = 1, 4 do - if UnitName(partyUnits[i]) == name then - ingroup = true - break - end - end + local ingroup = name and partyNames[name] -- update texture according to raid/group state if ingroup and frame.texture.ingroup ~= "PARTY" then @@ -97,10 +100,10 @@ local function UpdateIcon(frame, fallback) end local color = class and RAID_CLASS_COLORS[class] - if color then - frame.texture:SetVertexColor(color.r, color.g, color.b) - else - frame.texture:SetVertexColor(.5,1,.5) + local r, g, b = color and color.r or .5, color and color.g or 1, color and color.b or .5 + if frame.texture.classR ~= r or frame.texture.classG ~= g or frame.texture.classB ~= b then + frame.texture.classR, frame.texture.classG, frame.texture.classB = r, g, b + frame.texture:SetVertexColor(r, g, b) end end @@ -108,11 +111,13 @@ local function UpdateMapColors() this.elapsed = (this.elapsed or UPDATE_INTERVAL) + arg1 if this.elapsed < UPDATE_INTERVAL then return end this.elapsed = 0 + local partyRefreshed = false local buttons = this.buttons for i = 1, table.getn(buttons) do local frame = _G[buttons[i][1]] if frame and frame:IsVisible() then + if not partyRefreshed then RefreshPartyNames(); partyRefreshed = true end UpdateIcon(frame, buttons[i][2]) end end diff --git a/tests/README.md b/tests/README.md index 3a95a34..69ad63c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,7 +9,7 @@ sh tests/run_all.sh ../tools/lua5.0/lua5.0 The interpreter defaults to `lua5.0` if omitted. The runner discovers all `tests/test_*.lua` files, reports every failure, prints a final count, and exits unsuccessfully if any suite fails. It is developer tooling, is not loaded by the -addon, and has no in-game overhead. The current suite contains 31 test files. +addon, and has no in-game overhead. The current suite contains 45 test files. Individual checks can also be run from the addon root: @@ -119,3 +119,32 @@ Quest-log checks use the inspected Turtle native 512/256-pixel tiles, preserve l Recolor scheduling checks verify immediate/trailing behavior, one pending callback per frame, callback reuse, late children and FIFO cache limits with dynamic name/path changes. Recolor profiling tests verify opt-in traversal counts, recursive root attribution, unique/repeat visits and that stopped captures no longer receive updates. + +Module lifecycle checks cover foreign merchant hook chains and error recovery, +conditional-macro reagent changes, late minimap buttons, Turtle clock map +selection, scaled LFT button dragging, and AoE Loot removal/migration. + +Sell Junk checks cover throttled completion, stalled progress, extended active +sales, merchant close, profit reporting and fresh attempts. Item-cache checks +cover cold/warm metadata, native failure replies, hidden/removed/reused slots, +coalesced refreshes, disabled visuals and native/fallback deferred scheduling. + +Cooldown checks compare native formatting against TimeConvert at color/unit +boundaries, verify no native Lua update driver, shared formatter reuse, CVar +changes without global writes, ownership flags, native number-only children for +legacy indicators, and fallback timer/clock-wrap behavior. Locale checks retain the class mappings and Hunter fallback names +while rejecting obsolete sections and Turtle debuff writes. + +Cast-bar tests cover native duration snapshots and separate reusable fallback +durations, cast/channel direction, pushback, interruption, expiry cancellation, +self-targets, native player fade, target aura layout, mover previews, SuperWoW +timing and older API animation. Unit-frame styling tests cover explicit/legacy +hook contexts, per-unit routing, aliases, startup passes, Druid fixed mana, +packed native colors and corrective updates after artwork overwrites. + +Control-timer checks cover native totem fill/text, replacement/expiry, preview +and disable cleanup, timed/untimed control effects, priority changes, silent +school-lockout extensions and older API fallback. Cosmetic checks count skipped +resource-copy writes, party-name lookups and map tint/icon writes, and retain +per-tab spam suppression while pruning the cache in place. These operation +counts do not measure in-game CPU or FPS gains. diff --git a/tests/fixtures/module_mock.lua b/tests/fixtures/module_mock.lua new file mode 100644 index 0000000..36ef659 --- /dev/null +++ b/tests/fixtures/module_mock.lua @@ -0,0 +1,50 @@ +-- Minimal vanilla frame environment for module lifecycle regression tests. +return function() + local env=setmetatable({},{__index=getfenv(0)});env._G=env + env.ShaguTweaks={T=setmetatable({},{__index=function(_,key)return key end}),API={}} + env.ShaguTweaks.GetGlobalEnv=function()return env end + env.ShaguTweaks.register=function(_,module)env.module=module;return module end + env.frames,env.hooks={},{} + local methods={} + function methods:SetScript(key,value)self.scripts[key]=value end + function methods:GetScript(key)return self.scripts[key] end + function methods:RegisterEvent(key)self.events[key]=true end + function methods:UnregisterEvent(key)self.events[key]=nil end + function methods:UnregisterAllEvents()self.events={}end + function methods:Hide()self.visible=false end + function methods:Show() + local was=self.visible;self.visible=true + if not was and self.scripts.OnShow then + local before=env.this;env.this=self;self.scripts.OnShow();env.this=before + end + end + function methods:IsVisible()return self.visible end + function methods:GetNormalTexture()return self end + function methods:SetText(value)self.text=value end + function methods:GetText()return self.text end + function methods:SetAlpha(value)self.alpha=value end + function methods:GetName()return self.name end + function methods:GetID()return self.id end + env.CreateFrame=function(_,name,parent) + local frame=setmetatable({scripts={},events={},visible=true,alpha=1,name=name,parent=parent}, + {__index=function(_,key) + if methods[key] then return methods[key] end + if string.find(key,'^[A-Z]') then return function()end end + end}) + table.insert(env.frames,frame);if name then env[name]=frame end;return frame + end + env.ShaguTweaks.hooksecurefunc=function(name,callback)env.hooks[name]=callback end + env.ShaguTweaks.HookScript=function(frame,script,callback)frame:SetScript(script,callback)end + env.DEFAULT_CHAT_FRAME={AddMessage=function()end} + env.Invoke=function(frame,script,event,payload) + local oldThis,oldEvent,oldArg=env.this,env.event,env.arg1 + env.this,env.event,env.arg1=frame,event,payload + frame.scripts[script]() + env.this,env.event,env.arg1=oldThis,oldEvent,oldArg + end + env.Load=function(path) + local f=assert(io.open(path));local source=f:read('*a');f:close() + local chunk=assert(loadstring(source,'@'..path));setfenv(chunk,env);chunk() + end + return env +end diff --git a/tests/test_buy_em_all_hooks.lua b/tests/test_buy_em_all_hooks.lua new file mode 100644 index 0000000..c056b35 --- /dev/null +++ b/tests/test_buy_em_all_hooks.lua @@ -0,0 +1,54 @@ +local root=(arg[1] or '.')..'/' +local env=dofile(root..'tests/fixtures/module_mock.lua')() +local api=env.ShaguTweaks.API +for _,key in ipairs({'GetItemCount','GetItemMaxStackSizeByID','GetItemFamily', + 'GetContainerNumFreeSlots','GetContainerItemID','GetContainerItemStackCount', + 'GetMerchantItemInfo','GetMerchantItemID','IsShiftKeyDown','IsControlKeyDown'}) do api[key]=function()end end +env.IsAddOnLoaded=function()return false end +env.GetLocale=function()return 'enUS'end +env.StaticPopupDialogs={} +env.MerchantFrame=env.CreateFrame();env.MerchantFrame.selectedTab=1 +local native,handled,foreign=0,0,0 +local lastButton,lastIgnore +env.MerchantItemButton_OnClick=function(button,ignore) + native=native+1;lastButton,lastIgnore=button,ignore;return 'native',nil,3 +end +env.Load(root..'mods/buy-em-all.lua') +local buy=env.STBuyEmAll +local realHandle=buy.HandleMerchantClick +buy.CreateUI=function()env.STBuyEmAllFrame=env.CreateFrame()end +buy.HandleMerchantClick=function()handled=handled+1;return false end +env.module:enable() +local installer=env.frames[table.getn(env.frames)] +local first=env.MerchantItemButton_OnClick +for i=1,10 do env.Invoke(installer,'OnEvent','MERCHANT_SHOW')end +assert(env.MerchantItemButton_OnClick==first) +local saved=first +env.MerchantItemButton_OnClick=function(button,ignore) + foreign=foreign+1;return saved(button,ignore) +end +env.Invoke(installer,'OnEvent','MERCHANT_SHOW') +local a,b,c=env.MerchantItemButton_OnClick('RightButton',nil) +assert(native==1 and handled==1 and foreign==1 and a=='native' and b==nil and c==3) +assert(lastButton=='RightButton' and lastIgnore==nil) +-- Error paths must release the guard so a following click is handled normally. +buy.HandleMerchantClick=function()error('expected click failure')end +assert(not pcall(env.MerchantItemButton_OnClick,'LeftButton')) +buy.HandleMerchantClick=function()handled=handled+1;return true end +env.MerchantItemButton_OnClick('LeftButton');assert(handled==2 and native==1) +-- The single-item route must reach the predecessor with modifiers bypassed. +buy.HandleMerchantClick=realHandle +buy.GetMerchantData=function()return 'Item',123,1,1,1 end +buy.FreeBagSpace=function()return 10,20 end +api.IsShiftKeyDown=function()return true end;api.IsControlKeyDown=function()return false end +env.GetMoney=function()return 100 end +env.ChatFrameEditBox={IsVisible=function()return false end} +env.this={GetID=function()return 1 end} +env.MerchantItemButton_OnClick('RightButton') +assert(native==2 and lastButton=='LeftButton' and lastIgnore==1) +-- A foreign handler that replaces the chain entirely remains reachable too. +env.MerchantItemButton_OnClick=function()return 'replacement'end +env.Invoke(installer,'OnEvent','MERCHANT_SHOW') +buy.HandleMerchantClick=function()return false end +assert(env.MerchantItemButton_OnClick('LeftButton')=='replacement') +print('Buy Em All: immutable hook predecessors, foreign wrapper/replacement, one dispatch, error recovery, native return values and single-item routing passed') diff --git a/tests/test_castbar_mover.lua b/tests/test_castbar_mover.lua index 102d0ea..f789953 100644 --- a/tests/test_castbar_mover.lua +++ b/tests/test_castbar_mover.lua @@ -1,17 +1,22 @@ local root=(arg[1] or '.')..'/' local methods={} +local frames={} local function object() - return setmetatable({scripts={}}, {__index=function(t,k) return methods[k] or function() end end}) + local f=setmetatable({scripts={}}, {__index=function(t,k) return methods[k] or function() end end}) + table.insert(frames,f);return f end function methods:SetScript(k,v) self.scripts[k]=v end function methods:CreateTexture() return object() end function methods:CreateFontString() return object() end function methods:SetPoint(...) self.point=arg end function methods:ClearAllPoints() self.point=nil end +function methods:SetVertexColor(...)self.tint=arg end UIParent=object();CastingBarFrame=object() CreateFrame=function(_,name) local f=object();if name then _G[name]=f end;return f end ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end}),GetGlobalEnv=function() return _G end} dofile(root..'api.lua') +local keys=false +ShaguTweaks.API.AreMoveModifiersDown=function()return keys end function ShaguTweaks:register() error('Cast-bar checkbox must not be registered') end ShaguTweaks_config={['Movable Cast Bar']=0,CastBarPosition={123,234}} local f=assert(io.open(root..'main.lua'));local source=f:read('*a');f:close();assert(loadstring(source)) @@ -25,6 +30,14 @@ assert(CastingBarFrame.point[4]==123 and CastingBarFrame.point[5]==234) CastingBarFrame:SetPoint('BOTTOM',UIParent,'BOTTOM',1,2) assert(CastingBarFrame.point[4]==123) local mover=ShaguTweaksCastBarMover +local listener +for _,frame in ipairs(frames)do if frame~=mover and frame.scripts.OnEvent then listener=frame end end +for _,palette in ipairs({{r=.3,g=.3,b=.3,a=.9},{r=1,g=.78,b=.4,a=1},{r=1,g=1,b=1,a=1}})do + ShaguTweaks.UITint=palette + event='MODIFIER_STATE_CHANGED';keys=true;listener.scripts.OnEvent() + assert(mover.border.tint[1]==palette.r and mover.border.tint[2]==palette.g and mover.border.tint[4]==palette.a) + keys=false;listener.scripts.OnEvent() +end initialize();assert(ShaguTweaksCastBarMover==mover) arg1='RightButton';mover.scripts.OnMouseUp() assert(not ShaguTweaks_config.CastBarPosition and CastingBarFrame.point[5]==60) diff --git a/tests/test_castbar_native.lua b/tests/test_castbar_native.lua new file mode 100644 index 0000000..7f33e41 --- /dev/null +++ b/tests/test_castbar_native.lua @@ -0,0 +1,155 @@ +local root=(arg[1] or '.')..'/' +local function read(path)local f=assert(io.open(root..path));local s=f:read('*a');f:close();return s end +local function Check(native) + local env=setmetatable({},{__index=getfenv(0)});env._G=env + local frames,modules,timers,queue={},{},{},{} + local now,cast,channel,selfTarget,queries=10,nil,nil,false,0 + local keys,remote=false,nil + local methods={} + local function object(kind,name,parent) + local f=setmetatable({kind=kind,name=name,parent=parent,scripts={},events={},shown=true,width=140,alpha=1}, + {__index=function(_,key)return methods[key] or (string.find(key,'^[A-Z]') and function()end or nil)end}) + table.insert(frames,f);if name then env[name]=f end;return f + end + function methods:SetScript(k,v)self.scripts[k]=v end + function methods:GetScript(k)return self.scripts[k]end + function methods:RegisterEvent(k)self.events[k]=true end + function methods:Hide()self.shown=false end + function methods:Show()self.shown=true end + function methods:IsShown()return self.shown end + function methods:SetPoint(...)self.point=arg end + function methods:ClearAllPoints()self.point=nil end + function methods:GetFont()return 'font',12,'' end + function methods:GetAlpha()return self.alpha end + function methods:SetAlpha(a)self.alpha=a end + function methods:SetWidth(w)self.width=w end + function methods:GetWidth()return self.width end + function methods:GetHeight()return 10 end + function methods:GetEffectiveScale()return 1 end + function methods:SetTexture(t)self.path=t end + function methods:SetText(t)self.text=t end + function methods:SetVertexColor(...)self.tint=arg end + function methods:SetBackdropBorderColor(...)self.rim=arg end + function methods:SetMinMaxValues(a,b)self.low,self.high=a,b;self.timer=nil end + function methods:SetValue(v)self.value=v;self.writes=(self.writes or 0)+1 end + function methods:SetTimerDuration(d,_,direction)self.timer,self.direction=d,direction end + function methods:GetStatusBarTexture()if not self.fill then self.fill=object('Texture')end;return self.fill end + function methods:CreateTexture()return object('Texture')end + function methods:CreateFontString()return object('FontString')end + env.CreateFrame=object + env.UIParent=object('Frame');env.TargetFrame=object('Frame');env.TargetofTargetFrame=object('Frame') + env.TargetFrameBuff1=object('Texture');env.TargetFrameBuff1:Hide() + env.TargetofTargetFrame:Hide() + env.CastingBarFrame=object('StatusBar');env.CastingBarText=object('FontString') + env.GetTime=function()return now end + env.UnitExists=function()return true end + env.UnitIsUnit=function(a,b)return a==b or selfTarget and ((a=='target' and b=='player') or (b=='target' and a=='player'))end + env.Enum={StatusBarInterpolation={Immediate=0},StatusBarTimerDirection={ElapsedTime=0,RemainingTime=1}} + env.C_EventUtils={IsEventValid=function()return true end} + env.C_Timer={NewTimer=function(delay,callback) + local t={at=now+delay,callback=callback};function t:Cancel()self.cancelled=true end + table.insert(timers,t);return t + end} + env.C_DurationUtil={CreateDuration=function() + local d={} + function d:SetTimeSpan(a,b)self.start,self.finish=a,b end + function d:GetStartTime()return self.start end + function d:GetEndTime()return self.finish end + return d + end,CreateDurationTextBinding=function() + local b={};function b:SetFontString(v)self.text=v end + function b:SetFormatter(v)self.formatter=v end + function b:SetExpiredText(v)self.expired=v end + function b:SetDuration(v)self.duration=v end + function b:Enable()self.enabled=true end + function b:Disable()self.enabled=false end + return b + end} + env.C_StringUtil={CreateNumericRuleFormatter=function()return {AddBreakpoint=function(self,v)self.rule=v end}end} + local suppliedDuration + env.UnitCastingDuration=function()return suppliedDuration end + env.UnitChannelDuration=env.UnitCastingDuration + env.ShaguTweaks={T=setmetatable({},{__index=function(_,k)return k end}),API={durationtimers=native,casts=true,eventutils=true,timer=true}, + libcast={db={}},GetGlobalEnv=function()return env end} + env.ShaguTweaks.UnitCastingInfo=function() + if remote then return 'Remote','','','remote',remote.start*1000,remote.finish*1000 end + end + function env.ShaguTweaks:register(m)modules[m.title]=m;return m end + env.ShaguTweaks.HookScript=function(frame,name,callback) + local original=frame.scripts[name];frame.scripts[name]=function()if original then original()end;callback()end + end + env.ShaguTweaks.QueueFunction=function(callback)table.insert(queue,callback)end + local API=env.ShaguTweaks.API + API.Defer=env.ShaguTweaks.QueueFunction + API.RegisterUnitEvent=function(f,e,a,b)f.events[e]={a,b}end + API.AreMoveModifiersDown=function()return keys end + API.UnitGUID=function()return 'guid' end + API.GetCastInfo=function()queries=queries+1;if cast then return cast.name or 'Cast','','','icon',cast.start*1000,cast.finish*1000 end end + API.GetChannelInfo=function()if channel then return 'Channel','','','icon',channel.start*1000,channel.finish*1000 end end + local src=read('api.lua');local a=assert(string.find(src,'API.GetCastDuration = function',1,true));local b=assert(string.find(src,'API.IsSpellInRange =',a,true)) + local helper=assert(loadstring('local API=ShaguTweaks.API;local _G=ShaguTweaks.GetGlobalEnv();'..string.sub(src,a,b-1)));setfenv(helper,env);helper() + local function load(path)local fn=assert(loadfile(root..path));setfenv(fn,env);fn()end + load('mods/target-castbar.lua');modules['Enemy Castbars']:enable() + load('mods/improved-castbar.lua');modules['Improved Castbar']:enable() + local target,player=env.ShaguTargetCastbar,env.STImprovedCastbar + local function emit(name,unit) + env.event,env.arg1=name,unit + for _,frame in ipairs(frames)do if frame.events[name] and frame.scripts.OnEvent then env.this=frame;frame.scripts.OnEvent()end end + env.this=nil + end + local function runQueue()local work=queue;queue={};for _,fn in ipairs(work)do fn()end end + cast={start=9,finish=15} + suppliedDuration=env.C_DurationUtil.CreateDuration();suppliedDuration:SetTimeSpan(9,15) + emit('UNIT_SPELLCAST_START','target');emit('UNIT_SPELLCAST_START','player') + assert(target.shown and player.shown) + if native then + assert(target.timer==suppliedDuration and target.direction==0 and not target.scripts.OnUpdate) + assert(target.spark.point[2]==target:GetStatusBarTexture()) + assert(player.timerBinding.enabled and player.timerBinding.duration==suppliedDuration and not player.scripts.OnUpdate) + assert(player.timerBinding.formatter.rule.format=='%.1fs') + assert(env.CastingBarFrame.timer==nil,'player native fill/fade must remain untouched') + local old=target.expiry + -- Pushback replaces the snapshot and cancels the previous expiry. + cast.finish=17;emit('UNIT_SPELLCAST_DELAYED','target');emit('UNIT_SPELLCAST_DELAYED','player') + assert(old.cancelled and target.timer.finish==17 and player.timerBinding.duration.finish==17) + assert(suppliedDuration.finish==15,'a mismatched engine snapshot must not be mutated') + local fallback=target.fallbackDuration + cast=nil;suppliedDuration=nil;channel={start=10,finish=18};emit('UNIT_SPELLCAST_CHANNEL_START','target');emit('UNIT_SPELLCAST_CHANNEL_START','player') + assert(target.direction==1 and target.fallbackDuration==fallback and player.timerBinding.enabled) + channel.finish=16;emit('UNIT_SPELLCAST_CHANNEL_UPDATE','target');assert(target.timer.finish==16) + -- Aura layout work coalesces; animation does not query casts or write values. + emit('UNIT_AURA','target');emit('UNIT_AURA','target');assert(table.getn(queue)==1);runQueue() + env.TargetofTargetFrame:Show();env.TargetofTargetFrame.scripts.OnShow();runQueue();assert(target.positionY==-24) + local activeTimer=target.timer + env.ShaguTweaks.UITint={r=1,g=.78,b=.4,a=1};env.ShaguTweaks.UIBorder={r=.8,g=.62,b=.32,a=1} + keys=true;emit('MODIFIER_STATE_CHANGED');assert(env.ShaguTweaksTargetCastBarMover.shown and target.timer==activeTimer) + assert(env.ShaguTweaksTargetCastBarMover.border.tint[2]==.78 and env.ShaguTweaksTargetCastBarMover.icon.rim[1]==.8) + keys=false;emit('MODIFIER_STATE_CHANGED');assert(not env.ShaguTweaksTargetCastBarMover.shown) + local before=queries;now=11;assert(queries==before and not target.writes) + -- Cancel/stop clears target immediately, retaining player text for native fade. + channel=nil;emit('UNIT_SPELLCAST_INTERRUPTED','target');emit('UNIT_SPELLCAST_STOP','player') + assert(not target.shown and not target.timer and not player.timerBinding.enabled and player.shown) + env.CastingBarFrame.scripts.OnHide();assert(not player.shown) + -- Missing STOP still expires; replacement casts cannot be hidden by old timers. + cast={start=11,finish=15};emit('UNIT_SPELLCAST_START','target');local expiry=target.expiry + cast.finish=19;emit('UNIT_SPELLCAST_DELAYED','target');assert(expiry.cancelled) + now=19;target.expiry.callback();assert(not target.shown and not target.timer) + -- Player-only events refresh a self-target; unrelated player casts do not. + now=20;cast={start=20,finish=25};selfTarget=true;emit('UNIT_SPELLCAST_START','player');assert(target.shown) + cast=nil;emit('UNIT_SPELLCAST_STOP','player');assert(not target.shown) + selfTarget=false;cast={start=20,finish=25};emit('UNIT_SPELLCAST_START','player');assert(not target.shown) + cast=nil;emit('PLAYER_TARGET_CHANGED');assert(not target.shown) + -- Complete remote timing still uses the optional GUID cache complement. + env.ShaguTweaks.superwow_active=true;env.ShaguTweaks.libcast.db.guid={cast='Remote'} + remote={start=20,finish=26};env.ShaguTweaks.libcast.OnRemoteChanged('guid') + assert(target.shown and target.timer.finish==26 and target.text.text=='Remote') + remote=nil;env.ShaguTweaks.libcast.db.guid=nil;env.ShaguTweaks.libcast.OnRemoteChanged('guid') + assert(not target.shown) + else + assert(target.scripts.OnUpdate and player.scripts.OnUpdate and not player.timerBinding) + env.this=target;target.scripts.OnUpdate();assert(target.value==1) + env.this=player;env.arg1=.1;player.scripts.OnUpdate();assert(player.timerText.text=='5.0s') + end +end +Check(true);Check(false) +print('Castbars: native snapshot/fallback durations, fill/spark/text ownership, delay/channel replacement, cancellation/expiry, player fade, self-target routing and older API animation passed') diff --git a/tests/test_control_timers_native.lua b/tests/test_control_timers_native.lua new file mode 100644 index 0000000..568613d --- /dev/null +++ b/tests/test_control_timers_native.lua @@ -0,0 +1,187 @@ +local root=(arg[1] or '.')..'/' +local function Check(native) + local env=setmetatable({},{__index=getfenv(0)});env._G=env + local modules,frames,timers,slots,effects={},{},{},{},{} + local now,keys,totemReads,effectReads,schoolReads=10,false,0,0,0 + local methods={} + local function object(kind,name,parent) + local f=setmetatable({kind=kind,parent=parent,scripts={},events={},shown=true,writes=0}, + {__index=function(_,key)return methods[key] or (string.find(key,'^[A-Z]') and function()end or nil)end}) + table.insert(frames,f);if name then env[name]=f end;return f + end + function methods:RegisterEvent(name)self.events[name]=true end + function methods:UnregisterAllEvents()self.events={}end + function methods:SetScript(k,v)self.scripts[k]=v end + function methods:Show()self.shown=true end + function methods:Hide()self.shown=false end + function methods:IsShown()return self.shown end + function methods:CreateTexture()return object('Texture',nil,self)end + function methods:CreateFontString()return object('FontString',nil,self)end + function methods:SetTexture(path)self.path=path;self.writes=self.writes+1 end + function methods:SetText(text)self.text=text;self.writes=self.writes+1 end + function methods:SetTextColor(...)self.textColor=arg end + function methods:SetStatusBarColor(...)self.color=arg end + function methods:SetVertexColor(...)self.color=arg end + function methods:SetWidth(n)self.width=n end + function methods:SetStatusBarTexture(p)self.path=p end + function methods:SetMinMaxValues()self.timer=nil end + function methods:SetValue(v)self.value=v end + function methods:SetTimerDuration(d,_,direction)self.timer,self.direction=d,direction end + env.CreateFrame=object;env.UIParent=object('Frame') + env.SlashCmdList={};env.ShaguTweaks_config={} + env.GetTime=function()return now end + env.UnitClass=function()return 'Shaman','SHAMAN' end + env.GameTooltip={Hide=function()end,SetOwner=function()end,SetTotem=function()end} + env.Enum={StatusBarInterpolation={Immediate=0},StatusBarTimerDirection={RemainingTime=1},NumericRuleFormatRounding={Down=2}} + env.C_Timer={NewTimer=function(delay,fn) + local t={at=now+delay,callback=fn};function t:Cancel()self.cancelled=true end + table.insert(timers,t);return t + end} + env.C_DurationUtil={CreateDuration=function() + local d={finish=0,total=0} + function d:SetTimeFromEnd(finish,total)self.finish,self.total=finish,total end + function d:GetRemainingDuration()return math.max(0,self.finish-now)end + return d + end,CreateDurationTextBinding=function() + local b={} + function b:SetFontString(text)self.text=text end + function b:SetDuration(d)self.duration=d end + function b:SetFormatter(f)self.formatter=f end + function b:SetExpiredText(t)self.expired=t end + function b:SetUpdateInterval(t)self.interval=t end + function b:Enable()self.enabled=true end + function b:Disable()self.enabled=false end + return b + end} + env.C_StringUtil={CreateNumericRuleFormatter=function()return {SetBreakpoints=function(self,rules)self.rules=rules end}end} + env.GetTotemInfo=function(slot) + local data=slots[slot] + if data then return true,'Totem',data.start,data.total,'icon' end + return false,'',0,0,'' + end + env.GetTotemTimeLeft=function(slot)totemReads=totemReads+1;local s=slots[slot];return s and math.max(0,s.start+s.total-now)or 0 end + env.GetTotemDuration=function(slot) + local d=env.C_DurationUtil.CreateDuration();local s=slots[slot] + if s then d:SetTimeFromEnd(s.start+s.total,s.total)end + return d + end + env.C_LossOfControl={GetActiveLossOfControlDataCount=function()effectReads=effectReads+1;return table.getn(effects)end, + GetActiveLossOfControlData=function(i) + local e=effects[i] + return {locType=e.kind,timeRemaining=e.finish and math.max(0,e.finish-now),duration=e.total, + displayText=e.label or e.kind,lockoutSchool=e.mask or 0,iconTexture='effect'} + end,GetSchoolLockout=function() + schoolReads=schoolReads+1;local mask,left=0,0 + for _,e in ipairs(effects)do if e.kind=='SCHOOL_INTERRUPT' and e.finish>now then mask=e.mask;left=math.max(left,e.finish-now)end end + return mask,mask~=0 and left or nil + end} + env.C_Spell={GetSchoolString=function()return 'Nature' end} + env.ShaguTweaks={T=setmetatable({},{__index=function(_,k)return k end}), + API={durationtimers=native,AreMoveModifiersDown=function()return keys end},GetGlobalEnv=function()return env end} + function env.ShaguTweaks:register(m)modules[m.title]=m;return m end + local function load(path)local f=assert(loadfile(root..path));setfenv(f,env);f()end + local function emit(name) + env.event=name + for _,f in ipairs(frames)do if f.events[name]then env.this=f;f.scripts.OnEvent()end end + env.this=nil + end + local function advance(delta) + local finish=now+delta + for iteration=1,1000 do + local nextTimer + for _,t in ipairs(timers)do + if not t.cancelled and not t.fired and t.at<=finish and (not nextTimer or t.at=candidate.threshold then rule=candidate end end + local function rounded(v,step,mode) + if not step then return v end + return (mode==2 and math.floor(v/step) or math.floor(v/step+.5))*step + end + n=rounded(n,rule.step,rule.rounding) + if rule.components then + local values={} + for _,part in ipairs(rule.components)do + local v=n;if part.div then v=v/part.div end;if part.mod then v=math.mod(v,part.mod)end + table.insert(values,rounded(v,part.step,part.rounding)) + end + return string.format(rule.format,unpack(values)) + end + return string.format(rule.format,n) + end + if native then + assert(not holder.scripts.OnUpdate and totem.binding.enabled and totem.bar.timer.total==30 and totem.bar.direction==1) + assert(totem.binding.formatter==env.ShaguTweaksTotem3.binding.formatter) + assert(totem.binding.formatter.rules[4].format=='|cffffffff%d:%02d') + assert(formatted(totem.binding.formatter,5)=='|cffff33335') + assert(formatted(totem.binding.formatter,5.001)=='|cffffe6335') + assert(formatted(totem.binding.formatter,10)=='|cffffe63310') + assert(formatted(totem.binding.formatter,10.001)=='|cffffffff10') + assert(formatted(totem.binding.formatter,61.9)=='|cffffffff1:01') + local original=totem.expiry;slots[1]={start=10,total=60};emit('PLAYER_TOTEM_UPDATE') + assert(original.cancelled and totem.bar.timer.finish==70) + local oldTimer=totem.expiry;keys=true;emit('MODIFIER_STATE_CHANGED') + assert(oldTimer.cancelled and not totem.binding.enabled and totem.bar.value==1) + keys=false;emit('MODIFIER_STATE_CHANGED');assert(totem.binding.enabled) + local before=totemReads;advance(61);assert(totemReads==before and not holder.shown) + assert(not totem.binding.enabled and not totem.shown) + slots[2]={start=now,total=30};emit('PLAYER_TOTEM_UPDATE') + local earth=env.ShaguTweaksTotem2;local pending=earth.expiry + modules['Totem Timers']:disable();assert(pending.cancelled and not earth.binding.enabled) + modules['Totem Timers']:enable();assert(earth.binding.enabled) + else + assert(holder.scripts.OnUpdate and not totem.binding) + assert(totem.time.text=='29') + now=36;env.arg1=.1;holder.scripts.OnUpdate();assert(totem.time.text=='3' and totem.time.textColor[2]==.2) + end + modules['Totem Timers']:disable() + effects={{kind='ROOT',finish=now+20,total=20},{kind='STUN',finish=now+4,total=4}} + load('mods/loss-of-control.lua');modules['Loss of Control Alert']:enable() + local alert=env.ShaguTweaksLossOfControl + assert(alert.title.text=='Stunned') + if native then + assert(not alert.scripts.OnUpdate and alert.bar.timer.total==4) + local before=effectReads;local writes=alert.icon.writes + advance(2);assert(effectReads==before and alert.icon.writes==writes,'native animation must not rescan/repaint') + advance(2.1);assert(alert.title.text=='Rooted' and alert.shown) + effects={};emit('LOSS_OF_CONTROL_UPDATE');assert(not alert.shown) + effects={{kind='STUN'}};emit('LOSS_OF_CONTROL_UPDATE') + assert(alert.shown and alert.time.text=='' and not alert.bar.shown) + keys=true;emit('MODIFIER_STATE_CHANGED');assert(alert.time.text=='8.0' and alert.bar.value==1) + keys=false;emit('MODIFIER_STATE_CHANGED');assert(alert.time.text=='' and alert.shown) + effects={{kind='SCHOOL_INTERRUPT',mask=8,finish=now+6,total=6}} + emit('LOSS_OF_CONTROL_UPDATE');local listReads=effectReads;advance(.4) + assert(effectReads==listReads and schoolReads>0,'steady lockouts use scalar queries only') + local oldEnd=alert.bar.timer.finish + effects[1].finish=now+10;effects[1].total=10 + advance(.3);assert(alert.bar.timer.finish>oldEnd and effectReads>listReads) + local scalarQuery=env.C_LossOfControl.GetSchoolLockout + env.C_LossOfControl.GetSchoolLockout=nil + listReads=effectReads;advance(.3);assert(effectReads>listReads,'older scalar-less API must still detect extensions') + env.C_LossOfControl.GetSchoolLockout=scalarQuery + advance(6);assert(alert.shown,'old expiry must not hide the extended lockout') + local beforeDisable=effectReads + modules['Loss of Control Alert']:disable();advance(20) + assert(not alert.shown and effectReads==beforeDisable) + modules['Loss of Control Alert']:enable();assert(not alert.shown) + -- A higher-priority newly applied effect replaces the lockout immediately. + effects={{kind='ROOT',finish=now+10,total=10}};emit('LOSS_OF_CONTROL_UPDATE') + table.insert(effects,{kind='FEAR',finish=now+3,total=3});emit('LOSS_OF_CONTROL_ADDED') + assert(alert.title.text=='Feared');advance(3.1);assert(alert.title.text=='Rooted') + else + assert(alert.scripts.OnUpdate and alert.time.text=='4.0') + env.arg1=.05;alert.scripts.OnUpdate();assert(alert.title.text=='Stunned') + end + modules['Loss of Control Alert']:disable() +end +Check(true);Check(false) +print('Control timers: native totem fill/text/expiry, replacements, preview/disable cleanup, loss-of-control priority/untimed effects, school extension scalar checks and older API fallback passed') diff --git a/tests/test_cooldown_native.lua b/tests/test_cooldown_native.lua new file mode 100644 index 0000000..ff53a37 --- /dev/null +++ b/tests/test_cooldown_native.lua @@ -0,0 +1,141 @@ +local root=(arg[1] or '.')..'/' +assert(loadfile(root..'mods/cooldown-numbers.lua')) +local function read(path)local f=assert(io.open(root..path));local s=f:read('*a');f:close();return s end +local helpers=read('helpers.lua') +local a=assert(string.find(helpers,'ShaguTweaks.TimeConvert = function',1,true)) +local b=assert(string.find(helpers,'\nend',a,true)) +local reference={ShaguTweaks={},ceil=math.ceil} +setmetatable(reference,{__index=getfenv(0)}) +local chunk=assert(loadstring(string.sub(helpers,a,b+3)));setfenv(chunk,reference);chunk() +local env=setmetatable({},{__index=getfenv(0)});env._G=env +local frames,formatted,cvar,now={},0,'1',101 +local nativeFactory +local function newFrame(parent) + local f={parent=parent,scripts={},visible=true,alpha=1} + function f:GetParent()return self.parent end + function f:GetHeight()return self.height or 36 end + function f:GetFrameLevel()return 4 end + function f:GetAlpha()return self.alpha end + function f:SetAlpha(n)self.alpha=n end + function f:SetAllPoints()end + function f:SetFrameLevel()end + function f:SetScript(k,v)self.scripts[k]=v end + function f:RegisterEvent(k)self.event=k end + function f:Hide()self.visible=false end + function f:Show()self.visible=true end + function f:CreateFontString() + local text={} + function text:SetFont(font,size,flags)self.font,self.size,self.flags=font,size,flags end + function text:ClearAllPoints()end + function text:SetPoint()end + function text:SetTextColor()end + function text:SetText(value)self.value=value end + return text + end + return f +end +env.CreateFrame=function(kind,_,parent) + if kind=='Cooldown' and env.createNative then return env.createNative(parent) end + local f=newFrame(parent);table.insert(frames,f);return f +end +env.GetTime=function()return now end +env.GetCVar=function()return cvar end +env.SetCVar=function()error('must not change global cooldown policy')end +env.STANDARD_TEXT_FONT='font' +env.Enum={NumericRuleFormatRounding={Up=1}} +env.C_StringUtil={CreateNumericRuleFormatter=function() + formatted=formatted+1 + local formatter={} + function formatter:SetBreakpoints(rules)self.rules=rules end + function formatter:FormatNumber(n) + local rule + for _,candidate in ipairs(self.rules)do if n>=candidate.threshold then rule=candidate end end + local function round(value,step)if step then return math.ceil(value/step)*step end;return value end + local value=round(n,rule.step) + if rule.components then value=round(value/rule.components[1].div,rule.components[1].step)end + return string.format(rule.format,value) + end + return formatter +end} +nativeFactory=env.C_StringUtil.CreateNumericRuleFormatter +local set +env.hooksecurefunc=function(name,callback)assert(name=='CooldownFrame_SetTimer');set=callback end +env.ShaguTweaks={T={},API={nativecooldowns=false},GetGlobalEnv=function()return env end,GetExpansion=function()return 'vanilla' end, + TimeConvert=reference.ShaguTweaks.TimeConvert,register=function(_,module)env.module=module;return module end} +local load=assert(loadfile(root..'mods/cooldown-numbers.lua'));setfenv(load,env);load();env.module:enable() +local parent=newFrame();parent.height=18 +local function native(owner) + local f=newFrame(owner or parent);f.nativeText=f:CreateFontString() + function f:GetCountdownFontString()return self.nativeText end + function f:SetCountdownFormatter(value)self.formatter=value end + function f:SetMinimumCountdownDuration(value)self.minimum=value end + function f:SetHideCountdownNumbers(value)self.hidden=value end + function f:SetDrawSwipe(value)self.swipe=value end + function f:SetDrawEdge(value)self.edge=value end + function f:SetDrawBling(value)self.bling=value end + function f:SetCooldown(start,duration)self.start,self.duration=start,duration;self.visible=true;self.starts=(self.starts or 0)+1 end + return f +end +local cd=native();set(cd,100,20,1) +assert(not cd.cooldowntext and not cd.scripts.OnUpdate and not cd.hidden) +assert(cd.minimum==1999 and cd.nativeText.size==18*.64) +assert(table.getn(frames)==1 and frames[1].event=='CVAR_UPDATE') +local second=native();set(second,100,20,1) +assert(formatted==1 and second.formatter==cd.formatter,'one shared native formatter') +for _,n in ipairs({.01,.99,1,4.01,4.99,5,5.01,9.99,10,59,59.99,60,60.01,3599.99,3600,86399.99,86400,172801})do + assert(cd.formatter:FormatNumber(n)==reference.ShaguTweaks.TimeConvert(n),'TimeConvert mismatch at '..n) +end +set(cd,100,1.5,1);assert(cd.hidden and not cd.cooldowntext) +set(cd,100,2,1);assert(not cd.hidden) +set(cd,0,20,1);assert(cd.hidden) +set(cd,100,20,0);assert(cd.hidden) +set(cd,100,20,1);cd.noCooldownCount=true;set(cd,100,20,1);assert(cd.hidden) +cd.noCooldownCount=nil;cd.pfCooldownType='spell';set(cd,100,20,1);assert(cd.hidden) +cd.pfCooldownType=nil;cd.readable=true;set(cd,100,20,1);assert(cd.hidden) +cd.readable=nil +local foreign=native();foreign.pfCooldownType='spell';set(foreign,100,20,1) +assert(foreign.hidden==nil and foreign.formatter==nil,'must leave foreign-owned native setup alone') +local excluded=native();excluded.noCooldownCount=true;set(excluded,100,20,1) +assert(excluded.hidden and not excluded.cooldowntext and not excluded.formatter) +-- CVar changes switch existing native indicators without setting the CVar. +local watcher=frames[1] +cvar='0';watcher.scripts.OnEvent() +assert(cd.cooldowntext and cd.cooldowntext.visible and cd.hidden) +local function tick(frame,dt)env.this,env.arg1=frame,dt;frame.scripts.OnUpdate();env.this,env.arg1=nil,nil end +tick(cd.cooldowntext,.1);assert(cd.cooldowntext.text.value==reference.ShaguTweaks.TimeConvert(19)) +local frameCount=table.getn(frames);watcher.scripts.OnEvent();assert(table.getn(frames)==frameCount) +cvar='1';watcher.scripts.OnEvent();assert(not cd.cooldowntext.visible and not cd.hidden) +-- Legacy Models keep the old renderer, including alpha, expiry and clock wrap. +local model=newFrame(parent);set(model,100,20,1);assert(model.cooldowntext.visible) +model.alpha=.4;tick(model.cooldowntext,.1);assert(model.cooldowntext.alpha==.4) +now=121;tick(model.cooldowntext,.1);assert(not model.cooldowntext.visible) +local cycle=2^32/1000;now=1;set(model,cycle-2,10,1);tick(model.cooldowntext,.1) +assert(model.cooldowntext.text.value==reference.ShaguTweaks.TimeConvert(7)) +-- Native numeric children optimize legacy Models without changing their sweep. +env.ShaguTweaks.API.nativecooldowns=true;env.createNative=native +local legacy=newFrame(parent);set(legacy,1,20,1) +local numbers=legacy.stCountdownNativeFrame +assert(numbers and not legacy.cooldowntext and not numbers.scripts.OnUpdate) +assert(numbers.parent==legacy and numbers.start==1 and numbers.duration==20) +assert(numbers.swipe==false and numbers.edge==false and numbers.bling==false) +local starts=numbers.starts;set(numbers,1,20,1) +assert(numbers.starts==starts,'native helper must not recursively decorate itself') +set(legacy,1,1,1);assert(not numbers.visible) +set(legacy,1,20,1);assert(numbers.visible) +cvar='0';watcher.scripts.OnEvent();assert(not numbers.visible and legacy.cooldowntext.visible) +cvar='1';watcher.scripts.OnEvent();assert(numbers.visible and not legacy.cooldowntext.visible) +assert(legacy.stCountdownNativeFrame==numbers) +legacy.noCooldownCount=true;set(legacy,1,20,1);assert(not numbers.visible) +-- A rejected native frame creation safely falls back and is not retried. +local attempts=0;env.createNative=function()attempts=attempts+1;error('unsupported frame')end +local unsupported=newFrame(parent);set(unsupported,1,20,1);set(unsupported,1,20,1) +assert(unsupported.cooldowntext.visible and attempts==1) +env.ShaguTweaks.API.nativecooldowns=false +-- Missing formatter APIs retain one renderer, suppressing native duplicates. +env.C_StringUtil.CreateNumericRuleFormatter=nil +local older=native();set(older,1,20,1) +assert(older.hidden and older.cooldowntext.visible) +env.C_StringUtil.CreateNumericRuleFormatter=nativeFactory +set(model,1,1,1);assert(not model.cooldowntext.visible) +model.noCooldownCount=true;set(model,1,20,1);assert(not model.cooldowntext.visible) +print('Cooldown numbers: shared native formatter, TimeConvert boundaries, global-CVar ownership/live changes, foreign flags, GCDs, legacy/older API fallback, alpha/expiry and clock wrap passed') diff --git a/tests/test_cosmetic_writes.lua b/tests/test_cosmetic_writes.lua new file mode 100644 index 0000000..0b6d511 --- /dev/null +++ b/tests/test_cosmetic_writes.lua @@ -0,0 +1,72 @@ +local root=(arg[1] or '.')..'/' +local env=setmetatable({},{__index=getfenv(0)});env._G=env +local partyCalls,frames,partyNames=0,{}, {'Alice','Bob'} +local function texture(path) + local t={path=path,writes=0,tints=0} + function t:GetTexture()return self.path end + function t:SetTexture(p)self.path=p;self.writes=self.writes+1 end + function t:SetVertexColor(r,g,b)self.r,self.g,self.b=r,g,b;self.tints=self.tints+1 end + function t:SetPoint()end + return t +end +env.GetAddOnInfo=function()return nil,'ShaguTweaks' end +env.UnitExists=function()return true end +env.UnitName=function(unit) + local _,_,id=string.find(unit,'party(%d)') + if id then partyCalls=partyCalls+1;return partyNames[tonumber(id)]end + return 'Remote' +end +env.UnitClass=function()return 'Mage','MAGE' end +env.RAID_CLASS_COLORS={WARLOCK={r=.5,g=.5,b=.9},MAGE={r=.2,g=.8,b=.9}} +env.WorldMapFrame={} +env.CreateFrame=function() + local f={scripts={}} + function f:SetScript(k,v)self.scripts[k]=v end + function f:RegisterEvent()end + function f:UnregisterEvent()self.unregistered=true end + table.insert(frames,f);return f +end +env.ShaguTweaks={T={},GetGlobalEnv=function()return env end,GetUnitData=function()return 'WARLOCK' end} +function env.ShaguTweaks:register(m)env.module=m;return m end +for i,name in ipairs({'Alice','Other','Third'})do + local f={name=name,frameName='WorldMapRaid'..i} + function f:GetName()return self.frameName end + function f:IsVisible()return self.visible~=false end + function f:CreateTexture()return texture()end + env[f.frameName]=f;env[f.frameName..'Icon']=texture('native') +end +local chunk=assert(loadfile(root..'mods/worldmap-colors.lua'));setfenv(chunk,env);chunk();env.module:enable() +local updater=frames[1] +local function tick()env.this,env.arg1=updater,.1;updater.scripts.OnUpdate()end +tick();assert(partyCalls==4,'party roster should be read once, not once per visible icon') +assert(env.WorldMapRaid1.texture.ingroup=='PARTY' and env.WorldMapRaid2.texture.ingroup=='RAID') +for i=1,3 do assert(env['WorldMapRaid'..i].texture.tints==1 and env['WorldMapRaid'..i..'Icon'].writes==1)end +tick();assert(partyCalls==8) +for i=1,3 do assert(env['WorldMapRaid'..i].texture.tints==1 and env['WorldMapRaid'..i..'Icon'].writes==1)end +partyNames={};tick();assert(env.WorldMapRaid1.texture.ingroup=='RAID') +env.WorldMapRaid2.name=nil;env.WorldMapRaid2.unit='raid10';tick() +assert(env.WorldMapRaid2.texture.r==.2,'packed map icons must use their actual unit') +env.WorldMapRaid2Icon.path='native again';tick();assert(env.WorldMapRaid2Icon.writes==2) +for i=1,3 do env['WorldMapRaid'..i].visible=false end +local before=partyCalls;tick();assert(partyCalls==before,'solo/empty maps should not query party names') +env.BattlefieldMinimap={};env.this=frames[2];frames[2].scripts.OnEvent();assert(frames[2].unregistered) + +-- Exercise the actual spam cache helpers while retaining the table identities. +local f=assert(io.open(root..'mods/chat-spam-filter.lua'));local source=f:read('*a');f:close() +local a=assert(string.find(source,'local COOLDOWN =',1,true)) +local b=assert(string.find(source,'-- SuperWoW exposes',a,true)) +local now=0 +env.GetTime=function()return now end +env.API={} +chunk=assert(loadstring(string.sub(source,a,b-1)..'return IsDuplicate, cache')) +setfenv(chunk,env);local duplicate,cache=chunk() +local tab1,tab2={},{} +assert(not duplicate(tab1,'repeat') and duplicate(tab1,'repeat')) +assert(not duplicate(tab2,'repeat'),'each chat tab has an independent cache') +local original=cache[tab1] +now=31;assert(not duplicate(tab1,'new') and cache[tab1]==original) +now=61;assert(duplicate(tab1,'repeat') and cache[tab1]==original) +now=70;assert(not duplicate(tab1,'repeat'),'duplicates must not extend their original 70s window') +now=101;assert(not duplicate(tab1,'later') and not original.new and not cache[tab2]) +assert(cache[tab1]==original,'pruning must retain the active per-tab table') +print('Cosmetic/cache writes: four party-name lookups per map pass, stable tint/icon writes skipped, live roster/unit/native changes retained, in-place spam pruning and per-tab cooldowns passed') diff --git a/tests/test_item_cache_retry.lua b/tests/test_item_cache_retry.lua new file mode 100644 index 0000000..ff8d1eb --- /dev/null +++ b/tests/test_item_cache_retry.lua @@ -0,0 +1,105 @@ +local root=(arg[1] or '.')..'/' +assert(loadfile(root..'mods/item-colors.lua')) +local file=assert(io.open(root..'mods/item-colors.lua'));local source=file:read('*a');file:close() +local cut=assert(string.find(source,'function Engine:Install()',1,true)) +local function Check(nativeTimer) + local env=assert(loadfile(root..'tests/fixtures/module_mock.lua'))()() + local qualities,live,queued={},{},{} + local refreshes,hideHooks=0,0 + env.ShaguTweaks.API.eventutils=true + env.ShaguTweaks.API.timer=nativeTimer + env.C_EventUtils={IsEventValid=function()return true end} + env.ShaguTweaks.API.GetItemQualityByID=function(id)return qualities[id]end + env.ShaguTweaks.API.GetItemInfoInstant=function()return nil,nil,nil,nil,nil,0 end + env.GetItemQualityColor=function(q)return q/10,.5,1 end + env.ShaguTweaks.AddBorder=function(button) + if not rawget(button,'ShaguTweaks_border') then + button.ShaguTweaks_border={SetBackdropBorderColor=function(self,r,g,b,a)self.r,self.a=r,a end} + end + return button.ShaguTweaks_border + end + env.ShaguTweaks.HookScript=function(button,script,callback) + hideHooks=hideHooks+1;assert(script=='OnHide');button.scripts[script]=callback + end + local function defer(callback)table.insert(queued,callback)end + env.ShaguTweaks.API.Defer=defer;env.ShaguTweaks.QueueFunction=defer + local chunk=assert(loadstring(string.sub(source,1,cut-1)..'return SetVisualColor, Engine, mailTradeOptions')) + setfenv(chunk,env);local paint,engine,opts=chunk() + engine.borders=true + engine.RefreshAll=function() + refreshes=refreshes+1 + for slot,id in pairs(live) do if slot:IsVisible() then paint(slot,id,opts) end end + end + local function button(id) + local slot=env.CreateFrame('Button');live[slot]=id;paint(slot,id,opts);return slot + end + local a=button(100);local events=env.frames[2] + local b=button(100) + assert(events.events.GET_ITEM_INFO_RECEIVED and a.ShaguTweaks_border.a==0) + paint(a,100,opts);assert(hideHooks==2,'one hide hook per cold button') + local function reply(id,success) + env.arg2=success;env.Invoke(events,'OnEvent','GET_ITEM_INFO_RECEIVED',id);env.arg2=nil + end + local function flush() + local work=queued;queued={} + for _,callback in ipairs(work)do callback()end + end + reply(999,true);assert(table.getn(queued)==0) + qualities[100]=3;reply(100,true);reply(100,true) + assert(table.getn(queued)==1 and not events.events.GET_ITEM_INFO_RECEIVED) + flush();assert(refreshes==1 and a.ShaguTweaks_border.r==.3 and b.ShaguTweaks_border.r==.3) + -- Warm items need neither an event listener nor a new hide hook. + button(100);assert(not events.events.GET_ITEM_INFO_RECEIVED and hideHooks==2) + -- Slot reused before the old reply: refresh reads the new identity. + live[a]=200;paint(a,200,opts) + live[a]=100 -- native slot changed, its ordinary paint has not run yet + qualities[200]=4;reply(200,1);flush() + assert(a.ShaguTweaks_border.r==.3,'old reply must not paint old quality') + -- Removed and hidden slots clear pending state and never resurrect a border. + live[a]=300;paint(a,300,opts);live[a]=nil;paint(a,nil,opts) + reply(300,true);assert(table.getn(queued)==0 and a.ShaguTweaks_border.a==0) + live[a]=400;paint(a,400,opts);a.visible=false;env.Invoke(a,'OnHide') + assert(not events.events.GET_ITEM_INFO_RECEIVED) + reply(400,true);assert(table.getn(queued)==0) + a.visible=true;live[a]=500;paint(a,500,opts);reply(500,false) + assert(not events.events.GET_ITEM_INFO_RECEIVED and table.getn(queued)==0) + paint(a,500,opts);reply(500,0) + assert(not events.events.GET_ITEM_INFO_RECEIVED and table.getn(queued)==0) + paint(a,500,opts);reply(500,nil) -- native ClassicAPI failure encoding + assert(not events.events.GET_ITEM_INFO_RECEIVED and table.getn(queued)==0) + -- Distinct replies in the same frame share one live refresh. + live[a]=600;paint(a,600,opts);live[b]=700;paint(b,700,opts) + qualities[600],qualities[700]=2,4;reply(600,true);reply(700,true) + assert(table.getn(queued)==1);flush() + assert(a.ShaguTweaks_border.r==.2 and b.ShaguTweaks_border.r==.4) + assert(not events.events.GET_ITEM_INFO_RECEIVED) + -- Disabling visuals before a deferred reply prevents repainting. + live[a]=800;paint(a,800,opts);qualities[800]=2;reply(800,true) + local before=refreshes;engine.borders=false;flush();assert(refreshes==before) + -- Older clients retain their ordinary refresh behavior without this event. + env.C_EventUtils.IsEventValid=function()return false end + engine.borders=true;paint(a,900,opts);assert(not events.events.GET_ITEM_INFO_RECEIVED) + -- The same retry works when only rarity glows are enabled. + env.C_EventUtils.IsEventValid=function()return true end + engine.borders=false;engine.glows=true + function a:CreateTexture() + local texture={} + function texture:SetTexture()end + function texture:SetBlendMode()end + function texture:SetPoint()end + function texture:SetVertexColor(r)self.r=r end + function texture:Hide()self.visible=false end + function texture:Show()self.visible=true end + return texture + end + -- Keep this scenario to one visible slot. + live[b]=nil + for slot in pairs(live)do if slot~=a then live[slot]=nil end end + a.ShaguTweaks_itemRarityGlow=false + live[a]=1000;paint(a,1000,opts) + assert(not a.ShaguTweaks_itemRarityGlow.visible) + qualities[1000]=4;reply(1000,1);flush() + assert(a.ShaguTweaks_itemRarityGlow.visible and a.ShaguTweaks_itemRarityGlow.r==.4) +end +Check(true);Check(false) +print('Item cache retry: cold/warm data, shared replies, live slot identities, removed/hidden items, failed replies, coalescing, disabled visuals and timer fallback passed') diff --git a/tests/test_lft_minimap_drag.lua b/tests/test_lft_minimap_drag.lua new file mode 100644 index 0000000..3b7e76f --- /dev/null +++ b/tests/test_lft_minimap_drag.lua @@ -0,0 +1,50 @@ +local root=(arg[1] or '.')..'/' +local f=assert(io.open(root..'mods/minimap-zoom.lua'));local source=f:read('*a');f:close() +assert(loadstring(source)) +local a=assert(string.find(source,'local function FixLFTButtonDrag()',1,true)) +local b=assert(string.find(source,'local module =',a,true)) +local function Check(uiScale,mapScale,buttonScale) + local env=dofile(root..'tests/fixtures/module_mock.lua')() + env.UIParent={effective=uiScale};env.Minimap={effective=uiScale*mapScale} + local calls=0 + local button={parent=env.Minimap,scale=buttonScale,scripts={}} + function button:GetParent()return self.parent end + function button:GetScale()return self.scale end + function button:GetEffectiveScale()return self.parent.effective*self.scale end + function button:SetParent(parent)self.parent=parent end + function button:SetScale(scale)self.scale=scale end + function button:GetLeft()return self.left end + function button:GetTop()return self.top end + function button:ClearAllPoints()self.cleared=true end + function button:SetPoint(point,parent,relative,x,y) + assert(point=='TOPLEFT' and parent==env.UIParent and relative=='BOTTOMLEFT') + self.left,self.top=x,y + end + function button:GetScript(script)return self.scripts[script]end + function button:SetScript(script,callback)self.scripts[script]=callback end + local reset=function()button:SetParent(env.Minimap);button.left,button.top=-22,-14 end + button.scripts.OnMouseUp=reset + button.scripts.OnDragStop=function(mouse) + assert(mouse=='LeftButton');calls=calls+1 + -- Exact native LFT XML behavior: StopMoving, then SetParent('UIParent'). + button:SetParent(env.UIParent) + end + button.left,button.top=1000/button:GetEffectiveScale(),650/button:GetEffectiveScale() + env.LFTMinimapButton=button + local chunk=assert(loadstring('local _G=ShaguTweaks.GetGlobalEnv();\n'..string.sub(source,a,b-1)..'return FixLFTButtonDrag')) + setfenv(chunk,env);local install=chunk() + install();local hook=button.scripts.OnDragStop;install();assert(button.scripts.OnDragStop==hook) + local before=button:GetEffectiveScale() + hook('LeftButton') + assert(calls==1 and button:GetParent()==env.Minimap and button.scale==buttonScale) + assert(math.abs(button:GetEffectiveScale()-before)<.000001) + assert(math.abs(button:GetLeft()*before-1000)<.000001 and math.abs(button:GetTop()*before-650)<.000001) + assert(button.scripts.OnMouseUp==reset) + reset();assert(button.parent==env.Minimap and button.scale==buttonScale and button.left==-22) + -- A foreign parent remains owned by its existing native/other-addon handler. + button.parent={effective=uiScale*2};hook('LeftButton');assert(button.parent==env.UIParent) + env.LFTMinimapButton=nil;install() -- non-Turtle clients + env.LFTMinimapButton={GetScript=function()end};install() +end +Check(.7,1.2,1);Check(1,1.5,.8);Check(.65,2,1) +print('LFT minimap drag: native reparenting preserves inherited size and screen position, custom/UI scales, unchanged reset, foreign parent and idempotent/non-Turtle setup passed') diff --git a/tests/test_locale_payload.lua b/tests/test_locale_payload.lua new file mode 100644 index 0000000..67e8e96 --- /dev/null +++ b/tests/test_locale_payload.lua @@ -0,0 +1,16 @@ +local root=(arg[1] or '.')..'/' +local env={};setmetatable(env,{__index=getfenv(0)}) +local chunk=assert(loadfile(root..'locales/locales_enUS.lua'));setfenv(chunk,env);chunk() +local locale=env.ShaguTweaks_locale.enUS +local sections=0 +for key in pairs(locale)do + assert(key=='class' or key=='customcast','unused locale section still loaded: '..key) + sections=sections+1 +end +assert(sections==2) +local count=0;for name,token in pairs(locale.class)do assert(type(name)=='string' and type(token)=='string');count=count+1 end +assert(count==9 and locale.class.Warlock=='WARLOCK' and locale.class.Druid=='DRUID') +assert(locale.customcast.AIMEDSHOT=='Aimed Shot' and locale.customcast.MULTISHOT=='Multi-Shot') +local f=assert(io.open(root..'mods/turtle-wow.lua'));local turtle=f:read('*a');f:close() +assert(not string.find(turtle,'L["debuffs"]',1,true),'obsolete Turtle writes remain') +print('Locale payload: only the nine class mappings and two live Hunter cast names remain; obsolete Turtle debuff writes removed') diff --git a/tests/test_minimap_clean_lifecycle.lua b/tests/test_minimap_clean_lifecycle.lua new file mode 100644 index 0000000..2f60122 --- /dev/null +++ b/tests/test_minimap_clean_lifecycle.lua @@ -0,0 +1,34 @@ +local root=(arg[1] or '.')..'/' +local env=dofile(root..'tests/fixtures/module_mock.lua')() +local function button(name,clickable) + return {alpha=1,GetName=function()return name end, + GetScript=function(self,k)if k=='OnClick' and self.clickable then return function()end end end, + SetAlpha=function(self,a)self.alpha=a end,clickable=clickable} +end +local first=button('FirstAddon',true) +local ignored=button('LFT_Minimap',true) +local children={first,ignored} +local scans,now,over=0,0,false +env.Minimap={GetNumChildren=function()return table.getn(children)end, + GetChildren=function()scans=scans+1;return unpack(children)end} +env.GetTime=function()return now end +env.ShaguTweaks.API={regionmouseover=true,IsMouseOver=function()return over end} +env.Load(root..'mods/minimap-clean.lua');env.module:enable() +local frame=env.module.frame +local function tick(time)now=time;env.Invoke(frame,'OnUpdate',nil,.1)end +tick(0);tick(4);assert(first.alpha==0 and ignored.alpha==1) +local before=scans +for i=1,50 do tick(4+i*.1)end +assert(scans==before) -- no subtree rebuild while the count is unchanged +local late=button('LateAddon',true);table.insert(children,late) +tick(10);assert(late.alpha==0 and first.alpha==0 and ignored.alpha==1) +-- Replaced children at the same count are invalidated when an addon loads. +local replacement=button('ReplacementAddon',true);children[3]=replacement +env.Invoke(frame,'OnEvent','ADDON_LOADED');tick(11);assert(replacement.alpha==0) +over=true;tick(12);assert(first.alpha==1 and replacement.alpha==1) +over=false;tick(13);tick(17);assert(replacement.alpha==0) +env.module:disable();assert(not frame.scripts.OnUpdate and not frame.events.ADDON_LOADED) +assert(first.alpha==1 and replacement.alpha==1) +env.module:enable();assert(frame.events.ADDON_LOADED and frame.scripts.OnUpdate) +assert(env.module.frame==frame and replacement.alpha==1) +print('Minimap Clean: late/replaced buttons inherit state, unchanged cache avoids traversal, LFT exclusion, hover delay and disable/re-enable passed') diff --git a/tests/test_personal_resource_display.lua b/tests/test_personal_resource_display.lua index 7ef8094..581f965 100644 --- a/tests/test_personal_resource_display.lua +++ b/tests/test_personal_resource_display.lua @@ -33,7 +33,8 @@ local function Check(big,timer,hideResources,druid) function methods:SetTexture(path) self.path=path end function methods:SetBackdrop(backdrop) self.backdrop=backdrop end function methods:GetTexture() return self.path end - function methods:GetStatusBarTexture() return {GetTexture=function() return 'NativeHealthTexture' end} end + function methods:GetStatusBarTexture() local path=self.barPath or 'NativeHealthTexture';return {GetTexture=function() return path end} end + function methods:SetStatusBarTexture(path) self.barPath=path;self.textureWrites=(self.textureWrites or 0)+1 end function methods:GetStatusBarColor() return unpack(self.barColor or {.53,.53,.93,1}) end function methods:SetText(text) self.text=text end function methods:SetTexCoord(a,b,c,d) self.uv={a,b,c,d} end @@ -107,7 +108,7 @@ local function Check(big,timer,hideResources,druid) function methods:GetMinMaxValues() return self.minimum or 0,self.maximum or 100 end function methods:SetValue(v) self.value=v end function methods:GetValue() return self.value or 75 end - function methods:SetStatusBarColor(r,g,b,a) self.barColor={r,g,b,a} end + function methods:SetStatusBarColor(r,g,b,a) self.barColor={r,g,b,a};self.colorWrites=(self.colorWrites or 0)+1 end function methods:StartSizing(direction) self.sizing=direction end ShaguTweaks.Abbreviate=function(v) return tostring(v) end ShaguTweaks_config={['Hide Player Frame Resources']=hideResources and 1 or 0, @@ -155,6 +156,17 @@ local function Check(big,timer,hideResources,druid) assert(display.health.width==180 and display.health.height==40 and display.health.left==420) assert(display.power.height==(big and 36 or 18)) assert(display.health.bar.value==75 and display.health.bar.maximum==100) + local copy=display.health.bar + local textureWrites,colorWrites=copy.textureWrites,copy.colorWrites + local r,g,b=PlayerFrameHealthBar:GetStatusBarColor() + for i=1,100 do PlayerFrameHealthBar:SetStatusBarColor(r,g,b,1) end + assert(copy.textureWrites==textureWrites and copy.colorWrites==colorWrites,'unchanged native colors must not rewrite copies') + PlayerFrameHealthBar:SetStatusBarColor(.1,.2,.3,1) + assert(copy.textureWrites==textureWrites and copy.colorWrites==colorWrites+1,'color-only update must not refresh prediction texture') + PlayerFrameHealthBar:SetStatusBarTexture('ChangedHealthTexture') + assert(copy.textureWrites==textureWrites+1 and copy.colorWrites==colorWrites+1) + PlayerFrameHealthBar:SetStatusBarTexture('ChangedHealthTexture') + assert(copy.textureWrites==textureWrites+1) PlayerFrameHealthBar:SetMinMaxValues(0,1000);PlayerFrameHealthBar:SetValue(400) assert(display.health.bar.value==400 and display.health.bar.maximum==1000) PlayerFrameHealthBar:SetStatusBarColor(.2,.3,.4,0) diff --git a/tests/test_reagent_macro_changes.lua b/tests/test_reagent_macro_changes.lua new file mode 100644 index 0000000..30d37b0 --- /dev/null +++ b/tests/test_reagent_macro_changes.lua @@ -0,0 +1,46 @@ +local root=(arg[1] or '.')..'/' +local env=dofile(root..'tests/fixtures/module_mock.lua')() +local selected,scans=11,0 +local actions={[1]='macro',[2]='spell'} +env.NUM_ACTIONBAR_BUTTONS=12 +for i=1,2 do + env['ActionButton'..i]=env.CreateFrame('Button','ActionButton'..i);env['ActionButton'..i].id=i + env['ActionButton'..i..'Count']=env.CreateFrame() +end +env.ActionButton_GetPagedID=function(button)return button.id end +env.HasAction=function(slot)scans=scans+1;return actions[slot]~=nil end +env.IsConsumableAction=function()return false end +env.C_EventUtils={IsEventValid=function(name)return name~='UPDATE_SHAPESHIFT_FORM'end} +local api=env.ShaguTweaks.API +api.eventutils=true +api.RegisterUnitEvent=function(frame,event,unit)assert(unit=='player');frame:RegisterEvent(event)end +api.GetActionInfo=function(slot)return actions[slot],slot end +api.GetMacroSpell=function()return 'Conditional','',selected end +api.GetSpellReagents=function(id)if id~=33 then return {{itemID=id}}end end +api.GetItemCount=function(id)return id*10 end +env.Load(root..'mods/actionbar-reagents.lua');env.module:enable() +local frame=env.module.counter +local function flush(event,unit) + env.Invoke(frame,'OnEvent',event,unit) + if frame.visible then env.Invoke(frame,'OnUpdate')end +end +flush('PLAYER_ENTERING_WORLD');assert(env.ActionButton1Count.text==110 and env.ActionButton2Count.text==20) +assert(frame.events.MODIFIER_STATE_CHANGED and not frame.events.UPDATE_SHAPESHIFT_FORM) +assert(frame.events.UPDATE_SHAPESHIFT_FORMS and frame.events.UNIT_AURA) +local initial=scans +selected=22;flush('MODIFIER_STATE_CHANGED');assert(env.ActionButton1Count.text==220 and scans==initial+1) +selected=33;flush('PLAYER_TARGET_CHANGED');assert(env.ActionButton1Count.text==nil) +selected=44;flush('UNIT_AURA','target');assert(env.ActionButton1Count.text==nil and not frame.pending) +flush('UNIT_AURA','player');assert(env.ActionButton1Count.text==440) +selected=nil;flush('UPDATE_SHAPESHIFT_FORMS');assert(env.ActionButton1Count.text==nil) +selected=55;flush('SPELL_UPDATE_COOLDOWN');assert(env.ActionButton1Count.text==550) +initial=scans;flush('BAG_UPDATE');assert(scans==initial and env.ActionButton1Count.text==550) +actions[1]=nil;flush('ACTIONBAR_SLOT_CHANGED') +assert(not frame.events.MODIFIER_STATE_CHANGED and not frame.events.UNIT_AURA) +assert(env.ActionButton1Count.text==nil) +actions[1]='macro';selected=66;flush('UPDATE_MACROS') +assert(frame.events.MODIFIER_STATE_CHANGED and env.ActionButton1Count.text==660) +env.ActionButton1Count.text='native cleared';env.this=env.ActionButton1 +env.hooks.ActionButton_UpdateCount();assert(env.ActionButton1Count.text==660) +local count=table.getn(env.frames);env.module:enable();assert(table.getn(env.frames)==count) +print('Reagents: conditional spell/reagent-free/unresolved transitions, macro-only scans, native event guards, unit filtering, count ownership and listener teardown/rearming passed') diff --git a/tests/test_removed_aoe_loot.lua b/tests/test_removed_aoe_loot.lua new file mode 100644 index 0000000..c181869 --- /dev/null +++ b/tests/test_removed_aoe_loot.lua @@ -0,0 +1,17 @@ +local root=(arg[1] or '.')..'/' +local function read(path)local f=assert(io.open(root..path));local s=f:read('*a');f:close();return s end +local toc=read('ShaguTweaks.toc');assert(not string.find(toc,'mods\\aoe-loot.lua',1,true)) +local file=io.open(root..'mods/aoe-loot.lua');assert(not file) +local api=read('api.lua');assert(loadstring(api)) +for _,name in ipairs({'API.aoeloot','API.containeropenable','API.IsContainerItemOpenable', + 'API.GetNearbyLootableUnits','API.IsLootScanInProgress','API.LootAllCorpses'}) do + assert(not string.find(api,name,1,true),name) +end +local main=read('main.lua');assert(loadstring(main)) +local retired='ShaguTweaks_config[ShaguTweaks.T["AoE Loot"]] = nil' +assert(string.find(main,retired,1,true)) +local env={ShaguTweaks_config={['AoE Loot']=1,['Loot Quality']=1},ShaguTweaks={T=setmetatable({},{__index=function(_,k)return k end})}} +local chunk=assert(loadstring(retired));setfenv(chunk,env);chunk() +assert(env.ShaguTweaks_config['AoE Loot']==nil and env.ShaguTweaks_config['Loot Quality']==1) +assert(string.find(read('Docs/EXTRAS_MERGE.md'),'https://github.com/rotaruandrei93/AutoAreaLoot',1,true)) +print('Removed AoE Loot: source/manifest/API cleanup, retired saved setting and migration replacement link passed') diff --git a/tests/test_sell_junk_completion.lua b/tests/test_sell_junk_completion.lua new file mode 100644 index 0000000..21f29a7 --- /dev/null +++ b/tests/test_sell_junk_completion.lua @@ -0,0 +1,54 @@ +local root=(arg[1] or '.')..'/' +assert(loadfile(root..'mods/sell-junk.lua')) +local env=assert(loadfile(root..'tests/fixtures/module_mock.lua'))()() +local now,count,money,sales,queries,enabled,messages=0,3,100,0,0,true,{} +env.GetTime=function()return now end +env.GetMoney=function()return money end +env.floor,env.mod=math.floor,math.mod +env.ShaguTweaks.API.GetNumJunkItems=function()queries=queries+1;return count end +env.ShaguTweaks.API.SellAllJunkItems=function()sales=sales+1 end +env.MerchantFrame={selectedTab=1} +env.MerchantFrame_Update=function()end +env.MerchantRepairText={SetText=function()end} +env.MerchantRepairItemButton={IsShown=function()return false end} +env.DEFAULT_CHAT_FRAME.AddMessage=function(_,text)table.insert(messages,text)end +env.Load(root..'mods/sell-junk.lua');env.module:enable() +local driver=env.frames[1];local button=driver.button +-- Model native OnHide delivery without changing the shared fixture. +function driver:Hide() + local visible=self.visible;self.visible=false + if visible and self.scripts.OnHide then env.Invoke(self,'OnHide') end +end +function button:Enable()enabled=true end +function button:Disable()enabled=false end +local function tick(dt) + now=now+dt + if driver:IsVisible() then env.Invoke(driver,'OnUpdate',nil,dt) end +end +local function start() + env.Invoke(driver,'OnEvent','MERCHANT_SHOW') + env.Invoke(button,'OnClick') + assert(driver:IsVisible() and not enabled) +end +start();local before=queries +for i=1,9 do tick(.01) end +assert(queries==before,'must not scan every frame') +tick(.02);assert(queries==before+1) +tick(4);assert(driver:IsVisible()) +tick(1);assert(not driver:IsVisible() and enabled and sales==1) +assert(not driver.started and not driver.progressUntil and table.getn(messages)==0) +-- Progress extends the deadline, including queues taking over five seconds. +start();tick(4);count=2;money=110;tick(.2) +tick(4);assert(driver:IsVisible()) +count=0;money=140;tick(.2) +assert(not driver:IsVisible() and not enabled and sales==2) +assert(table.getn(messages)==1) +-- Merchant close cancels waiting; later attempts start a fresh deadline. +count=2;start();tick(1) +env.Invoke(driver,'OnEvent','MERCHANT_CLOSED') +assert(not driver:IsVisible() and not driver.started) +now=now+20;start();tick(.2);assert(driver:IsVisible()) +tick(5);assert(not driver:IsVisible() and enabled and sales==4) +count=0;env.Invoke(button,'OnClick') +assert(not driver:IsVisible() and sales==4) +print('Sell Junk: throttled completion, stalled-sale timeout, progress renewal, profit once, merchant close and clean retry passed') diff --git a/tests/test_swing_native.lua b/tests/test_swing_native.lua index 5fd57b4..c3e3b6f 100644 --- a/tests/test_swing_native.lua +++ b/tests/test_swing_native.lua @@ -18,10 +18,12 @@ local function Check(native,methodAvailable,class) function methods:Show() self.shown=true end function methods:Hide() self.shown=false end function methods:IsShown() return self.shown end - function methods:CreateTexture() return object('Texture',nil,self) end + function methods:CreateTexture(_,layer)local t=object('Texture',nil,self);t.layer=layer;return t end function methods:CreateFontString() return object('FontString',nil,self) end function methods:SetText(value) self.text=value end function methods:SetStatusBarColor(...) self.color=arg end + function methods:SetVertexColor(...) self.tint=arg end + function methods:SetBackdropBorderColor(...) self.borderColor=arg end function methods:SetPoint(...) self.point=arg end function methods:GetStatusBarTexture() self.fill=self.fill or object('Texture');return self.fill end function methods:SetMinMaxValues(low,high) self.low,self.high=low,high;self.nativeTimer=nil end @@ -79,6 +81,24 @@ local function Check(native,methodAvailable,class) dofile(root..'mods/swing-timer.lua') modules['Swing Timer']:enable();modules['Wand Timer']:enable() local mh,oh,ra=ShaguTweaksSwingTimer_mainhand,ShaguTweaksSwingTimer_offhand,ShaguTweaksSwingTimer_ranged + -- All timer variants, including previews created before the palette, use + -- the same theme colors without changing the progress fill or weapon icon. + for _,palette in ipairs({{r=.3,g=.3,b=.3,a=.9},{r=1,g=.78,b=.4,a=1},{r=1,g=1,b=1,a=1}})do + ShaguTweaks.UITint=palette + ShaguTweaks.UIBorder={r=palette.r*.8,g=palette.g*.8,b=palette.b*.8,a=palette.a} + for _,bar in ipairs({mh,oh,ra})do + local fill=bar.color + this=bar;bar.scripts.OnShow();this=nil + assert(bar.border.layer=='OVERLAY' and bar.border.tint[1]==palette.r and bar.border.tint[4]==palette.a) + assert(bar.icon.borderColor[1]==palette.r*.8 and bar.icon.borderColor[4]==palette.a) + assert(bar.color==fill and not bar.icon.texture.tint) + end + end + ShaguTweaks.UITint,ShaguTweaks.UIBorder=nil,nil + for _,bar in ipairs({mh,oh,ra})do + this=bar;bar.scripts.OnShow();this=nil + assert(bar.border.tint[1]==1 and bar.icon.borderColor[1]==1) + end local listener,driver for _,frame in ipairs(frames) do if frame.events.PLAYER_SWING then listener=frame end diff --git a/tests/test_turtle_clock_map.lua b/tests/test_turtle_clock_map.lua new file mode 100644 index 0000000..eb0e5ef --- /dev/null +++ b/tests/test_turtle_clock_map.lua @@ -0,0 +1,29 @@ +local root=(arg[1] or '.')..'/' +local env=dofile(root..'tests/fixtures/module_mock.lua')() +env.TargetHPText,env.TargetHPPercText,env.Cartographer=true,true,true +env.ShaguTweaks.L={debuffs={}} +env.GetGameTime=function()return 8,30 end +env.GetMapContinents=function()return 'Kalimdor','Eastern Kingdoms'end +local continent,zone,mutations=2,7,0 +env.GetCurrentMapContinent=function()return continent end +env.GetCurrentMapZone=function()return zone end +env.SetMapToCurrentZone=function()mutations=mutations+1;continent,zone=1,3 end +env.SetMapZoom=function(c,z)mutations=mutations+1;continent,zone=c,z or 0 end +local maps={ + [11]={name='Durotar',parentMapID=-13},[-13]={name='Kalimdor',parentMapID=-694}, + [12]={name='Elwynn Forest',parentMapID=-14},[-14]={name='Eastern Kingdoms',parentMapID=-694}, + [-694]={name='World',parentMapID=0}, +} +local current=11 +env.C_Map={GetBestMapForUnit=function(unit)assert(unit=='player');return current end, + GetMapInfo=function(id)return maps[id]end} +env.Load(root..'mods/turtle-wow.lua');env.module:enable() +local clock=env.ShaguTweaks.GetTurtleClockTimes +local server,localTime=clock();assert(server=='20:30' and localTime=='08:30') +assert(continent==2 and zone==7 and mutations==0) +current=12;assert(clock()=='08:30' and mutations==0) +-- Missing hierarchy data and old API versions restore the previous view. +current=99;assert(clock()=='20:30' and continent==2 and zone==7 and mutations==2) +env.C_Map=nil;assert(clock()=='20:30' and continent==2 and zone==7 and mutations==4) +continent,zone=0,0;clock();assert(continent==0 and zone==0) +print('Turtle clock: native map hierarchy without view mutation, both continents, missing data, older API fallback and world-map restoration passed') diff --git a/tests/test_unitframe_style_routing.lua b/tests/test_unitframe_style_routing.lua new file mode 100644 index 0000000..5463cf7 --- /dev/null +++ b/tests/test_unitframe_style_routing.lua @@ -0,0 +1,106 @@ +local root=(arg[1] or '.')..'/' +local env=setmetatable({},{__index=getfenv(0)});env._G=env +local modules,hooks,frames,power,queries={},{},{},{player=0,target=0,party1=0},{} +local function texture(path) + local t={path=path,shown=true,coords={0,0,0,1,1,0,1,1},writes=0,crops=0} + function t:GetTexture()return self.path end + function t:SetTexture(path)self.path=path;self.writes=self.writes+1 end + function t:GetTexCoord()return unpack(self.coords)end + function t:SetTexCoord(l,r,top,bottom)self.coords={l,top,l,bottom,r,top,r,bottom};self.crops=self.crops+1 end + function t:SetAllPoints()end + function t:IsShown()return self.shown end + function t:Show()self.shown=true end + function t:Hide()self.shown=false end + return t +end +local function bar(unit) + local f={unit=unit,fill=texture('original'),color={.2,.3,.4,1},writes=0} + function f:GetStatusBarTexture()return self.fill end + function f:SetStatusBarTexture(path)self.fill:SetTexture(path)end + function f:GetStatusBarColor()return unpack(self.color)end + function f:SetStatusBarColor(r,g,b,a)self.color={r,g,b,a};self.writes=self.writes+1 end + function f:CreateTexture()return texture()end + return f +end +env.PlayerFrameManaBar=bar('player');env.PlayerFrameAlternatePowerBar=bar('player') +env.TargetFrameManaBar=bar('target');env.PartyMemberFrame1ManaBar=bar('party1') +env.MAX_PARTY_MEMBERS=1 +env.UnitExists=function(unit)queries[unit]=(queries[unit] or 0)+1;return true end +env.UnitPowerType=function(unit)return power[unit] or 0 end +env.UnitClass=function(unit)return 'Druid','DRUID' end +env.CreateFrame=function() + local f={scripts={}} + function f:SetScript(k,v)self.scripts[k]=v end + function f:RegisterEvent()end + function f:Hide()end + table.insert(frames,f);return f +end +env.ShaguTweaks={T=setmetatable({},{__index=function(_,k)return k end}),API={},GetGlobalEnv=function()return env end} +function env.ShaguTweaks:register(m)modules[m.title]=m;return m end +env.ShaguTweaks.hooksecurefunc=function(obj,name,callback) + if type(obj)=='string' then hooks[obj]=name;return end + local old=obj[name];obj[name]=function(self,r,g,b,a)old(self,r,g,b,a);callback(self,r,g,b,a)end +end +local function load(path)local f=assert(loadfile(root..path));setfenv(f,env);f()end +load('mods/unitframes-classcolor.lua');modules['Modern Class Colors']:enable() +local player,alt,target,party=env.PlayerFrameManaBar,env.PlayerFrameAlternatePowerBar,env.TargetFrameManaBar,env.PartyMemberFrame1ManaBar +local function reset() + queries={} + for _,f in ipairs({player,alt,target,party})do f.writes=0;f.fill.writes=0 end +end +reset();hooks.UnitFrame_UpdateManaType({unit='target',manabar=target}) +assert(queries.target==1 and not queries.player and not queries.party1) +assert(target.fill.writes==0 and target.writes==0,'unchanged styles must not repeat texture/tint writes') +-- Native overwrites are corrected even though the cached style key is stable. +target.fill.path='native';target.color={0,0,1,1};hooks.UnitFrame_UpdateManaType({manabar=target}) +assert(target.fill.writes==1 and target.writes==1 and target.color[1]==1) +reset();target:SetStatusBarColor(.5,.5,.5,1) +assert(target.writes==2 and not queries.player and not queries.party1) +reset();target.color={.999997,.999997,.999997,.999997};hooks.UnitFrame_UpdateManaType(target) +assert(target.writes==0,'packed native white should not trigger repeated corrective writes') +-- A player form change updates the primary and fixed-mana secondary bar only. +reset();power.player=3;hooks.UnitFrame_UpdateManaType({unit='player'}) +assert(queries.player==2 and not queries.target and not queries.party1) +assert(string.find(player.fill.path,'ModernPowerEnergyClass',1,true)) +assert(string.find(alt.fill.path,'ModernPowerManaClass',1,true)) +reset();hooks.UnitFrame_UpdateManaType({unit='raid40'});assert(not next(queries)) +env.this={manabar=party};hooks.UnitFrame_UpdateManaType('party1');env.this=nil +assert(queries.party1==1 and not queries.player) +power.target=7;reset();hooks.UnitFrame_UpdateManaType(target);assert(target.fill.path=='original') +hooks.UnitFrame_UpdateManaType(target);assert(target.fill.writes==1) +-- Keep the explicit full startup refresh after other modules initialize. +reset();env.this=frames[1];frames[1].scripts.OnUpdate();env.this=nil +assert(queries.player==2 and queries.target==1 and queries.party1==1) + +local classQueries,players,selfTarget={}, {},false +env.GetAddOnInfo=function()return 'ShaguTweaks','ShaguTweaks' end +env.UnitClass=function(unit)classQueries[unit]=(classQueries[unit] or 0)+1;return 'Druid','DRUID' end +env.UnitIsPlayer=function(unit)return players[unit]~=false end +env.UnitIsUnit=function(a,b)return a==b or selfTarget and ((a=='player' and b=='target')or(a=='target' and b=='player'))end +env.TargetofTarget_OnUpdate=function()end +env.ShaguTweaks.API.UnitGUID=function(unit)return unit..'guid' end +env.PlayerFrame={unit='player',portrait=texture('portrait')} +env.TargetFrame={unit='target',portrait=texture('portrait')} +env.TargetofTargetFrame={unit='targettarget',portrait=texture('portrait')} +for i=1,4 do env['PartyMemberFrame'..i]={unit='party'..i,portrait=texture('portrait')}end +load('mods/unitframes-classportrait.lua');modules['Unit Frame Class Portraits']:enable() +local events=frames[table.getn(frames)] +local function emit(name,unit) + classQueries={};env.event,env.arg1=name,unit;events.scripts.OnEvent() +end +emit('PLAYER_ENTERING_WORLD') +assert(classQueries.player==1 and classQueries.target==1 and classQueries.party4==1) +emit('UNIT_PORTRAIT_UPDATE','party2');assert(classQueries.party2==1 and not classQueries.player and not classQueries.target) +local portrait=env.PartyMemberFrame2.portrait;local writes,crops=portrait.writes,portrait.crops +emit('UNIT_PORTRAIT_UPDATE','party2');assert(portrait.writes==writes and portrait.crops==crops) +emit('UNIT_PORTRAIT_UPDATE','raid40');assert(not next(classQueries)) +selfTarget=true;emit('UNIT_PORTRAIT_UPDATE','player');assert(classQueries.player==1 and classQueries.target==1) +local tf=env.TargetFrame;tf.portrait.path='native';tf.portrait.coords={0,0,0,1,1,0,1,1} +hooks.UnitFrame_Update(tf);assert(string.find(tf.portrait.path,'UI-Classes-Circles',1,true)) +tf.portrait.path='native';env.this=tf;hooks.UnitFrame_Update('target');env.this=nil +assert(string.find(tf.portrait.path,'UI-Classes-Circles',1,true)) +players.target=false;tf.portrait.path='npc';hooks.UnitFrame_Update(tf) +assert(tf.portrait.path=='npc' and tf.portrait.coords[1]==0 and tf.portrait.coords[5]==1) +env.TargetofTarget_OnUpdate(.3) +assert(string.find(env.TargetofTargetFrame.portrait.path,'UI-Classes-Circles',1,true)) +print('Unit-frame styling: affected power/portrait routing, startup refreshes, fixed druid mana, stable keys/live overwrite correction, packed-white tolerance, aliases and native hook contexts passed')