-- NanamiPlates Spell Database -- Debuff duration tracking with rank support (ShaguPlates-style) -- Performance: Upvalue frequently used globals local pairs = pairs local type = type local tonumber = tonumber local string_gfind = string.gfind local string_sub = string.sub local GetTime = GetTime NanamiPlates_SpellDB = {} NanamiPlates_SpellDB.scanner = nil -- Chinese → English spell name mapping (for localized tooltip scanning) NanamiPlates_SpellDB.localeMap = { -- WARRIOR ["撕裂"] = "Rend", ["雷霆一击"] = "Thunder Clap", ["破甲攻击"] = "Sunder Armor", ["缴械"] = "Disarm", ["断筋"] = "Hamstring", ["挫志怒吼"] = "Demoralizing Shout", ["破胆怒吼"] = "Intimidating Shout", ["震荡猛击"] = "Concussion Blow", ["嘲讽打击"] = "Mocking Blow", ["刺耳怒吼"] = "Piercing Howl", ["致死打击"] = "Mortal Strike", ["重伤"] = "Deep Wound", ["冲锋昏迷"] = "Charge Stun", ["冲锋"] = "Charge", ["拦截昏迷"] = "Intercept Stun", ["拦截"] = "Intercept", ["挑战怒吼"] = "Challenging Shout", ["嘲讽"] = "Taunt", -- ROGUE ["偷袭"] = "Cheap Shot", ["肾击"] = "Kidney Shot", ["闷棍"] = "Sap", ["致盲"] = "Blind", ["凿击"] = "Gouge", ["割裂"] = "Rupture", ["绞喉"] = "Garrote", ["破甲"] = "Expose Armor", ["致残毒药"] = "Crippling Poison", ["致残毒药 II"] = "Crippling Poison II", ["致命毒药"] = "Deadly Poison", ["致命毒药 II"] = "Deadly Poison II", ["致命毒药 III"] = "Deadly Poison III", ["致命毒药 IV"] = "Deadly Poison IV", ["致命毒药 V"] = "Deadly Poison V", ["麻痹毒药"] = "Mind-numbing Poison", ["麻痹毒药 II"] = "Mind-numbing Poison II", ["麻痹毒药 III"] = "Mind-numbing Poison III", ["伤残毒药"] = "Wound Poison", ["伤残毒药 II"] = "Wound Poison II", ["伤残毒药 III"] = "Wound Poison III", ["伤残毒药 IV"] = "Wound Poison IV", ["速效毒药"] = "Instant Poison", ["脚踢 - 沉默"] = "Kick - Silenced", -- Turtle WoW rogue variants ["腐蚀毒药"] = "Crippling Poison", ["腐蚀毒药 II"] = "Crippling Poison II", -- MAGE ["冰霜新星"] = "Frost Nova", ["变形术"] = "Polymorph", ["变形术:猪"] = "Polymorph: Pig", ["变形术:龟"] = "Polymorph: Turtle", ["变形术:牛"] = "Polymorph: Cow", ["寒冰箭"] = "Frostbolt", ["冰锥术"] = "Cone of Cold", ["冰霜撕咬"] = "Frostbite", ["法术反制 - 沉默"] = "Counterspell - Silenced", ["冬天的寒意"] = "Winter's Chill", ["火球术"] = "Fireball", ["炎爆术"] = "Pyroblast", ["点燃"] = "Ignite", -- WARLOCK ["腐蚀术"] = "Corruption", ["献祭"] = "Immolate", ["恐惧术"] = "Fear", ["恐惧嚎叫"] = "Howl of Terror", ["死亡缠绕"] = "Death Coil", ["痛苦诅咒"] = "Curse of Agony", ["虚弱诅咒"] = "Curse of Weakness", ["鲁莽诅咒"] = "Curse of Recklessness", ["语言诅咒"] = "Curse of Tongues", ["元素诅咒"] = "Curse of the Elements", ["暗影诅咒"] = "Curse of Shadow", ["疲劳诅咒"] = "Curse of Exhaustion", ["末日诅咒"] = "Curse of Doom", ["生命虹吸"] = "Siphon Life", ["生命吸取"] = "Drain Life", ["法力吸取"] = "Drain Mana", ["灵魂吸取"] = "Drain Soul", ["放逐术"] = "Banish", ["奴役恶魔"] = "Enslave Demon", ["魅惑"] = "Seduction", ["暗影易伤"] = "Shadow Vulnerability", ["法术封锁"] = "Spell Lock", -- PRIEST ["暗言术:痛"] = "Shadow Word: Pain", ["心灵尖啸"] = "Psychic Scream", ["精神鞭笞"] = "Mind Flay", ["精神控制"] = "Mind Control", ["沉默"] = "Silence", ["虚弱灵魂"] = "Weakened Soul", ["噬灵瘟疫"] = "Devouring Plague", ["吸血鬼的拥抱"] = "Vampiric Embrace", ["昏厥"] = "Blackout", -- HUNTER ["毒蛇钉刺"] = "Serpent Sting", ["蝰蛇钉刺"] = "Viper Sting", ["毒蝎钉刺"] = "Scorpid Sting", ["震荡射击"] = "Concussive Shot", ["驱散射击"] = "Scatter Shot", ["摔绊"] = "Wing Clip", ["猎人印记"] = "Hunter's Mark", ["反击"] = "Counterattack", ["翼龙钉刺"] = "Wyvern Sting", ["冰冻陷阱效果"] = "Freezing Trap Effect", ["爆炸陷阱效果"] = "Explosive Trap Effect", ["献祭陷阱效果"] = "Immolation Trap Effect", ["冰霜陷阱光环"] = "Frost Trap Aura", ["胁迫"] = "Intimidation", ["诱捕"] = "Entrapment", ["恐吓野兽"] = "Scare Beast", -- DRUID ["月火术"] = "Moonfire", ["纠缠根须"] = "Entangling Roots", ["重击"] = "Bash", ["精灵之火"] = "Faerie Fire", ["精灵之火(野性)"] = "Faerie Fire (Feral)", ["撕碎"] = "Rake", ["撕扯"] = "Rip", ["突袭流血"] = "Pounce Bleed", ["突袭"] = "Pounce", ["虫群"] = "Insect Swarm", ["休眠"] = "Hibernate", ["野性冲锋效果"] = "Feral Charge Effect", ["低吼"] = "Growl", ["挑战咆哮"] = "Challenging Roar", ["挫志咆哮"] = "Demoralizing Roar", -- PALADIN ["制裁之锤"] = "Hammer of Justice", ["忏悔"] = "Repentance", ["超度亡灵"] = "Turn Undead", ["十字军审判"] = "Judgement of the Crusader", ["光明审判"] = "Judgement of Light", ["智慧审判"] = "Judgement of Wisdom", ["公正审判"] = "Judgement of Justice", ["审判"] = "Judgement", -- SHAMAN ["冰霜震击"] = "Frost Shock", ["地震术"] = "Earth Shock", ["烈焰震击"] = "Flame Shock", ["地缚"] = "Earthbind", ["风暴打击"] = "Stormstrike", ["报应"] = "Vindication", ["十字军打击"] = "Crusader Strike", -- SHAMAN (extra) ["石爪昏迷"] = "Stoneclaw Stun", -- OTHER ["战争践踏"] = "War Stomp", ["眩晕"] = "Dazed", ["锤击昏迷效果"] = "Mace Stun Effect", ["盾击 - 沉默"] = "Shield Bash - Silenced", ["复仇昏迷"] = "Revenge Stun", ["撕裂(宠物)"] = "Lacerate", ["冲击"] = "Impact", ["余波"] = "Aftermath", ["火焰风暴"] = "Pyroclasm", ["还击"] = "Riposte", -- ENGINEERING / ITEMS ["铁皮手雷"] = "Iron Grenade", ["瑟银手雷"] = "Thorium Grenade", ["闪光弹"] = "Flash Bomb", ["简易炸药"] = "Ez-Thro Dynamite", ["简易炸药 II"] = "Ez-Thro Dynamite II", ["致密炸药"] = "Dense Dynamite", ["固体炸药"] = "Solid Dynamite", ["高爆炸弹"] = "Hi-Explosive Bomb", ["黑铁炸弹"] = "Dark Iron Bomb", ["地精工兵炸药"] = "Goblin Sapper Charge", ["地精迫击炮"] = "Goblin Mortar", ["大炸弹"] = "The Big One", ["电网发射器"] = "Net-o-Matic", ["鲁莽冲锋"] = "Reckless Charge", ["侏儒精神控制帽"] = "Gnomish Mind Control Cap", ["侏儒电网发射器"] = "Gnomish Net-o-Matic", ["侏儒死亡射线"] = "Gnomish Death Ray", ["地精火箭头盔"] = "Goblin Rocket Helmet", ["冰霜手雷"] = "Frost Grenade", ["粗制铜质炸弹"] = "Rough Copper Bomb", ["大型铜质炸弹"] = "Large Copper Bomb", ["小型青铜炸弹"] = "Small Bronze Bomb", ["大型青铜炸弹"] = "Big Bronze Bomb", ["大型铁质炸弹"] = "Big Iron Bomb", ["地精地雷"] = "Goblin Land Mine", ["混乱射线"] = "Discombobulator Ray", ["大绳索网"] = "Large Rope Net", ["陷阱"] = "Trap", ["自由行动药水"] = "Free Action Potion", ["活力行动药水"] = "Living Action Potion", ["潮汐护符"] = "Tidal Charm", ["昏迷"] = "Stun", } -- ONLY unique texture → spell mappings here. -- Ambiguous textures (shared by multiple spells) are omitted; tooltip + locale handles them. NanamiPlates_SpellDB.textureToSpell = { -- Warrior (unique only) ["Interface\\Icons\\Ability_Rend"] = "Rend", ["Interface\\Icons\\Ability_Warrior_Decimate"] = "Improved Hamstring", ["Interface\\Icons\\Ability_Warrior_Sunder"] = "Sunder Armor", ["Interface\\Icons\\Ability_BullRush"] = "Challenging Shout", -- Warlock (unique only) ["Interface\\Icons\\Spell_Shadow_LifeDrain"] = "Tainted Blood Effect", ["Interface\\Icons\\Spell_Shadow_SoulLeech"] = "Dark Harvest", ["Interface\\Icons\\Spell_Shadow_AbominationExplosion"] = "Corruption", ["Interface\\Icons\\Spell_Shadow_CurseOfSargeras"] = "Curse of Agony", ["Interface\\Icons\\Spell_Shadow_CurseOfMannoroth"] = "Curse of Weakness", ["Interface\\Icons\\Spell_Shadow_UnholyStrength"] = "Curse of Recklessness", ["Interface\\Icons\\Spell_Shadow_CurseOfTounges"] = "Curse of Tongues", ["Interface\\Icons\\Spell_Shadow_ChillTouch"] = "Curse of the Elements", ["Interface\\Icons\\Spell_Shadow_CurseOfAchimonde"] = "Curse of Shadow", ["Interface\\Icons\\Spell_Shadow_GrimWard"] = "Curse of Exhaustion", ["Interface\\Icons\\Spell_Shadow_AuraOfDarkness"] = "Curse of Doom", ["Interface\\Icons\\Spell_Shadow_MindRot"] = "Curse of Idiocy", ["Interface\\Icons\\Spell_Fire_Immolation"] = "Immolate", ["Interface\\Icons\\Spell_Shadow_Requiem"] = "Siphon Life", ["Interface\\Icons\\Spell_Shadow_Cripple"] = "Banish", -- Druid (unique only) ["Interface\\Icons\\Spell_Nature_FaerieFire"] = "Faerie Fire", ["Interface\\Icons\\Ability_Druid_Disembowel"] = "Rake", ["Interface\\Icons\\Ability_Druid_Rip"] = "Rip", ["Interface\\Icons\\Ability_Druid_SupriseAttack"] = "Pounce Bleed", ["Interface\\Icons\\Ability_Druid_ChallangingRoar"] = "Challenging Roar", ["Interface\\Icons\\Spell_Nature_StarFall"] = "Moonfire", ["Interface\\Icons\\Spell_Nature_StrangleVines"] = "Entangling Roots", ["Interface\\Icons\\Spell_Nature_InsectSwarm"] = "Insect Swarm", ["Interface\\Icons\\Ability_Druid_DemoralizingRoar"] = "Demoralizing Roar", ["Interface\\Icons\\Ability_Druid_Mangle"] = "Mangle", -- Paladin (unique only) ["Interface\\Icons\\Spell_Holy_Vindication"] = "Vindication", -- Hunter (unique only) ["Interface\\Icons\\spell_lacerate_1C"] = "Lacerate", ["Interface\\Icons\\Spell_Frost_ChainsOfIce"] = "Freezing Trap Effect", ["Interface\\Icons\\Spell_Fire_SelfDestruct"] = "Explosive Trap Effect", ["Interface\\Icons\\Spell_Frost_FreezingBreath"] = "Frost Trap Aura", -- Rogue (unique only) ["Interface\\Icons\\Ability_CheapShot"] = "Cheap Shot", ["Interface\\Icons\\Ability_Rogue_KidneyShot"] = "Kidney Shot", ["Interface\\Icons\\Ability_Sap"] = "Sap", ["Interface\\Icons\\Ability_Rogue_Rupture"] = "Rupture", ["Interface\\Icons\\Ability_Rogue_Garrote"] = "Garrote", -- Other (unique only) ["Interface\\Icons\\Spell_Nature_Cyclone"] = "Thunderfury's Blessing", } -- Preferred names for textures when encountered as DEBUFFS (context-aware priority) NanamiPlates_SpellDB.debuffPriority = { ["Interface\\Icons\\Spell_Holy_HealingAura"] = "Judgement of Light", ["Interface\\Icons\\Spell_Holy_RighteousnessAura"] = "Judgement of Wisdom", ["Interface\\Icons\\Spell_Holy_HolySmite"] = "Judgement of the Crusader", ["Interface\\Icons\\Spell_Holy_SealOfWrath"] = "Judgement of Justice", } -- ============================================ -- DEBUFF DURATIONS BY SPELL NAME AND RANK -- Format: ["Spell Name"] = { [rank] = duration, [0] = default/max } -- ============================================ NanamiPlates_SpellDB.DEBUFFS = { -- WARRIOR ["Rend"] = {[1]=9, [2]=12, [3]=15, [4]=18, [5]=21, [6]=21, [7]=21, [0]=21}, ["Thunder Clap"] = {[1]=10, [2]=14, [3]=18, [4]=22, [5]=26, [6]=30, [0]=30}, ["Sunder Armor"] = {[0]=30}, ["Disarm"] = {[0]=10}, ["Hamstring"] = {[0]=15}, ["Improved Hamstring"] = {[0]=5}, ["Demoralizing Shout"] = {[0]=30}, ["Intimidating Shout"] = {[0]=8}, ["Concussion Blow"] = {[0]=5}, ["Mocking Blow"] = {[0]=6}, ["Piercing Howl"] = {[0]=6}, ["Mortal Strike"] = {[0]=10}, ["Deep Wound"] = {[0]=12}, ["Charge"] = {[0]=1}, ["Charge Stun"] = {[0]=1}, ["Intercept"] = {[0]=3}, ["Intercept Stun"] = {[0]=3}, ["Challenging Shout"] = {[0]=6}, ["Demoralizing Roar"] = {[0]=30}, ["Dazed"] = {[0]=4}, ["Taunt"] = {[0]=3}, -- ROGUE ["Cheap Shot"] = {[0]=4}, ["Kidney Shot"] = {[1]=0, [2]=1, [0]=1}, -- Rank1: 0+1s/CP, Rank2: 1+1s/CP ["Sap"] = {[1]=25, [2]=35, [3]=45, [0]=45}, ["Blind"] = {[0]=10}, ["Gouge"] = {[1]=4, [2]=4, [3]=4, [4]=4, [5]=4, [0]=4}, -- base 4s, +0.5s/talent ["Rupture"] = {[0]=8}, -- base 8s + 2s/CP (via DYN_DEBUFFS) ["Garrote"] = {[1]=18, [2]=18, [3]=18, [4]=18, [5]=18, [0]=18}, ["Expose Armor"] = {[0]=30}, ["Crippling Poison"] = {[0]=12}, ["Crippling Poison II"] = {[0]=12}, ["Deadly Poison"] = {[0]=12}, ["Deadly Poison II"] = {[0]=12}, ["Deadly Poison III"] = {[0]=12}, ["Deadly Poison IV"] = {[0]=12}, ["Deadly Poison V"] = {[0]=12}, ["Mind-numbing Poison"] = {[0]=14}, ["Mind-numbing Poison II"] = {[0]=14}, ["Mind-numbing Poison III"] = {[0]=14}, ["Wound Poison"] = {[0]=15}, ["Wound Poison II"] = {[0]=15}, ["Wound Poison III"] = {[0]=15}, ["Wound Poison IV"] = {[0]=15}, ["Instant Poison"] = {[0]=0}, ["Instant Poison II"] = {[0]=0}, ["Instant Poison III"] = {[0]=0}, ["Instant Poison IV"] = {[0]=0}, ["Instant Poison V"] = {[0]=0}, ["Instant Poison VI"] = {[0]=0}, -- MAGE ["Frost Nova"] = {[1]=8, [2]=8, [3]=8, [4]=8, [0]=8}, ["Polymorph"] = {[1]=20, [2]=30, [3]=40, [4]=50, [0]=50}, ["Polymorph: Pig"] = {[0]=50}, ["Polymorph: Turtle"] = {[0]=50}, ["Polymorph: Cow"] = {[0]=50}, ["Frostbolt"] = {[1]=5, [2]=6, [3]=7, [4]=8, [5]=9, [6]=9, [7]=9, [8]=9, [9]=9, [10]=9, [11]=9, [0]=9}, ["Cone of Cold"] = {[0]=8}, ["Frostbite"] = {[0]=5}, ["Counterspell - Silenced"] = {[0]=4}, ["Winter's Chill"] = {[0]=15}, ["Fireball"] = {[0]=8}, -- DoT component ["Pyroblast"] = {[0]=12}, -- DoT component ["Ignite"] = {[0]=4}, ["Fire Vulnerability"] = {[0]=30}, -- WARLOCK ["Corruption"] = {[1]=12, [2]=15, [3]=18, [4]=18, [5]=18, [6]=18, [7]=18, [0]=18}, ["Immolate"] = {[1]=15, [2]=15, [3]=15, [4]=15, [5]=15, [6]=15, [7]=15, [8]=15, [0]=15}, ["Fear"] = {[1]=10, [2]=15, [3]=20, [0]=20}, ["Howl of Terror"] = {[1]=10, [2]=15, [0]=15}, ["Death Coil"] = {[0]=3}, ["Curse of Agony"] = {[1]=24, [2]=24, [3]=24, [4]=24, [5]=24, [6]=24, [0]=24}, ["Curse of Weakness"] = {[0]=120}, ["Curse of Recklessness"] = {[0]=120}, ["Curse of Tongues"] = {[0]=30}, ["Curse of the Elements"] = {[0]=300}, ["Curse of Shadow"] = {[0]=300}, ["Curse of Exhaustion"] = {[0]=12}, ["Curse of Doom"] = {[0]=60}, ["Siphon Life"] = {[0]=30}, ["Drain Life"] = {[0]=5}, ["Drain Mana"] = {[0]=5}, ["Drain Soul"] = {[0]=15}, ["Banish"] = {[1]=20, [2]=30, [0]=30}, ["Enslave Demon"] = {[0]=300}, ["Seduction"] = {[0]=15}, ["Shadow Vulnerability"] = {[0]=30}, ["Dark Harvest"] = {[0]=8}, -- PRIEST ["Shadow Word: Pain"] = {[1]=18, [2]=18, [3]=18, [4]=18, [5]=18, [6]=18, [7]=18, [8]=18, [0]=18}, ["Psychic Scream"] = {[1]=8, [2]=8, [3]=8, [4]=8, [0]=8}, ["Mind Flay"] = {[0]=3}, ["Mind Control"] = {[0]=60}, ["Silence"] = {[0]=5}, ["Weakened Soul"] = {[0]=15}, ["Devouring Plague"] = {[0]=24}, ["Vampiric Embrace"] = {[0]=60}, ["Blackout"] = {[0]=3}, ["Mana Burn"] = {[0]=0}, -- instant ["Touch of Weakness"] = {[0]=120}, ["Mind Soothe"] = {[0]=15}, -- HUNTER ["Serpent Sting"] = {[1]=15, [2]=15, [3]=15, [4]=15, [5]=15, [6]=15, [7]=15, [8]=15, [9]=15, [0]=15}, ["Viper Sting"] = {[1]=8, [2]=8, [3]=8, [4]=8, [0]=8}, ["Scorpid Sting"] = {[0]=20}, ["Concussive Shot"] = {[0]=4}, ["Scatter Shot"] = {[0]=4}, ["Wing Clip"] = {[0]=10}, ["Improved Concussive Shot"] = {[0]=3}, ["Hunter's Mark"] = {[0]=120}, ["Counterattack"] = {[0]=5}, ["Wyvern Sting"] = {[0]=12}, -- sleep, then 12s DoT ["Freezing Trap Effect"] = {[1]=10,[2]=15,[3]=20,[0]=20}, ["Immolation Trap Effect"] = {[0]=15}, ["Explosive Trap Effect"] = {[0]=20}, ["Frost Trap Aura"] = {[0]=8}, ["Intimidation"] = {[0]=3}, ["Entrapment"] = {[0]=5}, ["Lacerate"] = {[0]=8}, ["Scare Beast"] = {[1]=10,[2]=15,[3]=20,[0]=20}, -- DRUID ["Moonfire"] = {[1]=9, [2]=18, [3]=18, [4]=18, [5]=18, [6]=18, [7]=18, [8]=18, [9]=18, [10]=18, [0]=18}, ["Entangling Roots"] = {[1]=12, [2]=15, [3]=18, [4]=21, [5]=24, [6]=27, [0]=27}, ["Bash"] = {[1]=2, [2]=3, [3]=4, [0]=4}, ["Faerie Fire"] = {[0]=40}, ["Faerie Fire (Feral)"] = {[0]=40}, ["Rake"] = {[1]=9, [2]=9, [3]=9, [4]=9, [0]=9}, ["Rip"] = {[0]=8}, -- +2s per combo point, handled dynamically ["Pounce Bleed"] = {[0]=18}, ["Pounce"] = {[0]=3}, -- stun component ["Insect Swarm"] = {[0]=12}, ["Hibernate"] = {[1]=20, [2]=30, [3]=40, [0]=40}, ["Feral Charge Effect"] = {[0]=4}, ["Challenging Roar"] = {[0]=6}, ["Mangle"] = {[0]=12}, ["Growl"] = {[0]=3}, -- PALADIN ["Hammer of Justice"] = {[1]=3, [2]=4, [3]=5, [4]=6, [0]=6}, ["Turn Undead"] = {[0]=20}, ["Repentance"] = {[0]=6}, ["Crusader Strike"] = {[0]=30}, ["Judgement of the Crusader"] = {[0]=10}, ["Judgement of Light"] = {[0]=10}, ["Judgement of Wisdom"] = {[0]=10}, ["Judgement of Justice"] = {[0]=10}, ["Judgement"] = {[0]=10}, ["Vindication"] = {[0]=10}, -- SHAMAN ["Frost Shock"] = {[1]=8, [2]=8, [3]=8, [4]=8, [0]=8}, ["Earth Shock"] = {[0]=2}, -- interrupt ["Flame Shock"] = {[1]=12, [2]=12, [3]=12, [4]=12, [5]=12, [6]=12, [0]=12}, ["Earthbind"] = {[0]=5}, -- per pulse ["Stoneclaw Stun"] = {[0]=3}, ["Stormstrike"] = {[0]=12}, -- Felhunter ["Tainted Blood Effect"] = {[0]=10}, ["Spell Lock"] = {[0]=6}, -- MISC / PVP / RACIAL / PROCS ["War Stomp"] = {[0]=2}, ["Tidal Charm"] = {[0]=3}, ["Impact"] = {[0]=2}, ["Aftermath"] = {[0]=5}, ["Pyroclasm"] = {[0]=3}, ["Mace Stun Effect"] = {[0]=3}, ["Blackout"] = {[0]=3}, ["Revenge Stun"] = {[0]=3}, ["Sleep"] = {[1]=20, [2]=30, [0]=30}, ["Riposte"] = {[0]=6}, ["Shield Bash - Silenced"] = {[0]=6}, ["Kick - Silenced"] = {[0]=2}, ["Stun"] = {[0]=3}, -- ENGINEERING / ITEMS ["Iron Grenade"] = {[0]=3}, ["Thorium Grenade"] = {[0]=3}, ["Flash Bomb"] = {[0]=10}, ["Ez-Thro Dynamite"] = {[0]=2}, ["Ez-Thro Dynamite II"] = {[0]=2}, ["Dense Dynamite"] = {[0]=5}, ["Solid Dynamite"] = {[0]=3}, ["Hi-Explosive Bomb"] = {[0]=3}, ["Dark Iron Bomb"] = {[0]=4}, ["Goblin Sapper Charge"] = {[0]=0}, ["Goblin Mortar"] = {[0]=3}, ["The Big One"] = {[0]=5}, ["Net-o-Matic"] = {[0]=10}, ["Reckless Charge"] = {[0]=3}, ["Gnomish Mind Control Cap"] = {[0]=20}, ["Gnomish Net-o-Matic"] = {[0]=10}, ["Gnomish Death Ray"] = {[0]=0}, ["Goblin Rocket Helmet"] = {[0]=30}, ["Frost Grenade"] = {[0]=5}, ["Rough Copper Bomb"] = {[0]=2}, ["Large Copper Bomb"] = {[0]=2}, ["Small Bronze Bomb"] = {[0]=2}, ["Big Bronze Bomb"] = {[0]=3}, ["Big Iron Bomb"] = {[0]=3}, ["Goblin Land Mine"] = {[0]=5}, ["Discombobulator Ray"] = {[0]=12}, ["Large Rope Net"] = {[0]=10}, ["Trap"] = {[0]=10}, -- CONSUMABLES ["Free Action Potion"] = {[0]=30}, ["Living Action Potion"] = {[0]=5}, ["Skull of Impending Doom"] = {[0]=0}, ["Tidal Charm"] = {[0]=3}, -- Other ["Thunderfury"] = {[0]=12}, ["Thunderfury's Blessing"] = {[0]=12}, } -- Dynamic debuffs that scale with combo points NanamiPlates_SpellDB.COMBO_POINT_DEBUFFS = { ["Kidney Shot"] = true, ["Rupture"] = true, ["Rip"] = true, } -- Dynamic debuffs that scale with talents NanamiPlates_SpellDB.DYN_DEBUFFS = { ["Rupture"] = "Rupture", ["Kidney Shot"] = "Kidney Shot", ["Rip"] = "Rip", ["Rend"] = "Rend", ["Shadow Word: Pain"] = "Shadow Word: Pain", ["Demoralizing Shout"] = "Demoralizing Shout", ["Frostbolt"] = "Frostbolt", ["Gouge"] = "Gouge", } -- Shared debuffs that can only exist once on a target (shared across all players of the same class) NanamiPlates_SpellDB.SHARED_DEBUFFS = { -- Rogue ["Expose Armor"] = "ROGUE", -- Mage ["Winter's Chill"] = "MAGE", ["Fire Vulnerability"] = "MAGE", ["Frostbolt"] = "MAGE", -- Warrior ["Thunder Clap"] = "WARRIOR", ["Demoralizing Shout"] = "WARRIOR", ["Sunder Armor"] = "WARRIOR", ["Challenging Shout"] = "WARRIOR", ["Hamstring"] = "WARRIOR", ["Mortal Strike"] = "WARRIOR", ["Piercing Howl"] = "WARRIOR", ["Disarm"] = "WARRIOR", ["Taunt"] = true, -- Druid ["Faerie Fire"] = "DRUID", ["Faerie Fire (Feral)"] = "DRUID", ["Demoralizing Roar"] = "DRUID", ["Entangling Roots"] = "DRUID", ["Hibernate"] = "DRUID", ["Bash"] = "DRUID", ["Pounce"] = "DRUID", ["Challenging Roar"] = "DRUID", ["Feral Charge Effect"] = "DRUID", ["Mangle"] = "DRUID", ["Growl"] = true, -- Priest ["Shadow Vulnerability"] = "PRIEST", -- Can also be Warlock, but primarily Priest ["Silence"] = "PRIEST", ["Touch of Weakness"] = "PRIEST", ["Mind Soothe"] = "PRIEST", -- Warlock (curses are shared, but Malediction allows CoA + long curse) ["Curse of Agony"] = "WARLOCK", ["Curse of Weakness"] = "WARLOCK", ["Curse of Recklessness"] = "WARLOCK", ["Curse of Tongues"] = "WARLOCK", ["Curse of the Elements"] = "WARLOCK", ["Curse of Shadow"] = "WARLOCK", ["Curse of Exhaustion"] = "WARLOCK", ["Curse of Doom"] = "WARLOCK", ["Curse of Idiocy"] = "WARLOCK", -- Hunter ["Hunter's Mark"] = "HUNTER", ["Scorpid Sting"] = "HUNTER", ["Scare Beast"] = "HUNTER", ["Freezing Trap Effect"] = "HUNTER", ["Immolation Trap Effect"] = "HUNTER", ["Explosive Trap Effect"] = "HUNTER", ["Frost Trap Aura"] = "HUNTER", ["Entrapment"] = "HUNTER", -- Paladin ["Hammer of Justice"] = "PALADIN", ["Repentance"] = "PALADIN", ["Crusader Strike"] = "PALADIN", ["Vindication"] = "PALADIN", ["Judgement of the Crusader"] = "PALADIN", ["Judgement of Light"] = "PALADIN", ["Judgement of Wisdom"] = "PALADIN", ["Judgement of Justice"] = "PALADIN", ["Judgement"] = "PALADIN", -- Other ["Thunderfury"] = true, ["Thunderfury's Blessing"] = true, ["Gift of Arthas"] = true, ["Spell Vulnerability"] = true, ["Armor Shatter"] = true, } -- Rogue Poisons - always show for Rogue players with "Only My Debuffs" enabled -- Since only Rogues can apply these, they are always "mine" when present NanamiPlates_SpellDB.ROGUE_POISONS = { ["Crippling Poison"] = true, ["Crippling Poison II"] = true, ["Deadly Poison"] = true, ["Deadly Poison II"] = true, ["Deadly Poison III"] = true, ["Deadly Poison IV"] = true, ["Deadly Poison V"] = true, ["Instant Poison"] = true, ["Instant Poison II"] = true, ["Instant Poison III"] = true, ["Instant Poison IV"] = true, ["Instant Poison V"] = true, ["Instant Poison VI"] = true, ["Mind-numbing Poison"] = true, ["Mind-numbing Poison II"] = true, ["Mind-numbing Poison III"] = true, ["Wound Poison"] = true, ["Wound Poison II"] = true, ["Wound Poison III"] = true, ["Wound Poison IV"] = true, } -- Rogue Poison TEXTURES - for icon-based detection when tooltip scanning fails -- This ensures poisons are detected even without spell name NanamiPlates_SpellDB.ROGUE_POISON_TEXTURES = { -- Crippling Poison ["Interface\\Icons\\Ability_PoisonSting"] = "Crippling Poison", -- Deadly Poison ["Interface\\Icons\\Ability_Rogue_DualWeild"] = "Deadly Poison", -- Instant Poison ["Interface\\Icons\\Ability_Poisons"] = "Instant Poison", -- Mind-numbing Poison ["Interface\\Icons\\Spell_Nature_NullifyDisease"] = "Mind-numbing Poison", -- Wound Poison ["Interface\\Icons\\INV_Misc_Herb_16"] = "Wound Poison", } -- Hunter Traps - show for Hunter players when "Only My Debuffs" is enabled -- These are placed on ground and triggered by enemies, so ownership can't be tracked reliably NanamiPlates_SpellDB.HUNTER_TRAPS = { ["Freezing Trap Effect"] = true, ["Immolation Trap Effect"] = true, ["Explosive Trap Effect"] = true, ["Frost Trap Aura"] = true, ["Entrapment"] = true, } -- Hunter Trap TEXTURES - for icon-based detection when tooltip scanning fails NanamiPlates_SpellDB.HUNTER_TRAP_TEXTURES = { ["Interface\\Icons\\Spell_Frost_ChainsOfIce"] = "Freezing Trap Effect", ["Interface\\Icons\\Spell_Fire_FlameShock"] = "Immolation Trap Effect", ["Interface\\Icons\\Spell_Fire_SelfDestruct"] = "Explosive Trap Effect", ["Interface\\Icons\\Spell_Frost_FreezingBreath"] = "Frost Trap Aura", ["Interface\\Icons\\Spell_Frost_FrostNova"] = "Frost Trap Aura", ["Interface\\Icons\\Ability_Ensnare"] = "Entrapment", } -- Hunter Stings - for Hunter players, treat as owned for reliable display NanamiPlates_SpellDB.HUNTER_STINGS = { ["Serpent Sting"] = true, ["Viper Sting"] = true, ["Scorpid Sting"] = true, ["Wyvern Sting"] = true, } -- ============================================ -- WARLOCK MALEDICTION SUPPORT (TurtleWoW) -- Malediction talent allows 2 curses: 1 long curse + Curse of Agony -- ============================================ -- All Warlock curses - for detection when "Only My Debuffs" is enabled -- Since only Warlocks can apply curses, treat any visible curse as "mine" NanamiPlates_SpellDB.WARLOCK_CURSES = { ["Curse of Agony"] = true, ["Curse of Weakness"] = true, ["Curse of Recklessness"] = true, ["Curse of Tongues"] = true, ["Curse of the Elements"] = true, ["Curse of Shadow"] = true, ["Curse of Exhaustion"] = true, ["Curse of Doom"] = true, ["Curse of Idiocy"] = true, } -- Warlock Curse TEXTURES - for icon-based detection when tooltip scanning fails NanamiPlates_SpellDB.WARLOCK_CURSE_TEXTURES = { ["Interface\\Icons\\Spell_Shadow_CurseOfSargeras"] = "Curse of Agony", ["Interface\\Icons\\Spell_Shadow_CurseOfMannoroth"] = "Curse of Weakness", ["Interface\\Icons\\Spell_Shadow_UnholyStrength"] = "Curse of Recklessness", ["Interface\\Icons\\Spell_Shadow_CurseOfTounges"] = "Curse of Tongues", ["Interface\\Icons\\Spell_Shadow_ChillTouch"] = "Curse of the Elements", ["Interface\\Icons\\Spell_Shadow_CurseOfAchimonde"] = "Curse of Shadow", ["Interface\\Icons\\Spell_Shadow_GrimWard"] = "Curse of Exhaustion", ["Interface\\Icons\\Spell_Shadow_AuraOfDarkness"] = "Curse of Doom", ["Interface\\Icons\\Spell_Shadow_MindRot"] = "Curse of Idiocy", } -- All curses except Agony and Doom that can coexist with Agony under Malediction NanamiPlates_SpellDB.WARLOCK_LONG_CURSES = { ["Curse of Weakness"] = true, ["Curse of Recklessness"] = true, ["Curse of Tongues"] = true, ["Curse of the Elements"] = true, ["Curse of Shadow"] = true, ["Curse of Exhaustion"] = true, ["Curse of Idiocy"] = true, } -- Curse of Agony (short curse that can coexist with long curses under Malediction) NanamiPlates_SpellDB.WARLOCK_AGONY_CURSE = "Curse of Agony" -- Curses that auto-apply highest rank Curse of Agony when Malediction is active NanamiPlates_SpellDB.MALEDICTION_AUTO_AGONY = { ["Curse of Recklessness"] = true, ["Curse of Shadow"] = true, ["Curse of the Elements"] = true, } -- Cache for Corruption haste talent check ("快速衰弱" / "Nightfall" in TurtleWoW) -- This talent makes Corruption instant but shortens its tick interval and total -- duration by the player's total spell haste percentage. NanamiPlates_SpellDB.corruptionHasteCache = { hasChecked = false, talentRank = 0, lastCheck = 0, } function NanamiPlates_SpellDB:GetCorruptionHasteRank() local now = GetTime() local cache = self.corruptionHasteCache if cache.hasChecked and (now - cache.lastCheck) < 10 then return cache.talentRank end local _, playerClass = UnitClass("player") if playerClass ~= "WARLOCK" then cache.hasChecked = true cache.talentRank = 0 cache.lastCheck = now return 0 end for i = 1, 25 do local name, _, _, _, rank, maxRank = GetTalentInfo(1, i) if not name then break end if maxRank == 2 and (name == "\229\191\171\233\128\159\232\161\176\229\188\177" or name == "Nightfall" or name == "Rapid Deterioration") then cache.hasChecked = true cache.talentRank = rank or 0 cache.lastCheck = now return cache.talentRank end end cache.hasChecked = true cache.talentRank = 0 cache.lastCheck = now return 0 end -- Cache for Malediction talent check NanamiPlates_SpellDB.maledictionCache = { hasChecked = false, hasMalediction = false, lastCheck = 0, } -- Check if player has Malediction talent (TurtleWoW Affliction tree) -- Malediction is in Affliction tree (tree 1) - typically around tier 5-6 function NanamiPlates_SpellDB:HasMalediction() local now = GetTime() local cache = self.maledictionCache -- Cache result for 5 seconds to avoid excessive talent queries if cache.hasChecked and (now - cache.lastCheck) < 5 then return cache.hasMalediction end -- Only check for Warlocks local _, playerClass = UnitClass("player") if playerClass ~= "WARLOCK" then cache.hasChecked = true cache.hasMalediction = false cache.lastCheck = now return false end -- Search Affliction tree (tree 1) for Malediction talent -- TurtleWoW places it around tier 5-6, we scan all talents to be safe for i = 1, 25 do local name, _, _, _, rank, maxRank = GetTalentInfo(1, i) if not name then break end if maxRank == 1 and (name == "Malediction" or name == "\233\130\170\229\146\146") and rank and rank > 0 then cache.hasChecked = true cache.hasMalediction = true cache.lastCheck = now return true end end cache.hasChecked = true cache.hasMalediction = false cache.lastCheck = now return false end -- Check if two curses can coexist (Malediction allows long curse + Curse of Agony) -- Curse of Doom cannot coexist with Agony even with Malediction function NanamiPlates_SpellDB:CanCursesCoexist(curse1, curse2) if not self:HasMalediction() then return false end if curse1 == "Curse of Doom" or curse2 == "Curse of Doom" then return false end local isAgony1 = (curse1 == self.WARLOCK_AGONY_CURSE) local isAgony2 = (curse2 == self.WARLOCK_AGONY_CURSE) local isLong1 = self.WARLOCK_LONG_CURSES and self.WARLOCK_LONG_CURSES[curse1] local isLong2 = self.WARLOCK_LONG_CURSES and self.WARLOCK_LONG_CURSES[curse2] return (isAgony1 and isLong2) or (isAgony2 and isLong1) end -- Check if a curse is a warlock curse (for shared debuff handling) function NanamiPlates_SpellDB:IsWarlockCurse(effect) return effect == self.WARLOCK_AGONY_CURSE or self.WARLOCK_LONG_CURSES[effect] end -- Hunter Sting TEXTURES - for icon-based detection when tooltip scanning fails NanamiPlates_SpellDB.HUNTER_STING_TEXTURES = { ["Interface\\Icons\\Ability_Hunter_Quickshot"] = "Serpent Sting", ["Interface\\Icons\\Ability_Hunter_AimedShot"] = "Viper Sting", ["Interface\\Icons\\Ability_Hunter_CriticalShot"] = "Scorpid Sting", ["Interface\\Icons\\INV_Spear_02"] = "Wyvern Sting", } -- Warlock DOT TEXTURES - for icon-based detection when tooltip scanning returns localized names NanamiPlates_SpellDB.WARLOCK_DOT_TEXTURES = { ["Interface\\Icons\\Spell_Shadow_AbominationExplosion"] = "Corruption", ["Interface\\Icons\\Spell_Shadow_Requiem"] = "Siphon Life", ["Interface\\Icons\\Spell_Fire_Immolation"] = "Immolate", ["Interface\\Icons\\Spell_Shadow_SoulLeech"] = "Dark Harvest", } -- Debuffs that are bound to the owner (should be visible when "Only My Debuffs" is active) NanamiPlates_SpellDB.OWNER_BOUND_DEBUFFS = { -- Warrior ["Rend"] = true, ["Deep Wound"] = true, -- Warlock ["Immolate"] = true, ["Corruption"] = true, ["Curse of Agony"] = true, ["Curse of Weakness"] = true, ["Curse of Recklessness"] = true, ["Curse of Tongues"] = true, ["Curse of the Elements"] = true, ["Curse of Shadow"] = true, ["Curse of Exhaustion"] = true, ["Curse of Doom"] = true, ["Curse of Idiocy"] = true, ["Siphon Life"] = true, -- Priest ["Shadow Word: Pain"] = true, ["Devouring Plague"] = true, -- Druid ["Moonfire"] = true, ["Insect Swarm"] = true, ["Rip"] = true, ["Rake"] = true, ["Pounce Bleed"] = true, -- Rogue (excluding poisons - those are handled via ROGUE_POISONS visibility exception) ["Sap"] = true, ["Kidney Shot"] = true, ["Blind"] = true, ["Garrote"] = true, ["Rupture"] = true, ["Gouge"] = true, -- Hunter ["Serpent Sting"] = true, ["Wing Clip"] = true, ["Lacerate"] = true, -- Mage ["Ignite"] = true, -- Paladin ["Vindication"] = true, } -- ============================================ -- DEBUFF TRACKING STATE -- objects[unit][unitlevel][effect] = {effect, start, duration} -- ============================================ NanamiPlates_SpellDB.objects = {} NanamiPlates_SpellDB.pending = {} -- Array: [1]=unit, [2]=unitlevel, [3]=effect, [4]=duration -- ============================================ -- OWNER_BOUND_DEBUFFS OWNERSHIP CACHE -- Tracks player's own applications of OWNER_BOUND_DEBUFFS -- Format: ownerBoundCache[unit][effect] = {start, duration} -- Used to infer ownership when "Only My Debuffs" is enabled -- ============================================ NanamiPlates_SpellDB.ownerBoundCache = {} -- ============================================ -- DURATION LOOKUP FUNCTIONS -- ============================================ -- Search function to avoid duplication function NanamiPlates_SpellDB:FindEffectData(u, lvl, eff) if not self.objects[u] then return nil end if self.objects[u][lvl] and self.objects[u][lvl][eff] then return self.objects[u][lvl][eff] elseif self.objects[u][0] and self.objects[u][0][eff] then return self.objects[u][0][eff] else for l, effects in pairs(self.objects[u]) do if effects[eff] then return effects[eff] end end end return nil end -- Get max rank for a spell function NanamiPlates_SpellDB:GetMaxRank(effect) local spellData = self.DEBUFFS[effect] if not spellData then return 0 end local max = 0 for id in pairs(spellData) do if id > max then max = id end end return max end -- Get duration by spell name and rank function NanamiPlates_SpellDB:GetDuration(effect, rank) if not effect then return 0 end -- Translate localized name to English if needed if not self.DEBUFFS[effect] then if self.localeMap and self.localeMap[effect] then effect = self.localeMap[effect] elseif self.learnedLocale and self.learnedLocale[effect] then effect = self.learnedLocale[effect] end end local spellData = self.DEBUFFS[effect] if not spellData then return 0 end -- Parse rank from string like "Rank 2" if needed local rankNum = 0 if rank then if type(rank) == "number" then rankNum = rank elseif type(rank) == "string" then -- Extract number from "Rank X" format for num in string_gfind(rank, "(%d+)") do rankNum = tonumber(num) or 0 break end end end -- If exact rank not found, use max rank if not spellData[rankNum] then rankNum = self:GetMaxRank(effect) end local duration = spellData[rankNum] or spellData[0] or 0 -- Handle dynamic duration adjustments -- For combo point abilities, cache the CP count at cast time -- because combo points are consumed after cast local cp = GetComboPoints("player", "target") or 0 if cp > 0 and self.COMBO_POINT_DEBUFFS and self.COMBO_POINT_DEBUFFS[effect] then self.lastComboPoints = cp elseif cp == 0 and self.COMBO_POINT_DEBUFFS and self.COMBO_POINT_DEBUFFS[effect] then cp = self.lastComboPoints or 0 end if effect == self.DYN_DEBUFFS["Rupture"] then duration = duration + cp * 2 elseif effect == self.DYN_DEBUFFS["Kidney Shot"] then duration = duration + cp * 1 elseif effect == self.DYN_DEBUFFS["Rip"] then duration = duration + cp * 2 elseif effect == self.DYN_DEBUFFS["Demoralizing Shout"] then -- Booming Voice: 10% per talent local _,_,_,_,count = GetTalentInfo(2, 1) if count and count > 0 then duration = duration + (duration / 100 * (count * 10)) end elseif effect == self.DYN_DEBUFFS["Shadow Word: Pain"] then -- Improved Shadow Word: Pain: +3s per talent local _,_,_,_,count = GetTalentInfo(3, 4) if count and count > 0 then duration = duration + count * 3 end elseif effect == self.DYN_DEBUFFS["Frostbolt"] then -- Permafrost: +1s per talent local _,_,_,_,count = GetTalentInfo(3, 7) if count and count > 0 then duration = duration + count end elseif effect == self.DYN_DEBUFFS["Gouge"] then -- Improved Gouge: +.5s per talent local _,_,_,_,count = GetTalentInfo(2, 1) if count and count > 0 then duration = duration + (count * 0.5) end elseif effect == self.DYN_DEBUFFS["Rend"] then return duration end if effect == "Corruption" then local hasteRank = self:GetCorruptionHasteRank() if hasteRank > 0 then local hastePct = hasteRank * 0.03 duration = duration / (1 + hastePct) end end return duration end -- ============================================ -- PENDING SPELL TRACKING -- ============================================ -- Store recent casts by spell name for combat log fallback NanamiPlates_SpellDB.recentCasts = {} -- Secondary pending slot for Malediction dual-curse support NanamiPlates_SpellDB.pendingCurse = {} function NanamiPlates_SpellDB:AddPending(unit, unitlevel, effect, duration) if not unit or not effect then return end if not self.DEBUFFS[effect] then return end if duration <= 0 then return end -- Always store recent cast keyed by spell name (for combat log fallback) self.recentCasts[effect] = { duration = duration, time = GetTime() } -- Try to get GUID for unique identification (SuperWoW) local unitKey = unit if UnitGUID and UnitExists("target") and UnitName("target") == unit then local guid = UnitGUID and UnitGUID("target") if guid then unitKey = guid end end -- Malediction dual-curse handling: use separate pending slots for curses local isNewCurse = self:IsWarlockCurse(effect) local isExistingCurse = self.pending[3] and self:IsWarlockCurse(self.pending[3]) if isNewCurse and isExistingCurse and self:HasMalediction() then -- Both are curses and Malediction is active -- Check if they can coexist (one long curse + Curse of Agony) if self:CanCursesCoexist(effect, self.pending[3]) then -- Store the new curse in secondary pending slot self.pendingCurse[1] = unitKey self.pendingCurse[2] = unitlevel or 0 self.pendingCurse[3] = effect self.pendingCurse[4] = duration self.pendingCurse[5] = unit return -- Don't overwrite primary pending end end -- Standard pending: overwrite previous self.pending[1] = unitKey self.pending[2] = unitlevel or 0 self.pending[3] = effect self.pending[4] = duration self.pending[5] = unit -- Store original name for fallback lookups end function NanamiPlates_SpellDB:RemovePending() self.pending[1] = nil self.pending[2] = nil self.pending[3] = nil self.pending[4] = nil self.pending[5] = nil end -- Clear secondary curse pending slot (Malediction support) function NanamiPlates_SpellDB:RemovePendingCurse() self.pendingCurse[1] = nil self.pendingCurse[2] = nil self.pendingCurse[3] = nil self.pendingCurse[4] = nil self.pendingCurse[5] = nil end function NanamiPlates_SpellDB:PersistPending(effect) local persisted = false -- Check primary pending slot if self.pending[3] then if self.pending[3] == effect or (effect == nil and self.pending[3]) then -- Store by GUID (pending[1]) for accurate per-mob tracking -- Mark as isOwn = true since this is the player's own debuff self:RefreshEffect(self.pending[1], self.pending[2], self.pending[3], self.pending[4], true) -- Also store by name (pending[5]) as fallback for non-SuperWoW lookups if self.pending[5] and self.pending[5] ~= self.pending[1] then self:RefreshEffect(self.pending[5], self.pending[2], self.pending[3], self.pending[4], true) end persisted = true self:RemovePending() end end -- Check secondary curse pending slot (Malediction dual-curse support) if self.pendingCurse[3] then if self.pendingCurse[3] == effect or (effect == nil and self.pendingCurse[3]) then -- Store by GUID for accurate per-mob tracking self:RefreshEffect(self.pendingCurse[1], self.pendingCurse[2], self.pendingCurse[3], self.pendingCurse[4], true) -- Also store by name as fallback if self.pendingCurse[5] and self.pendingCurse[5] ~= self.pendingCurse[1] then self:RefreshEffect(self.pendingCurse[5], self.pendingCurse[2], self.pendingCurse[3], self.pendingCurse[4], true) end persisted = true self:RemovePendingCurse() end end return persisted end -- ============================================ -- EFFECT TRACKING -- ============================================ function NanamiPlates_SpellDB:AddEffect(unit, unitlevel, effect, duration, isOwn) if not unit or not effect then return end unitlevel = unitlevel or 0 if not self.objects[unit] then self.objects[unit] = {} end if not self.objects[unit][unitlevel] then self.objects[unit][unitlevel] = {} end local existing = self.objects[unit][unitlevel][effect] if existing and existing.start then if existing.duration and (existing.start + existing.duration) > GetTime() then return end end if not self.objects[unit][unitlevel][effect] then self.objects[unit][unitlevel][effect] = {} end self.objects[unit][unitlevel][effect].effect = effect self.objects[unit][unitlevel][effect].start = GetTime() self.objects[unit][unitlevel][effect].duration = duration or self:GetDuration(effect) self.objects[unit][unitlevel][effect].isOwn = isOwn or false end function NanamiPlates_SpellDB:RefreshEffect(unit, unitlevel, effect, duration, isOwn) if not unit or not effect then return end unitlevel = unitlevel or 0 -- Always refresh start time and duration if not self.objects[unit] then self.objects[unit] = {} end if not self.objects[unit][unitlevel] then self.objects[unit][unitlevel] = {} end if not self.objects[unit][unitlevel][effect] then self.objects[unit][unitlevel][effect] = {} end self.objects[unit][unitlevel][effect].effect = effect self.objects[unit][unitlevel][effect].start = GetTime() self.objects[unit][unitlevel][effect].duration = duration or self:GetDuration(effect) self.objects[unit][unitlevel][effect].isOwn = isOwn ~= false -- default to true for backwards compatibility end function NanamiPlates_SpellDB:UpdateDuration(unit, unitlevel, effect, duration) if not unit or not effect or not duration then return end unitlevel = unitlevel or 0 if self.objects[unit] and self.objects[unit][unitlevel] and self.objects[unit][unitlevel][effect] then self.objects[unit][unitlevel][effect].duration = duration end end -- ============================================ -- OWNER_BOUND_DEBUFFS OWNERSHIP TRACKING -- ============================================ -- Track player's application of an OWNER_BOUND_DEBUFF function NanamiPlates_SpellDB:TrackOwnerBoundDebuff(unit, effect, duration) if not unit or not effect then return end if not self.OWNER_BOUND_DEBUFFS[effect] then return end if not self.ownerBoundCache[unit] then self.ownerBoundCache[unit] = {} end self.ownerBoundCache[unit][effect] = { start = GetTime(), duration = duration or self:GetDuration(effect, 0) or 30 } end -- Check if player owns an OWNER_BOUND_DEBUFF on a unit -- Returns true if player has a valid (non-expired) cached application function NanamiPlates_SpellDB:IsOwnerBoundDebuffMine(unit, effect) if not unit or not effect then return false end if not self.OWNER_BOUND_DEBUFFS[effect] then return false end local cache = self.ownerBoundCache[unit] if not cache or not cache[effect] then return false end local entry = cache[effect] if not entry.start or not entry.duration then return false end local now = GetTime() local elapsed = now - entry.start -- Allow 2 second grace period beyond expected duration -- This handles slight timing variations if elapsed > entry.duration + 2 then -- Entry expired, clean it up cache[effect] = nil return false end return true end -- Clean up expired entries from ownerBoundCache function NanamiPlates_SpellDB:CleanupOwnerBoundCache() local now = GetTime() for unit, effects in pairs(self.ownerBoundCache) do local hasAny = false for effect, entry in pairs(effects) do if entry.start and entry.duration then local elapsed = now - entry.start if elapsed > entry.duration + 5 then -- Grace period expired, remove effects[effect] = nil else hasAny = true end else effects[effect] = nil end end -- Clean up empty unit tables if not hasAny then self.ownerBoundCache[unit] = nil end end end -- Remove a specific debuff tracking when it fades function NanamiPlates_SpellDB:RemoveOwnerBoundDebuff(unit, effect) if not unit or not effect then return end if self.ownerBoundCache[unit] then self.ownerBoundCache[unit][effect] = nil end end -- ============================================ -- UNITDEBUFF WRAPPER -- Returns: effect, rank, texture, stacks, dtype, duration, timeleft, isOwn -- ============================================ function NanamiPlates_SpellDB:UnitDebuff(unit, id) local unitname = UnitName(unit) local unitlevel = UnitLevel(unit) or 0 local texture, stacks, dtype = UnitDebuff(unit, id) local duration, timeleft = nil, -1 local rank = nil local effect = nil local isOwn = false if texture then -- Get spell name via tooltip scanning -- Try the unit first, but if it's a GUID and that fails, try "target" if it matches effect = self:ScanDebuff(unit, id) -- If scanning failed and this unit is the target, try scanning "target" instead if (not effect or effect == "") and UnitName("target") == unitname then effect = self:ScanDebuff("target", id) end effect = effect or "" end -- Check tracked debuffs with level if effect and effect ~= "" and (self.objects[unitname] or (UnitGUID and UnitGUID(unit) and self.objects[UnitGUID(unit)])) then local data = nil local unitguid = UnitGUID and UnitGUID(unit) local dataName = self:FindEffectData(unitname, unitlevel, effect) local dataGUID = unitguid and self:FindEffectData(unitguid, unitlevel, effect) if dataName and dataGUID then if (dataName.start or 0) >= (dataGUID.start or 0) then data = dataName else data = dataGUID end else data = dataName or dataGUID end if data and data.start and data.duration then -- Clean up expired if data.duration + data.start < GetTime() then -- Don't remove here, let it be cleaned up elsewhere data = nil else duration = data.duration timeleft = duration + data.start - GetTime() isOwn = data.isOwn == true end end end -- Fallback: if we have effect name but no tracked data, get duration from DB -- Don't set timeleft - let the caller handle untracked debuffs with their own timer cache if effect and effect ~= "" and (not duration or duration <= 0) then local dbDuration = self:GetDuration(effect, 0) if dbDuration and dbDuration > 0 then duration = dbDuration -- timeleft stays at -1, signaling caller to use their own timer cache end end return effect, rank, texture, stacks, dtype, duration, timeleft, isOwn end function NanamiPlates_SpellDB:InitScanner() if self.scanner then return end -- Create hidden tooltip for scanning self.scanner = CreateFrame("GameTooltip", "NanamiPlatesDebuffScanner", UIParent, "GameTooltipTemplate") self.scanner:SetOwner(UIParent, "ANCHOR_NONE") end -- Check if a string looks like a SuperWoW GUID local function IsGUID(unit) if not unit or type(unit) ~= "string" then return false end return string_sub(unit, 1, 2) == "0x" end function NanamiPlates_SpellDB:ScanDebuff(unit, index) if not self.scanner then self:InitScanner() end local texture = UnitDebuff(unit, index) if not texture then return nil end -- 1. Prioritize debuff-specific mappings (manually curated, always correct) if self.debuffPriority and self.debuffPriority[texture] then return self.debuffPriority[texture] end -- 2. Try tooltip scanning FIRST (most accurate, returns actual debuff name) local scanUnit = unit local canScanTooltip = true if IsGUID(unit) then if UnitExists("target") then local targetGUID = UnitGUID and UnitGUID("target") if targetGUID and targetGUID == unit then scanUnit = "target" else canScanTooltip = false end else canScanTooltip = false end end if canScanTooltip then self.scanner:ClearLines() self.scanner:SetUnitDebuff(scanUnit, index) local textLeft = getglobal("NanamiPlatesDebuffScannerTextLeft1") if textLeft then local effect = textLeft:GetText() if effect and effect ~= "" then local originalTooltipName = effect -- Try localeMap translation (static) if self.localeMap and self.localeMap[effect] then effect = self.localeMap[effect] end -- Try runtime learned locale cache if not self.DEBUFFS[effect] and self.learnedLocale and self.learnedLocale[effect] then effect = self.learnedLocale[effect] end -- Only use textureToSpell when tooltip name doesn't resolve in DEBUFFS if not self.DEBUFFS[effect] and texture and self.textureToSpell[texture] then local texName = self.textureToSpell[texture] if self.DEBUFFS[texName] then self:LearnLocale(originalTooltipName, texName) effect = texName end end -- Auto-learn: if tooltip name not in DB, but we just cast a known spell if not self.DEBUFFS[effect] and self.lastCastSpell and self.lastCastTime then if (GetTime() - self.lastCastTime) < 3 and self.DEBUFFS[self.lastCastSpell] then self:LearnLocale(originalTooltipName, self.lastCastSpell) effect = self.lastCastSpell self.lastCastSpell = nil end end -- Cache texture -> resolved name (only if we got a valid DEBUFFS match) if texture and effect and effect ~= "" and self.DEBUFFS[effect] then self.textureToSpell[texture] = effect end end if effect and effect ~= "" then return effect end end end -- 3. Fallback to textureToSpell (for non-target GUIDs or when tooltip produced nothing) if self.textureToSpell[texture] then return self.textureToSpell[texture] end return nil end -- Scan spellbook at login to build texture → spell mappings and locale learning function NanamiPlates_SpellDB:ScanSpellbook() self.learnedLocale = self.learnedLocale or {} -- Build a reverse map: English name → texture from our existing textureToSpell local engToTex = {} for tex, eng in pairs(self.textureToSpell) do if self.DEBUFFS[eng] then engToTex[eng] = tex end end local i = 1 while true do local localName, rank = GetSpellName(i, "spell") if not localName then break end local spellTex = GetSpellTexture(i, "spell") if spellTex and localName and localName ~= "" then -- Case 1: spellbook name is already in DEBUFFS (English client or matching name) if self.DEBUFFS[localName] then if not self.textureToSpell[spellTex] or not self.DEBUFFS[self.textureToSpell[spellTex]] then self.textureToSpell[spellTex] = localName end else -- Case 2: spellbook name is localized (e.g. Chinese) -- Try to find the English name via localeMap local engName = self.localeMap and self.localeMap[localName] if engName and self.DEBUFFS[engName] then self.learnedLocale[localName] = engName if not self.textureToSpell[spellTex] or not self.DEBUFFS[self.textureToSpell[spellTex]] then self.textureToSpell[spellTex] = engName end else -- Case 3: Try matching by texture path to find English name local existing = self.textureToSpell[spellTex] if existing and self.DEBUFFS[existing] and existing ~= localName then self.learnedLocale[localName] = existing end end end end i = i + 1 end end -- Learn locale mapping: associate a localized tooltip name with an English DEBUFFS name -- Called when we detect a debuff was just cast by the player function NanamiPlates_SpellDB:LearnLocale(localeName, englishName) if not localeName or not englishName or localeName == englishName then return end if localeName == "" or englishName == "" then return end if not self.DEBUFFS[englishName] then return end self.learnedLocale = self.learnedLocale or {} if not self.learnedLocale[localeName] then self.learnedLocale[localeName] = englishName end end -- Get spell name and rank from action bar slot by matching texture to spellbook -- Returns the HIGHEST rank spell that matches the texture (since all ranks share same texture) function NanamiPlates_SpellDB:ScanAction(slot) local actionTexture = GetActionTexture(slot) if not actionTexture then return nil, nil end -- Search through spellbook to find ALL matching textures and return highest rank local bestName, bestRank, bestRankNum = nil, nil, -1 local i = 1 while true do local spellName, spellRank = GetSpellName(i, "spell") if not spellName then break end local spellTexture = GetSpellTexture(i, "spell") if spellTexture and spellTexture == actionTexture then -- Parse rank number from "Rank X" string local rankNum = 0 if spellRank then for num in string.gfind(spellRank, "(%d+)") do rankNum = tonumber(num) or 0 break end end -- Keep track of highest rank found if rankNum > bestRankNum then bestName = spellName bestRank = spellRank bestRankNum = rankNum end end i = i + 1 end -- Return highest rank if found if bestName then return bestName, bestRank end -- If not found in spellbook, try tooltip as fallback if not self.scanner then self:InitScanner() end self.scanner:ClearLines() self.scanner:SetAction(slot) local textLeft = getglobal("NanamiPlatesDebuffScannerTextLeft1") local textRight = getglobal("NanamiPlatesDebuffScannerTextRight1") local effect = textLeft and textLeft:GetText() or nil local rank = textRight and textRight:GetText() or nil return effect, rank end -- Expose on the main addon table NanamiPlates.SpellDB = NanamiPlates_SpellDB