Files
SuperCleveRoidMacros/Extensions/MultiTargetTracker.lua
T
2026-01-02 22:31:02 -05:00

1419 lines
54 KiB
Lua

--[[
Multi-Target Debuff Tracker Extension (Cursive-style rewrite)
Tracks player's debuffs across multiple enemy targets
Based on Cursive addon architecture by Pepopo
Features:
- Per-class spell tables (only tracks spells you can cast)
- UNIT_CASTEVENT based tracking with debuff scan verification
- Resist/immune detection via chat parsing
- Display priority: raid marks -> highest HP -> rest
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
local Extension = CleveRoids.RegisterExtension("MultiTargetTracker")
-- ============================================================================
-- Configuration
-- ============================================================================
local MAX_TRACKED_TARGETS = 15
local MAX_DEBUFFS_PER_ROW = 8
local UPDATE_INTERVAL = 0.1 -- 10 Hz
local CONFIG = {
barHeight = 20,
barSpacing = 2,
targetIndicatorSize = 8,
raidIconSize = 16,
healthBarWidth = 140,
debuffIconSize = 20,
debuffIconSpacing = 2,
padding = 2,
titleHeight = 16,
textSize = 10,
}
local BACKDROP_BORDER = {
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 8,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
}
local BACKDROP_BACKGROUND = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
tile = true, tileSize = 16,
insets = { left = 0, right = 0, top = 0, bottom = 0 }
}
local COLORS = {
borderNormal = { 0.2, 0.2, 0.2, 1 },
borderHover = { 1, 1, 1, 1 },
borderCombat = { 0.8, 0.2, 0.2, 1 },
healthDefault = { 1, 0.8, 0.2, 1 },
timerNormal = { 1, 1, 1 },
timerExpiring = { 1, 0.2, 0.2 },
titleText = { 1, 0.82, 0, 1 },
}
-- ============================================================================
-- Per-Class Spell Tables (Cursive format)
-- ============================================================================
local playerClassName = nil -- Set in LoadClassSpells()
-- Combo point tracking for finisher duration calculation
local previousComboPoints = 0
local currentComboPoints = 0
local function GetComboPointsUsed()
if currentComboPoints == 0 then
return previousComboPoints
end
return currentComboPoints
end
-- Duration calculators for combo point scaling
local function getRipDuration()
return 8 + GetComboPointsUsed() * 2
end
local function getRuptureDuration()
local duration = 6 + GetComboPointsUsed() * 2
-- Check for Improved Rupture talent (Assassination tree, tier 10)
local _, _, _, _, count = GetTalentInfo(1, 10)
if count and count > 0 then
duration = duration + (count * 2)
end
return duration
end
local function getKidneyShotDuration()
return 1 + GetComboPointsUsed()
end
local function getGougeDuration()
-- Improved Gouge talent (Combat tree, tier 1)
local _, _, _, _, count = GetTalentInfo(2, 1)
if count and count > 0 then
return 4 + (count * 0.5)
end
return 4
end
-- Class spell tables
local function getDruidSpells()
return {
-- Entangling Roots
[339] = { name = "entangling roots", rank = 1, duration = 12 },
[1062] = { name = "entangling roots", rank = 2, duration = 15 },
[5195] = { name = "entangling roots", rank = 3, duration = 18 },
[5196] = { name = "entangling roots", rank = 4, duration = 21 },
[9852] = { name = "entangling roots", rank = 5, duration = 24 },
[9853] = { name = "entangling roots", rank = 6, duration = 27 },
-- Hibernate
[2637] = { name = "hibernate", rank = 1, duration = 20 },
[18657] = { name = "hibernate", rank = 2, duration = 30 },
[18658] = { name = "hibernate", rank = 3, duration = 40 },
-- Faerie Fire (all versions use same name for blocking)
[770] = { name = "faerie fire", rank = 1, duration = 40 },
[778] = { name = "faerie fire", rank = 2, duration = 40 },
[9749] = { name = "faerie fire", rank = 3, duration = 40 },
[9907] = { name = "faerie fire", rank = 4, duration = 40 },
[16855] = { name = "faerie fire", rank = 1, duration = 40 },
[17387] = { name = "faerie fire", rank = 2, duration = 40 },
[17388] = { name = "faerie fire", rank = 3, duration = 40 },
[17389] = { name = "faerie fire", rank = 4, duration = 40 },
[16857] = { name = "faerie fire", rank = 1, duration = 40 },
[17390] = { name = "faerie fire", rank = 2, duration = 40 },
[17391] = { name = "faerie fire", rank = 3, duration = 40 },
[17392] = { name = "faerie fire", rank = 4, duration = 40 },
-- Insect Swarm
[5570] = { name = "insect swarm", rank = 1, duration = 18 },
[24974] = { name = "insect swarm", rank = 2, duration = 18 },
[24975] = { name = "insect swarm", rank = 3, duration = 18 },
[24976] = { name = "insect swarm", rank = 4, duration = 18 },
[24977] = { name = "insect swarm", rank = 5, duration = 18 },
-- Moonfire
[8921] = { name = "moonfire", rank = 1, duration = 9 },
[8924] = { name = "moonfire", rank = 2, duration = 12 },
[8925] = { name = "moonfire", rank = 3, duration = 12 },
[8926] = { name = "moonfire", rank = 4, duration = 12 },
[8927] = { name = "moonfire", rank = 5, duration = 12 },
[8928] = { name = "moonfire", rank = 6, duration = 12 },
[8929] = { name = "moonfire", rank = 7, duration = 12 },
[9833] = { name = "moonfire", rank = 8, duration = 12 },
[9834] = { name = "moonfire", rank = 9, duration = 12 },
[9835] = { name = "moonfire", rank = 10, duration = 12 },
-- Rake
[1822] = { name = "rake", rank = 1, duration = 9 },
[1823] = { name = "rake", rank = 2, duration = 9 },
[1824] = { name = "rake", rank = 3, duration = 9 },
[9904] = { name = "rake", rank = 4, duration = 9 },
-- Rip (combo point scaling)
[1079] = { name = "rip", rank = 1, duration = 8, calculateDuration = getRipDuration },
[9492] = { name = "rip", rank = 2, duration = 8, calculateDuration = getRipDuration },
[9493] = { name = "rip", rank = 3, duration = 8, calculateDuration = getRipDuration },
[9752] = { name = "rip", rank = 4, duration = 8, calculateDuration = getRipDuration },
[9894] = { name = "rip", rank = 5, duration = 8, calculateDuration = getRipDuration },
[9896] = { name = "rip", rank = 6, duration = 8, calculateDuration = getRipDuration },
-- Bash
[5211] = { name = "bash", rank = 1, duration = 2 },
[6798] = { name = "bash", rank = 2, duration = 3 },
[8983] = { name = "bash", rank = 3, duration = 4 },
-- Demoralizing Roar
[99] = { name = "demoralizing roar", rank = 1, duration = 30 },
[1735] = { name = "demoralizing roar", rank = 2, duration = 30 },
[9490] = { name = "demoralizing roar", rank = 3, duration = 30 },
[9747] = { name = "demoralizing roar", rank = 4, duration = 30 },
[9898] = { name = "demoralizing roar", rank = 5, duration = 30 },
-- Pounce Bleed
[9005] = { name = "pounce bleed", rank = 1, duration = 18 },
[9823] = { name = "pounce bleed", rank = 2, duration = 18 },
[9827] = { name = "pounce bleed", rank = 3, duration = 18 },
}
end
local function getRogueSpells()
return {
-- Blind
[2094] = { name = "blind", rank = 1, duration = 10 },
[21060] = { name = "blind", rank = 1, duration = 10 },
-- Sap
[6770] = { name = "sap", rank = 1, duration = 25 },
[2070] = { name = "sap", rank = 2, duration = 35 },
[11297] = { name = "sap", rank = 3, duration = 45 },
-- Gouge (talent scaling)
[1776] = { name = "gouge", rank = 1, duration = 4, calculateDuration = getGougeDuration },
[1777] = { name = "gouge", rank = 2, duration = 4, calculateDuration = getGougeDuration },
[8629] = { name = "gouge", rank = 3, duration = 4, calculateDuration = getGougeDuration },
[11285] = { name = "gouge", rank = 4, duration = 4, calculateDuration = getGougeDuration },
[11286] = { name = "gouge", rank = 5, duration = 4, calculateDuration = getGougeDuration },
-- Rupture (combo point + talent scaling)
[1943] = { name = "rupture", rank = 1, duration = 6, calculateDuration = getRuptureDuration },
[8639] = { name = "rupture", rank = 2, duration = 6, calculateDuration = getRuptureDuration },
[8640] = { name = "rupture", rank = 3, duration = 6, calculateDuration = getRuptureDuration },
[11273] = { name = "rupture", rank = 4, duration = 6, calculateDuration = getRuptureDuration },
[11274] = { name = "rupture", rank = 5, duration = 6, calculateDuration = getRuptureDuration },
[11275] = { name = "rupture", rank = 6, duration = 6, calculateDuration = getRuptureDuration },
-- Kidney Shot (combo point scaling)
[408] = { name = "kidney shot", rank = 1, duration = 1, calculateDuration = getKidneyShotDuration },
[8643] = { name = "kidney shot", rank = 2, duration = 1, calculateDuration = getKidneyShotDuration },
-- Expose Armor
[8647] = { name = "expose armor", rank = 1, duration = 30 },
[8649] = { name = "expose armor", rank = 2, duration = 30 },
[8650] = { name = "expose armor", rank = 3, duration = 30 },
[11197] = { name = "expose armor", rank = 4, duration = 30 },
[11198] = { name = "expose armor", rank = 5, duration = 30 },
-- Garrote
[703] = { name = "garrote", rank = 1, duration = 18 },
[8631] = { name = "garrote", rank = 2, duration = 18 },
[8632] = { name = "garrote", rank = 3, duration = 18 },
[8633] = { name = "garrote", rank = 4, duration = 18 },
[11289] = { name = "garrote", rank = 5, duration = 18 },
[11290] = { name = "garrote", rank = 6, duration = 18 },
-- Deadly Poison
[2818] = { name = "deadly poison", rank = 1, duration = 12 },
[2819] = { name = "deadly poison ii", rank = 2, duration = 12 },
[11353] = { name = "deadly poison iii", rank = 3, duration = 12 },
[11354] = { name = "deadly poison iv", rank = 4, duration = 12 },
[25349] = { name = "deadly poison v", rank = 5, duration = 12 },
-- Hemorrhage
[16511] = { name = "hemorrhage", rank = 1, duration = 15 },
-- Cheap Shot
[1833] = { name = "cheap shot", rank = 1, duration = 4 },
[14902] = { name = "cheap shot", rank = 1, duration = 4 },
}
end
local function getWarriorSpells()
return {
-- Rend
[772] = { name = "rend", rank = 1, duration = 9 },
[6546] = { name = "rend", rank = 2, duration = 12 },
[6547] = { name = "rend", rank = 3, duration = 15 },
[6548] = { name = "rend", rank = 4, duration = 18 },
[11572] = { name = "rend", rank = 5, duration = 21 },
[11573] = { name = "rend", rank = 6, duration = 21 },
[11574] = { name = "rend", rank = 7, duration = 21 },
-- Hamstring
[1715] = { name = "hamstring", rank = 1, duration = 15 },
[7372] = { name = "hamstring", rank = 2, duration = 15 },
[7373] = { name = "hamstring", rank = 3, duration = 15 },
-- Thunder Clap
[6343] = { name = "thunder clap", rank = 1, duration = 10 },
[8198] = { name = "thunder clap", rank = 2, duration = 14 },
[8205] = { name = "thunder clap", rank = 3, duration = 18 },
[11580] = { name = "thunder clap", rank = 4, duration = 22 },
[11581] = { name = "thunder clap", rank = 5, duration = 26 },
-- Demoralizing Shout
[1160] = { name = "demoralizing shout", rank = 1, duration = 30 },
[6190] = { name = "demoralizing shout", rank = 2, duration = 30 },
[11554] = { name = "demoralizing shout", rank = 3, duration = 30 },
[11555] = { name = "demoralizing shout", rank = 4, duration = 30 },
[11556] = { name = "demoralizing shout", rank = 5, duration = 30 },
-- Sunder Armor
[7386] = { name = "sunder armor", rank = 1, duration = 30 },
[7405] = { name = "sunder armor", rank = 2, duration = 30 },
[8380] = { name = "sunder armor", rank = 3, duration = 30 },
[11596] = { name = "sunder armor", rank = 4, duration = 30 },
[11597] = { name = "sunder armor", rank = 5, duration = 30 },
}
end
local function getWarlockSpells()
return {
-- Corruption
[172] = { name = "corruption", rank = 1, duration = 12 },
[6222] = { name = "corruption", rank = 2, duration = 15 },
[6223] = { name = "corruption", rank = 3, duration = 18 },
[7648] = { name = "corruption", rank = 4, duration = 18 },
[11671] = { name = "corruption", rank = 5, duration = 18 },
[11672] = { name = "corruption", rank = 6, duration = 18 },
[25311] = { name = "corruption", rank = 7, duration = 18 },
-- Curse of Agony
[980] = { name = "curse of agony", rank = 1, duration = 24 },
[1014] = { name = "curse of agony", rank = 2, duration = 24 },
[6217] = { name = "curse of agony", rank = 3, duration = 24 },
[11711] = { name = "curse of agony", rank = 4, duration = 24 },
[11712] = { name = "curse of agony", rank = 5, duration = 24 },
[11713] = { name = "curse of agony", rank = 6, duration = 24 },
-- Siphon Life
[18265] = { name = "siphon life", rank = 1, duration = 30 },
[18879] = { name = "siphon life", rank = 2, duration = 30 },
[18880] = { name = "siphon life", rank = 3, duration = 30 },
[18881] = { name = "siphon life", rank = 4, duration = 30 },
-- Curse of Doom
[603] = { name = "curse of doom", rank = 1, duration = 60 },
-- Curse of Recklessness
[704] = { name = "curse of recklessness", rank = 1, duration = 120 },
[7658] = { name = "curse of recklessness", rank = 2, duration = 120 },
[7659] = { name = "curse of recklessness", rank = 3, duration = 120 },
[11717] = { name = "curse of recklessness", rank = 4, duration = 120 },
-- Curse of Shadow
[17862] = { name = "curse of shadow", rank = 1, duration = 300 },
[17937] = { name = "curse of shadow", rank = 2, duration = 300 },
-- Curse of the Elements
[1490] = { name = "curse of the elements", rank = 1, duration = 300 },
[11721] = { name = "curse of the elements", rank = 2, duration = 300 },
[11722] = { name = "curse of the elements", rank = 3, duration = 300 },
-- Curse of Tongues
[1714] = { name = "curse of tongues", rank = 1, duration = 30 },
[11719] = { name = "curse of tongues", rank = 2, duration = 30 },
-- Curse of Weakness
[702] = { name = "curse of weakness", rank = 1, duration = 120 },
[1108] = { name = "curse of weakness", rank = 2, duration = 120 },
[6205] = { name = "curse of weakness", rank = 3, duration = 120 },
[7646] = { name = "curse of weakness", rank = 4, duration = 120 },
[11707] = { name = "curse of weakness", rank = 5, duration = 120 },
[11708] = { name = "curse of weakness", rank = 6, duration = 120 },
-- Curse of Exhaustion
[18223] = { name = "curse of exhaustion", rank = 1, duration = 12 },
-- Immolate
[348] = { name = "immolate", rank = 1, duration = 15 },
[707] = { name = "immolate", rank = 2, duration = 15 },
[1094] = { name = "immolate", rank = 3, duration = 15 },
[2941] = { name = "immolate", rank = 4, duration = 15 },
[11665] = { name = "immolate", rank = 5, duration = 15 },
[11667] = { name = "immolate", rank = 6, duration = 15 },
[11668] = { name = "immolate", rank = 7, duration = 15 },
[25309] = { name = "immolate", rank = 8, duration = 15 },
-- Death Coil
[6789] = { name = "death coil", rank = 1, duration = 3 },
[17925] = { name = "death coil", rank = 2, duration = 3 },
[17926] = { name = "death coil", rank = 3, duration = 3 },
-- Banish
[710] = { name = "banish", rank = 1, duration = 20 },
[18647] = { name = "banish", rank = 2, duration = 30 },
-- Fear
[5782] = { name = "fear", rank = 1, duration = 10 },
[6213] = { name = "fear", rank = 2, duration = 15 },
[6215] = { name = "fear", rank = 3, duration = 20 },
}
end
local function getHunterSpells()
return {
-- Scorpid Sting
[3043] = { name = "scorpid sting", rank = 1, duration = 20 },
[14275] = { name = "scorpid sting", rank = 2, duration = 20 },
[14276] = { name = "scorpid sting", rank = 3, duration = 20 },
[14277] = { name = "scorpid sting", rank = 4, duration = 20 },
-- Serpent Sting
[1978] = { name = "serpent sting", rank = 1, duration = 15 },
[13549] = { name = "serpent sting", rank = 2, duration = 15 },
[13550] = { name = "serpent sting", rank = 3, duration = 15 },
[13551] = { name = "serpent sting", rank = 4, duration = 15 },
[13552] = { name = "serpent sting", rank = 5, duration = 15 },
[13553] = { name = "serpent sting", rank = 6, duration = 15 },
[13554] = { name = "serpent sting", rank = 7, duration = 15 },
[13555] = { name = "serpent sting", rank = 8, duration = 15 },
[25295] = { name = "serpent sting", rank = 9, duration = 15 },
-- Viper Sting
[3034] = { name = "viper sting", rank = 1, duration = 8 },
[14279] = { name = "viper sting", rank = 2, duration = 8 },
[14280] = { name = "viper sting", rank = 3, duration = 8 },
-- Wing Clip
[2974] = { name = "wing clip", rank = 1, duration = 10 },
[14267] = { name = "wing clip", rank = 2, duration = 10 },
[14268] = { name = "wing clip", rank = 3, duration = 10 },
-- Concussive Shot
[5116] = { name = "concussive shot", rank = 1, duration = 4 },
-- Wyvern Sting
[19386] = { name = "wyvern sting", rank = 1, duration = 12 },
[24132] = { name = "wyvern sting", rank = 2, duration = 12 },
[24133] = { name = "wyvern sting", rank = 3, duration = 12 },
-- Hunter's Mark
[1130] = { name = "hunter's mark", rank = 1, duration = 120 },
[14323] = { name = "hunter's mark", rank = 2, duration = 120 },
[14324] = { name = "hunter's mark", rank = 3, duration = 120 },
[14325] = { name = "hunter's mark", rank = 4, duration = 120 },
}
end
local function getMageSpells()
return {
-- Polymorph
[118] = { name = "polymorph", rank = 1, duration = 20 },
[12824] = { name = "polymorph", rank = 2, duration = 30 },
[12825] = { name = "polymorph", rank = 3, duration = 40 },
[12826] = { name = "polymorph", rank = 4, duration = 50 },
[28270] = { name = "polymorph: cow", rank = 1, duration = 50 },
[28271] = { name = "polymorph: turtle", rank = 1, duration = 50 },
[28272] = { name = "polymorph: pig", rank = 1, duration = 50 },
-- Frostbolt (snare component)
[116] = { name = "frostbolt", rank = 1, duration = 5 },
[205] = { name = "frostbolt", rank = 2, duration = 6 },
[837] = { name = "frostbolt", rank = 3, duration = 6 },
[7322] = { name = "frostbolt", rank = 4, duration = 7 },
[8406] = { name = "frostbolt", rank = 5, duration = 7 },
[8407] = { name = "frostbolt", rank = 6, duration = 8 },
[8408] = { name = "frostbolt", rank = 7, duration = 8 },
[10179] = { name = "frostbolt", rank = 8, duration = 9 },
[10180] = { name = "frostbolt", rank = 9, duration = 9 },
[10181] = { name = "frostbolt", rank = 10, duration = 9 },
[25304] = { name = "frostbolt", rank = 11, duration = 9 },
}
end
local function getPriestSpells()
return {
-- Shackle Undead
[9485] = { name = "shackle undead", rank = 1, duration = 30 },
[9486] = { name = "shackle undead", rank = 2, duration = 40 },
[10955] = { name = "shackle undead", rank = 3, duration = 50 },
-- Mind Control
[605] = { name = "mind control", rank = 1, duration = 60 },
[10911] = { name = "mind control", rank = 2, duration = 30 },
[10912] = { name = "mind control", rank = 3, duration = 30 },
-- Devouring Plague
[2944] = { name = "devouring plague", rank = 1, duration = 24 },
[19276] = { name = "devouring plague", rank = 2, duration = 24 },
[19277] = { name = "devouring plague", rank = 3, duration = 24 },
[19278] = { name = "devouring plague", rank = 4, duration = 24 },
[19279] = { name = "devouring plague", rank = 5, duration = 24 },
[19280] = { name = "devouring plague", rank = 6, duration = 24 },
-- Shadow Word: Pain
[589] = { name = "shadow word: pain", rank = 1, duration = 18 },
[594] = { name = "shadow word: pain", rank = 2, duration = 18 },
[970] = { name = "shadow word: pain", rank = 3, duration = 18 },
[992] = { name = "shadow word: pain", rank = 4, duration = 18 },
[2767] = { name = "shadow word: pain", rank = 5, duration = 18 },
[10892] = { name = "shadow word: pain", rank = 6, duration = 18 },
[10893] = { name = "shadow word: pain", rank = 7, duration = 18 },
[10894] = { name = "shadow word: pain", rank = 8, duration = 18 },
-- Vampiric Embrace
[15286] = { name = "vampiric embrace", rank = 1, duration = 60 },
-- Holy Fire (DoT component)
[14914] = { name = "holy fire", rank = 1, duration = 10 },
[15262] = { name = "holy fire", rank = 2, duration = 10 },
[15263] = { name = "holy fire", rank = 3, duration = 10 },
[15264] = { name = "holy fire", rank = 4, duration = 10 },
[15265] = { name = "holy fire", rank = 5, duration = 10 },
[15266] = { name = "holy fire", rank = 6, duration = 10 },
[15267] = { name = "holy fire", rank = 7, duration = 10 },
[15261] = { name = "holy fire", rank = 8, duration = 10 },
}
end
local function getShamanSpells()
return {
-- Flame Shock
[8050] = { name = "flame shock", rank = 1, duration = 12 },
[8052] = { name = "flame shock", rank = 2, duration = 12 },
[8053] = { name = "flame shock", rank = 3, duration = 12 },
[10447] = { name = "flame shock", rank = 4, duration = 12 },
[10448] = { name = "flame shock", rank = 5, duration = 12 },
[29228] = { name = "flame shock", rank = 6, duration = 12 },
-- Frost Shock (snare)
[8056] = { name = "frost shock", rank = 1, duration = 8 },
[8058] = { name = "frost shock", rank = 2, duration = 8 },
[10472] = { name = "frost shock", rank = 3, duration = 8 },
[10473] = { name = "frost shock", rank = 4, duration = 8 },
}
end
local function getPaladinSpells()
return {
-- Judgement of the Crusader
[21183] = { name = "judgement of the crusader", rank = 1, duration = 10 },
[20188] = { name = "judgement of the crusader", rank = 2, duration = 10 },
[20300] = { name = "judgement of the crusader", rank = 3, duration = 10 },
[20301] = { name = "judgement of the crusader", rank = 4, duration = 10 },
[20302] = { name = "judgement of the crusader", rank = 5, duration = 10 },
[20303] = { name = "judgement of the crusader", rank = 6, duration = 10 },
-- Judgement of Light
[20185] = { name = "judgement of light", rank = 1, duration = 10 },
[20267] = { name = "judgement of light", rank = 2, duration = 10 },
[20268] = { name = "judgement of light", rank = 3, duration = 10 },
[20271] = { name = "judgement of light", rank = 4, duration = 10 },
-- Judgement of Wisdom
[20186] = { name = "judgement of wisdom", rank = 1, duration = 10 },
[20354] = { name = "judgement of wisdom", rank = 2, duration = 10 },
[20355] = { name = "judgement of wisdom", rank = 3, duration = 10 },
-- Judgement of Justice
[20184] = { name = "judgement of justice", rank = 1, duration = 10 },
-- Hammer of Justice
[853] = { name = "hammer of justice", rank = 1, duration = 3 },
[5588] = { name = "hammer of justice", rank = 2, duration = 4 },
[5589] = { name = "hammer of justice", rank = 3, duration = 5 },
[10308] = { name = "hammer of justice", rank = 4, duration = 6 },
}
end
-- ============================================================================
-- State Variables
-- ============================================================================
local trackedSpellIds = {} -- spellID -> { name, rank, duration, texture, ... }
local trackedSpellNamesToTextures = {} -- "rake" -> texture path
local discoveredGuids = {} -- guid -> timestamp (when discovered)
local trackedDebuffs = {} -- guid -> { [spellName] = { spellID, rank, duration, start, texture } }
local pendingCast = {} -- Current pending cast { spellID, targetGuid, time }
local lastTargetGuid = nil -- Last target GUID for resist detection
local mainFrame = nil
local unitFrames = {} -- [row] -> frame
local isFrameVisible = false
local isDisabled = false
local isUnlocked = false
-- ============================================================================
-- Spell Loading
-- ============================================================================
local function LoadClassSpells()
trackedSpellIds = {}
trackedSpellNamesToTextures = {}
-- Get player class at load time (not file parse time)
local _, className = UnitClass("player")
playerClassName = className
if not playerClassName then
CleveRoids.Print("MultiTargetTracker: Could not determine player class")
return false
end
local spells = nil
if playerClassName == "DRUID" then
spells = getDruidSpells()
elseif playerClassName == "ROGUE" then
spells = getRogueSpells()
elseif playerClassName == "WARRIOR" then
spells = getWarriorSpells()
elseif playerClassName == "WARLOCK" then
spells = getWarlockSpells()
elseif playerClassName == "HUNTER" then
spells = getHunterSpells()
elseif playerClassName == "MAGE" then
spells = getMageSpells()
elseif playerClassName == "PRIEST" then
spells = getPriestSpells()
elseif playerClassName == "SHAMAN" then
spells = getShamanSpells()
elseif playerClassName == "PALADIN" then
spells = getPaladinSpells()
end
if not spells then
CleveRoids.Print("MultiTargetTracker: No spells defined for class " .. tostring(playerClassName))
return false
end
local spellCount = 0
for spellID, data in pairs(spells) do
-- Get texture via SpellInfo
local name, rank, texture = SpellInfo(spellID)
data.texture = texture
trackedSpellIds[spellID] = data
trackedSpellNamesToTextures[data.name] = texture
spellCount = spellCount + 1
end
return true, spellCount
end
-- ============================================================================
-- GUID Discovery (Cursive core.lua style)
-- ============================================================================
local function AddGuid(guid)
if not guid or guid == "" or guid == "0x0" or guid == "0x000000000" then
return
end
if string.sub(guid, 1, 2) ~= "0x" then
return
end
if UnitExists(guid) and not UnitIsDead(guid) then
discoveredGuids[guid] = GetTime()
end
end
local function AddUnit(unit)
local _, guid = UnitExists(unit)
if guid and not UnitIsDead(unit) then
discoveredGuids[guid] = GetTime()
end
end
local function RemoveGuid(guid)
discoveredGuids[guid] = nil
trackedDebuffs[guid] = nil
end
-- ============================================================================
-- Debuff Scanning
-- ============================================================================
local function ScanGuidForDebuff(guid, spellID)
if not CleveRoids.hasSuperwow then return false end
-- Scan debuff slots 1-16
for slot = 1, 16 do
local _, _, _, id = UnitDebuff(guid, slot)
if not id then break end
if id == spellID then return true end
end
-- Scan buff slots (for overflow debuffs) 1-32
for slot = 1, 32 do
local _, _, id = UnitBuff(guid, slot)
if not id then break end
if id == spellID then return true end
end
return false
end
local function ScanGuidForAllDebuffs(guid)
if not CleveRoids.hasSuperwow then return end
if not trackedDebuffs[guid] then return end
for spellName, debuffData in pairs(trackedDebuffs[guid]) do
if not ScanGuidForDebuff(guid, debuffData.spellID) then
-- Debuff no longer on target, remove it
trackedDebuffs[guid][spellName] = nil
end
end
end
-- ============================================================================
-- Debuff Application
-- ============================================================================
local function GetDebuffDuration(spellID)
local spellData = trackedSpellIds[spellID]
if not spellData then return 0 end
if spellData.calculateDuration then
return spellData.calculateDuration()
end
return spellData.duration or 0
end
local function ApplyDebuff(spellID, targetGuid, startTime, duration)
local spellData = trackedSpellIds[spellID]
if not spellData then return end
-- Verify debuff is actually on target
if not ScanGuidForDebuff(targetGuid, spellID) then
return
end
if not trackedDebuffs[targetGuid] then
trackedDebuffs[targetGuid] = {}
end
trackedDebuffs[targetGuid][spellData.name] = {
spellID = spellID,
rank = spellData.rank,
duration = duration,
start = startTime,
texture = spellData.texture,
}
-- Ensure GUID is in discovery list
discoveredGuids[targetGuid] = GetTime()
end
local function RemoveDebuff(guid, spellName)
if trackedDebuffs[guid] and trackedDebuffs[guid][spellName] then
trackedDebuffs[guid][spellName] = nil
end
end
local function TimeRemaining(debuffData)
local remaining = debuffData.duration - (GetTime() - debuffData.start)
return remaining > 0 and remaining or 0
end
local function HasAnyDebuff(guid)
if trackedDebuffs[guid] and next(trackedDebuffs[guid]) then
return true
end
return false
end
-- ============================================================================
-- Event Handlers
-- ============================================================================
local eventFrame = CreateFrame("Frame", "CleveRoidsMTTEventFrame", UIParent)
-- GUID Discovery events
eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
eventFrame:RegisterEvent("UNIT_COMBAT")
eventFrame:RegisterEvent("UNIT_MODEL_CHANGED")
-- Cast tracking
eventFrame:RegisterEvent("UNIT_CASTEVENT")
-- Resist/immune detection
eventFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
-- Fade detection
eventFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER")
-- Combo points for finisher tracking
eventFrame:RegisterEvent("PLAYER_COMBO_POINTS")
-- Resist patterns
local resist_pattern = "Your (.+) was resisted by (.+)"
local immune_pattern = "Your (.+) fail.+%. (.+) is immune"
local missed_pattern = "Your (.+) missed (.+)"
local parried_pattern = "Your (.+) is parried by (.+)"
local blocked_pattern = "Your (.+) was blocked by (.+)"
local dodged_pattern = "Your (.+) was dodged by (.+)"
-- Fade pattern
local fades_pattern = "(.+) fades from (.+)"
eventFrame:SetScript("OnEvent", function()
if event == "PLAYER_TARGET_CHANGED" then
AddUnit("target")
elseif event == "UNIT_COMBAT" or event == "UNIT_MODEL_CHANGED" then
-- arg1 is GUID in SuperWoW
if arg1 then
AddGuid(arg1)
end
elseif event == "UNIT_CASTEVENT" then
-- UNIT_CASTEVENT args:
-- arg1 = casterGUID
-- arg2 = targetGUID
-- arg3 = event type ("START", "CAST", "FAIL", "CHANNEL")
-- arg4 = spell ID
-- arg5 = cast duration
local casterGuid = arg1
local targetGuid = arg2
local eventType = arg3
local spellID = arg4
-- Check if caster is player
local _, playerGuid = UnitExists("player")
if not playerGuid or casterGuid ~= playerGuid then
return
end
if eventType == "CAST" then
if trackedSpellIds[spellID] and targetGuid then
lastTargetGuid = targetGuid
-- Calculate duration
local duration = GetDebuffDuration(spellID)
-- Apply debuff after a short delay to account for travel time
local delay = 0.2
local _, _, ping = GetNetStats()
if ping and ping > 0 and ping < 500 then
delay = 0.05 + (ping / 1000)
end
-- Store pending cast
pendingCast = {
spellID = spellID,
targetGuid = targetGuid,
time = GetTime(),
duration = duration - delay,
}
-- Schedule application
local applyFrame = CreateFrame("Frame")
applyFrame.elapsed = 0
applyFrame:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed >= delay then
if pendingCast.spellID == spellID and pendingCast.targetGuid == targetGuid then
ApplyDebuff(spellID, targetGuid, GetTime(), pendingCast.duration)
pendingCast = {}
end
this:SetScript("OnUpdate", nil)
end
end)
end
elseif eventType == "START" then
if trackedSpellIds[spellID] and targetGuid then
pendingCast = {
spellID = spellID,
targetGuid = targetGuid,
time = GetTime(),
}
end
elseif eventType == "FAIL" then
pendingCast = {}
end
elseif event == "CHAT_MSG_SPELL_SELF_DAMAGE" then
local message = arg1
local patterns = { resist_pattern, immune_pattern }
-- Add melee patterns for melee classes
if playerClassName == "DRUID" or playerClassName == "ROGUE" or playerClassName == "WARRIOR" then
table.insert(patterns, missed_pattern)
table.insert(patterns, parried_pattern)
table.insert(patterns, blocked_pattern)
table.insert(patterns, dodged_pattern)
end
for _, pattern in pairs(patterns) do
local _, _, spellName, target = string.find(message, pattern)
if spellName and target then
-- Cancel pending cast
pendingCast = {}
return
end
end
elseif event == "CHAT_MSG_SPELL_AURA_GONE_OTHER" then
local message = arg1
local _, _, spellName, target = string.find(message, fades_pattern)
if spellName and target then
local lowerSpellName = string.lower(spellName)
-- Check if this is a spell we track
if trackedSpellNamesToTextures[lowerSpellName] then
-- Find the GUID for this target and verify
for guid, debuffs in pairs(trackedDebuffs) do
if debuffs[lowerSpellName] then
if not ScanGuidForDebuff(guid, debuffs[lowerSpellName].spellID) then
RemoveDebuff(guid, lowerSpellName)
end
end
end
end
end
elseif event == "PLAYER_COMBO_POINTS" then
previousComboPoints = currentComboPoints
currentComboPoints = GetComboPoints()
end
end)
-- ============================================================================
-- UI Creation
-- ============================================================================
local function GetBarWidth()
return CONFIG.targetIndicatorSize + CONFIG.raidIconSize + CONFIG.padding +
CONFIG.healthBarWidth + CONFIG.padding +
(MAX_DEBUFFS_PER_ROW * (CONFIG.debuffIconSize + CONFIG.debuffIconSpacing))
end
local function FormatHealth(hp)
if not hp then return "" end
if hp >= 1000000 then
return string.format("%.1fm", hp / 1000000)
elseif hp >= 1000 then
return string.format("%.1fk", hp / 1000)
end
return tostring(hp)
end
local function CreateUnitFrame(parent, index)
local frame = CreateFrame("Frame", "CleveRoidsMTTRow" .. index, parent)
frame:SetWidth(GetBarWidth())
frame:SetHeight(CONFIG.barHeight)
frame.guid = nil
frame.hover = false
-- First section: target indicator + raid icon
local firstSection = CreateFrame("Frame", nil, frame)
firstSection:SetPoint("LEFT", frame, "LEFT", 0, 0)
firstSection:SetWidth(CONFIG.targetIndicatorSize + CONFIG.raidIconSize + CONFIG.padding)
firstSection:SetHeight(CONFIG.barHeight)
frame.firstSection = firstSection
-- Target indicator
local targetIndicator = firstSection:CreateTexture(nil, "OVERLAY")
targetIndicator:SetWidth(CONFIG.targetIndicatorSize)
targetIndicator:SetHeight(8)
targetIndicator:SetPoint("LEFT", firstSection, "LEFT", 0, 0)
targetIndicator:SetTexture("Interface\\AddOns\\Cursive\\img\\target-left")
targetIndicator:Hide()
frame.targetIndicator = targetIndicator
-- Raid icon
local raidIcon = firstSection:CreateTexture(nil, "OVERLAY")
raidIcon:SetWidth(CONFIG.raidIconSize)
raidIcon:SetHeight(CONFIG.raidIconSize)
raidIcon:SetPoint("RIGHT", firstSection, "RIGHT", 0, 0)
raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
raidIcon:Hide()
frame.raidIcon = raidIcon
-- Second section: health bar (clickable)
local secondSection = CreateFrame("Button", nil, frame)
secondSection:SetPoint("LEFT", firstSection, "RIGHT", 0, 0)
secondSection:SetWidth(CONFIG.healthBarWidth + CONFIG.padding)
secondSection:SetHeight(CONFIG.barHeight)
secondSection.parent = frame
frame.secondSection = secondSection
-- Click to target
secondSection:RegisterForClicks("LeftButtonUp", "RightButtonUp")
secondSection:SetScript("OnClick", function()
if this.parent.guid then
if arg1 == "LeftButton" then
TargetUnit(this.parent.guid)
elseif arg1 == "RightButton" then
TargetUnit(this.parent.guid)
if not PlayerFrame.inCombat then
AttackTarget()
end
end
end
end)
secondSection:SetScript("OnEnter", function()
this.parent.hover = true
if this.parent.healthBar and this.parent.healthBar.border then
this.parent.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1)
end
if this.parent.guid then
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.parent.guid)
GameTooltip:Show()
end
end)
secondSection:SetScript("OnLeave", function()
this.parent.hover = false
GameTooltip:Hide()
end)
-- Health bar
local healthBar = CreateFrame("StatusBar", nil, secondSection)
healthBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
healthBar:SetStatusBarColor(COLORS.healthDefault[1], COLORS.healthDefault[2], COLORS.healthDefault[3], 1)
healthBar:SetMinMaxValues(0, 100)
healthBar:SetValue(100)
healthBar:SetPoint("LEFT", secondSection, "LEFT", CONFIG.padding, 0)
healthBar:SetWidth(CONFIG.healthBarWidth)
healthBar:SetHeight(CONFIG.barHeight)
frame.healthBar = healthBar
-- Health bar backdrop
healthBar:SetBackdrop(BACKDROP_BACKGROUND)
healthBar:SetBackdropColor(0, 0, 0, 1)
-- Health bar border
local border = CreateFrame("Frame", nil, healthBar)
border:SetBackdrop(BACKDROP_BORDER)
border:SetBackdropBorderColor(COLORS.borderNormal[1], COLORS.borderNormal[2], COLORS.borderNormal[3], 1)
border:SetPoint("TOPLEFT", healthBar, "TOPLEFT", -2, 2)
border:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", 2, -2)
healthBar.border = border
-- HP text
local hpText = healthBar:CreateFontString(nil, "OVERLAY", "GameFontWhite")
hpText:SetPoint("TOPRIGHT", healthBar, "TOPRIGHT", -2, -2)
hpText:SetFont(STANDARD_TEXT_FONT, CONFIG.textSize, "OUTLINE")
hpText:SetJustifyH("RIGHT")
frame.hpText = hpText
-- Name text
local nameText = healthBar:CreateFontString(nil, "OVERLAY", "GameFontWhite")
nameText:SetPoint("TOPLEFT", healthBar, "TOPLEFT", 2, -2)
nameText:SetPoint("BOTTOMRIGHT", hpText, "BOTTOMLEFT", -2, 0)
nameText:SetFont(STANDARD_TEXT_FONT, CONFIG.textSize, "OUTLINE")
nameText:SetJustifyH("LEFT")
frame.nameText = nameText
-- Third section: debuff icons
local thirdSection = CreateFrame("Frame", nil, frame)
thirdSection:SetPoint("LEFT", secondSection, "RIGHT", 0, 0)
thirdSection:SetWidth(MAX_DEBUFFS_PER_ROW * (CONFIG.debuffIconSize + CONFIG.debuffIconSpacing))
thirdSection:SetHeight(CONFIG.barHeight)
frame.thirdSection = thirdSection
-- Debuff icons
frame.debuffIcons = {}
for i = 1, MAX_DEBUFFS_PER_ROW do
local icon = thirdSection:CreateTexture(nil, "OVERLAY")
icon:SetWidth(CONFIG.debuffIconSize)
icon:SetHeight(CONFIG.debuffIconSize)
icon:SetPoint("LEFT", thirdSection, "LEFT", (i - 1) * (CONFIG.debuffIconSize + CONFIG.debuffIconSpacing) + CONFIG.padding, 0)
icon:Hide()
local timer = thirdSection:CreateFontString(nil, "OVERLAY", "GameFontNormal")
timer:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
timer:SetTextColor(1, 1, 1)
timer:SetAllPoints(icon)
timer:Hide()
icon.timer = timer
frame.debuffIcons[i] = icon
end
frame:Hide()
return frame
end
local function CreateMainFrame()
local frame = CreateFrame("Frame", "CleveRoidsMultiTargetFrame", UIParent)
frame:SetWidth(GetBarWidth() + 10)
frame:SetHeight(CONFIG.titleHeight + (MAX_TRACKED_TARGETS * (CONFIG.barHeight + CONFIG.barSpacing)) + 10)
frame:SetPoint("CENTER", UIParent, "CENTER", 200, 100)
frame:SetBackdrop(BACKDROP_BACKGROUND)
frame:SetBackdropColor(0, 0, 0, 0.8)
frame:EnableMouse(true)
frame:SetMovable(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function()
if isUnlocked then
this:StartMoving()
end
end)
frame:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
SaveFramePosition()
end)
-- Title
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontWhite")
title:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -2)
title:SetFont(STANDARD_TEXT_FONT, CONFIG.textSize, "OUTLINE")
title:SetText("Debuff Tracker")
title:SetTextColor(COLORS.titleText[1], COLORS.titleText[2], COLORS.titleText[3], 1)
frame.titleText = title
-- Create unit frames
for i = 1, MAX_TRACKED_TARGETS do
local unitFrame = CreateUnitFrame(frame, i)
unitFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -(CONFIG.titleHeight + (i - 1) * (CONFIG.barHeight + CONFIG.barSpacing)))
unitFrames[i] = unitFrame
end
frame:Hide()
return frame
end
-- ============================================================================
-- Frame Position Saving
-- ============================================================================
local function SaveFramePosition()
if not mainFrame then return end
local point, relativeTo, relativePoint, xOfs, yOfs = mainFrame:GetPoint()
CleveRoidMacros = CleveRoidMacros or {}
CleveRoidMacros.multiTargetPos = {
point = point,
relativePoint = relativePoint,
x = xOfs,
y = yOfs,
}
end
local function RestoreFramePosition()
if not mainFrame then return end
CleveRoidMacros = CleveRoidMacros or {}
local pos = CleveRoidMacros.multiTargetPos
if pos and pos.point then
mainFrame:ClearAllPoints()
mainFrame:SetPoint(pos.point, UIParent, pos.relativePoint or pos.point, pos.x or 0, pos.y or 0)
end
end
-- ============================================================================
-- Display Logic (Cursive-style priority)
-- ============================================================================
local function ShouldDisplayGuid(guid)
if not UnitExists(guid) then return false end
if UnitIsDead(guid) then return false end
if not HasAnyDebuff(guid) then return false end
return true
end
local function GetSortedDebuffs(guidDebuffs)
local debuffList = {}
for name, data in pairs(guidDebuffs) do
table.insert(debuffList, { name = name, data = data })
end
-- Sort by time remaining (soonest first)
table.sort(debuffList, function(a, b)
return TimeRemaining(a.data) < TimeRemaining(b.data)
end)
return debuffList
end
local function UpdateUnitFrame(frame, guid)
frame.guid = guid
-- Update health bar
if frame.healthBar then
frame.healthBar:SetMinMaxValues(0, UnitHealthMax(guid) or 100)
frame.healthBar:SetValue(UnitHealth(guid) or 0)
end
-- Update name
local name = UnitName(guid)
if name and frame.nameText then
frame.nameText:SetText(name)
end
-- Update HP text
if frame.hpText then
local hp = UnitHealth(guid)
frame.hpText:SetText(FormatHealth(hp))
end
-- Update border color
if frame.healthBar and frame.healthBar.border then
if frame.hover then
frame.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1)
elseif UnitAffectingCombat(guid) then
frame.healthBar.border:SetBackdropBorderColor(0.8, 0.2, 0.2, 1)
else
frame.healthBar.border:SetBackdropBorderColor(0.2, 0.2, 0.2, 1)
end
end
-- Update target indicator
if frame.targetIndicator then
if UnitIsUnit("target", guid) then
frame.targetIndicator:Show()
else
frame.targetIndicator:Hide()
end
end
-- Update raid icon
if frame.raidIcon then
local raidIndex = GetRaidTargetIndex(guid)
if raidIndex then
SetRaidTargetIconTexture(frame.raidIcon, raidIndex)
frame.raidIcon:Show()
else
frame.raidIcon:Hide()
end
end
-- Update debuff icons
for i = 1, MAX_DEBUFFS_PER_ROW do
frame.debuffIcons[i]:Hide()
frame.debuffIcons[i].timer:Hide()
end
local guidDebuffs = trackedDebuffs[guid]
if guidDebuffs then
local sortedDebuffs = GetSortedDebuffs(guidDebuffs)
for i, debuff in ipairs(sortedDebuffs) do
if i > MAX_DEBUFFS_PER_ROW then break end
local icon = frame.debuffIcons[i]
local remaining = TimeRemaining(debuff.data)
if remaining > 0 then
icon:SetTexture(debuff.data.texture)
icon:Show()
-- Timer text
local timerText
if remaining < 10 then
timerText = string.format("%.1f", remaining)
else
timerText = string.format("%d", math.ceil(remaining))
end
icon.timer:SetText(timerText)
-- Timer color
if remaining < 3 then
icon.timer:SetTextColor(1, 0.2, 0.2)
else
icon.timer:SetTextColor(1, 1, 1)
end
icon.timer:Show()
end
end
end
frame:Show()
end
local function UpdateDisplay()
if isDisabled then return end
-- Cleanup old/dead GUIDs
for guid, time in pairs(discoveredGuids) do
if not UnitExists(guid) or UnitIsDead(guid) then
RemoveGuid(guid)
elseif GetTime() - time > 900 and not UnitIsVisible(guid) then
-- 15 minutes old and not visible
RemoveGuid(guid)
end
end
-- Collect displayable GUIDs
local displayable = {}
-- Priority 1: Raid marked targets
for i = 8, 1, -1 do
local _, guid = UnitExists("mark" .. i)
if guid and ShouldDisplayGuid(guid) then
table.insert(displayable, guid)
end
end
-- Priority 2: Highest HP targets (top 3)
local hpTargets = {}
for guid, _ in pairs(discoveredGuids) do
if ShouldDisplayGuid(guid) then
local found = false
for _, dg in ipairs(displayable) do
if dg == guid then found = true break end
end
if not found then
table.insert(hpTargets, { guid = guid, hp = UnitHealthMax(guid) or 0 })
end
end
end
table.sort(hpTargets, function(a, b) return a.hp > b.hp end)
for i = 1, 3 do
if hpTargets[i] then
table.insert(displayable, hpTargets[i].guid)
end
end
-- Priority 3: Remaining targets
for guid, _ in pairs(discoveredGuids) do
if table.getn(displayable) >= MAX_TRACKED_TARGETS then break end
if ShouldDisplayGuid(guid) then
local found = false
for _, dg in ipairs(displayable) do
if dg == guid then found = true break end
end
if not found then
table.insert(displayable, guid)
end
end
end
-- Update unit frames
local showFrame = false
for i = 1, MAX_TRACKED_TARGETS do
local frame = unitFrames[i]
local guid = displayable[i]
if guid then
UpdateUnitFrame(frame, guid)
showFrame = true
else
frame:Hide()
end
end
-- Show/hide main frame
if mainFrame then
if showFrame and not isDisabled then
if not isFrameVisible then
mainFrame:Show()
isFrameVisible = true
end
elseif not isUnlocked then
if isFrameVisible then
mainFrame:Hide()
isFrameVisible = false
end
end
end
end
-- ============================================================================
-- Update Loop
-- ============================================================================
local lastUpdate = 0
local function OnUpdate()
local now = GetTime()
if now - lastUpdate < UPDATE_INTERVAL then return end
lastUpdate = now
UpdateDisplay()
end
-- ============================================================================
-- Extension Load
-- ============================================================================
function Extension.OnLoad()
if not CleveRoids.hasSuperwow then
CleveRoids.Print("MultiTargetTracker: SuperWoW required, extension disabled")
return
end
-- Load class spells
local success, spellCount = LoadClassSpells()
if not success then
CleveRoids.Print("MultiTargetTracker: Failed to load spells, extension disabled")
return
end
-- Create main frame
mainFrame = CreateMainFrame()
-- Restore position
RestoreFramePosition()
-- Restore hidden state
CleveRoidMacros = CleveRoidMacros or {}
if CleveRoidMacros.multiTargetHidden then
isDisabled = true
mainFrame:Hide()
isFrameVisible = false
end
-- Start update loop
mainFrame:SetScript("OnUpdate", function()
OnUpdate()
end)
CleveRoids.Print("Multi-Target Tracker loaded for " .. playerClassName .. " (" .. spellCount .. " spells). Use /cleveroid mtt for commands.")
end
-- ============================================================================
-- API Export
-- ============================================================================
CleveRoids.MultiTargetTracker = {
Show = function()
if mainFrame then
mainFrame:Show()
isFrameVisible = true
isDisabled = false
CleveRoidMacros = CleveRoidMacros or {}
CleveRoidMacros.multiTargetHidden = nil
end
end,
Hide = function()
if mainFrame then
mainFrame:Hide()
isFrameVisible = false
isDisabled = true
CleveRoidMacros = CleveRoidMacros or {}
CleveRoidMacros.multiTargetHidden = true
end
end,
Toggle = function()
if mainFrame then
if isDisabled then
CleveRoids.MultiTargetTracker.Show()
else
CleveRoids.MultiTargetTracker.Hide()
end
end
end,
IsVisible = function()
return isFrameVisible
end,
IsDisabled = function()
return isDisabled
end,
Unlock = function()
isUnlocked = true
if mainFrame then
mainFrame:Show()
isFrameVisible = true
if mainFrame.titleText then
mainFrame.titleText:SetText("Debuff Tracker (UNLOCKED)")
mainFrame.titleText:SetTextColor(0, 1, 0, 1)
end
end
CleveRoids.Print("Multi-Target Tracker: Unlocked - drag to move")
end,
Lock = function()
isUnlocked = false
if mainFrame and mainFrame.titleText then
mainFrame.titleText:SetText("Debuff Tracker")
mainFrame.titleText:SetTextColor(COLORS.titleText[1], COLORS.titleText[2], COLORS.titleText[3], 1)
end
SaveFramePosition()
CleveRoids.Print("Multi-Target Tracker: Locked")
end,
ResetPosition = function()
CleveRoidMacros = CleveRoidMacros or {}
CleveRoidMacros.multiTargetPos = nil
if mainFrame then
mainFrame:ClearAllPoints()
mainFrame:SetPoint("CENTER", UIParent, "CENTER", 200, 100)
end
CleveRoids.Print("Multi-Target Tracker: Position reset")
end,
ClearTargets = function()
discoveredGuids = {}
trackedDebuffs = {}
CleveRoids.Print("Multi-Target Tracker: Cleared all targets")
end,
}
_G["CleveRoids"] = CleveRoids