Files
Nanami-Plates/SpellDB.lua
2026-03-20 10:20:05 +08:00

1485 lines
51 KiB
Lua

-- 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