implement standalone debuff timer system

This commit is contained in:
Jrc13245
2025-10-21 22:18:10 -04:00
parent 0f3078d52b
commit 8542a05170
5 changed files with 753 additions and 251 deletions
+568 -188
View File
@@ -3,20 +3,29 @@
License: MIT License
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {} -- redundant since we're loading first but peace of mind if another file is added top of chain
local CleveRoids = _G.CleveRoids or {}
local _G = getfenv(0)
local strfind = string.find
local find = string.find -- Add this line
local gsub = string.gsub
local lower = string.lower
local floor = math.floor
local pairs = pairs
local ipairs = ipairs
local type = type
local tonumber = tonumber
local tostring = tostring
local GetTime = GetTime
-- Vanilla 1.12.1 compatibility: hooksecurefunc polyfill
if type(hooksecurefunc) ~= "function" then
function hooksecurefunc(arg1, arg2, arg3)
local tgt, fname, post
-- Variant A: hooksecurefunc("GlobalFuncName", postHook)
if type(arg1) == "string" and type(arg2) == "function" and arg3 == nil then
fname, post = arg1, arg2
local orig = _G[fname]
if type(orig) ~= "function" then return end
_G[fname] = function(...)
-- Capture arguments into a local table to avoid arg scope issues
local args = arg or {}
local n = (type(args) == "table" and args.n) or 0
local r = { orig(unpack(args, 1, n)) }
@@ -26,13 +35,11 @@ if type(hooksecurefunc) ~= "function" then
return
end
-- Variant B: hooksecurefunc(table, "MethodName", postHook)
if type(arg1) == "table" and type(arg2) == "string" and type(arg3) == "function" then
tgt, fname, post = arg1, arg2, arg3
local orig = tgt[fname]
if type(orig) ~= "function" then return end
tgt[fname] = function(...)
-- Capture arguments into a local table to avoid arg scope issues
local args = arg or {}
local n = (type(args) == "table" and args.n) or 0
local r = { orig(unpack(args, 1, n)) }
@@ -41,25 +48,20 @@ if type(hooksecurefunc) ~= "function" then
end
return
end
-- If neither signature matches, do nothing (fail-safe)
end
end
-- Lua 5.0 compatibility shims (WoW 1.12): string.match / string.gmatch
if type(string.match) ~= "function" then
function string.match(s, pattern, init)
if s == nil or pattern == nil then return nil end
local i, j, c1, c2, c3, c4, c5 = string.find(s, pattern, init)
if not i then return nil end
-- If the pattern has captures, return them (first 5 for simplicity).
if c1 ~= nil then return c1, c2, c3, c4, c5 end
-- Otherwise return the matched substring (Lua 5.1+ behavior).
return string.sub(s, i, j)
end
end
if type(string.gmatch) ~= "function" then
-- Simple gmatch built on repeated string.find
function string.gmatch(s, pattern)
local pos = 1
return function()
@@ -77,9 +79,6 @@ function CleveRoids.Seq(_, i)
return (i or 0) + 1
end
-- Trims any leading or trailing white space characters from the given string
-- str: The string to trim
-- returns: The trimmed string
function CleveRoids.Trim(str)
if not str then
return nil
@@ -95,9 +94,9 @@ do
CleveRoids.__mo = CleveRoids.__mo or { sources = {}, current = nil }
local PRIORITY = {
pfui = 3, -- unitframe hovers should win
blizz = 3, -- same level as pfui
tooltip = 1, -- lowest; don’t stomp frames
pfui = 3,
blizz = 3,
tooltip = 1,
}
local function getBest()
@@ -114,9 +113,8 @@ do
end
local function apply(unit)
-- SuperWoW path if available; otherwise internal fallback
if CleveRoids.hasSuperwow and _G.SetMouseoverUnit then
_G.SetMouseoverUnit(unit) -- nil clears
_G.SetMouseoverUnit(unit)
else
CleveRoids.mouseoverUnit = unit
end
@@ -124,7 +122,6 @@ do
end
function CleveRoids.SetMouseoverFrom(source, unit)
-- set/refresh a source’s unit
if not source then return end
CleveRoids.__mo.sources[source] = unit
local _, bestUnit = getBest()
@@ -137,7 +134,7 @@ do
function CleveRoids.ClearMouseoverFrom(source, unitIfMatch)
if not source then return end
if unitIfMatch and CleveRoids.__mo.sources[source] ~= unitIfMatch then
return -- don’t clear if it was replaced meanwhile
return
end
CleveRoids.__mo.sources[source] = nil
local _, bestUnit = getBest()
@@ -179,10 +176,6 @@ function CleveRoids.Split(s, p, trim)
until false
end
-- Splits the given string into a list of sub-strings
-- str: The string to split
-- seperatorPattern: The seperator between sub-string. May contain patterns
-- returns: A list of sub-strings
function CleveRoids.splitString(str, seperatorPattern)
local tbl = {}
if not str then
@@ -237,17 +230,14 @@ function CleveRoids.splitStringIgnoringQuotes(str, separator)
end
end
-- Add the last segment if it exists
if temp ~= "" then
temp = CleveRoids.Trim(temp)
table.insert(result, temp)
end
-- if nothing was found, return the empty string
return (next(result) and result or {""})
end
-- Prints all the given arguments into WoW's default chat frame
function CleveRoids.Print(...)
local c = "|cFF4477FFCleveR|r|cFFFFFFFFoid :: |r"
local out = ""
@@ -331,187 +321,506 @@ CleveRoids.comparators["~="] = CleveRoids.comparators.ne
_G["CleveRoids"] = CleveRoids
-- === CleveRoids.libdebuff (Vanilla 1.12.1 / Lua 5.0) =======================
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
_G.CleveRoids = CleveRoids
-- create table
CleveRoids.libdebuff = CleveRoids.libdebuff or {}
local lib = CleveRoids.libdebuff
-- storage modeled after pfUI: objects[name][level][effect] = {start,duration,caster}
lib.objects = lib.objects or {}
lib.pending = lib.pending or {} -- {unit, level, effect, duration, caster}
local lastspell
lib.guidToName = lib.guidToName or {}
-- configurable durations (extend as needed)
lib.durations = lib.durations or {
["Sunder Armor"] = 30,
["Expose Armor"] = 30,
-- ["Faerie Fire"] = 40,
-- ["Rend"] = 21,
-- WARRIOR
[772] = 9, -- Rend (Rank 1)
[6546] = 12, -- Rend (Rank 2)
[6547] = 15, -- Rend (Rank 3)
[6548] = 18, -- Rend (Rank 4)
[11572] = 21, -- Rend (Rank 5)
[11573] = 21, -- Rend (Rank 6)
[11574] = 21, -- Rend (Rank 7)
[7386] = 30, -- Sunder Armor (Rank 1)
[7405] = 30, -- Sunder Armor (Rank 2)
[8380] = 30, -- Sunder Armor (Rank 3)
[8647] = 30, -- Sunder Armor (Rank 4)
[11597] = 30, -- Sunder Armor (Rank 5)
[7372] = 15, -- Hamstring (Rank 1)
[7373] = 15, -- Hamstring (Rank 2)
[1715] = 15, -- Hamstring (Rank 3)
[6343] = 10, -- Thunder Clap (Rank 1)
[8198] = 10, -- Thunder Clap (Rank 2)
[8205] = 10, -- Thunder Clap (Rank 3)
[11580] = 10, -- Thunder Clap (Rank 4)
[11581] = 10, -- Thunder Clap (Rank 5)
[1160] = 30, -- Demoralizing Shout (Rank 1)
[6190] = 30, -- Demoralizing Shout (Rank 2)
[11554] = 30, -- Demoralizing Shout (Rank 3)
[11555] = 30, -- Demoralizing Shout (Rank 4)
[11556] = 30, -- Demoralizing Shout (Rank 5)
[12323] = 6, -- Piercing Howl
-- ROGUE
[2094] = 10, -- Blind
[21060] = 10, -- Blind (alternate?)
[6770] = 25, -- Sap (Rank 1)
[2070] = 35, -- Sap (Rank 2)
[11297] = 45, -- Sap (Rank 3)
[1776] = 4, -- Gouge (Rank 1) - base duration
[1777] = 4, -- Gouge (Rank 2)
[8629] = 4, -- Gouge (Rank 3)
[11285] = 4, -- Gouge (Rank 4)
[11286] = 4, -- Gouge (Rank 5)
[1943] = 6, -- Rupture (Rank 1) - base duration (scales with combo points)
[8639] = 6, -- Rupture (Rank 2)
[8640] = 6, -- Rupture (Rank 3)
[11273] = 6, -- Rupture (Rank 4)
[11274] = 6, -- Rupture (Rank 5)
[11275] = 6, -- Rupture (Rank 6)
[408] = 1, -- Kidney Shot (Rank 1) - base duration (scales with combo points)
[8643] = 1, -- Kidney Shot (Rank 2)
[8647] = 30, -- Expose Armor (Rank 1)
[8649] = 30, -- Expose Armor (Rank 2)
[8650] = 30, -- Expose Armor (Rank 3)
[11197] = 30, -- Expose Armor (Rank 4)
[11198] = 30, -- Expose Armor (Rank 5)
[703] = 18, -- Garrote (Rank 1)
[8631] = 18, -- Garrote (Rank 2)
[8632] = 18, -- Garrote (Rank 3)
[8633] = 18, -- Garrote (Rank 4)
[11289] = 18, -- Garrote (Rank 5)
[11290] = 18, -- Garrote (Rank 6)
[2818] = 12, -- Deadly Poison (Rank 1)
[2819] = 12, -- Deadly Poison II (Rank 2)
[11353] = 12, -- Deadly Poison III (Rank 3)
[11354] = 12, -- Deadly Poison IV (Rank 4)
[25349] = 12, -- Deadly Poison V (Rank 5)
[16511] = 15, -- Hemorrhage
[1833] = 4, -- Cheap Shot
[14902] = 4, -- Cheap Shot (alternate?)
-- Rogue poisons (not in Cursive, estimates):
[3409] = 12, -- Crippling Poison
[5760] = 16, -- Mind-numbing Poison (Rank 1)
[8694] = 16, -- Mind-numbing Poison (Rank 2)
[11399] = 16, -- Mind-numbing Poison (Rank 3)
[13218] = 15, -- Wound Poison (Rank 1)
[13222] = 15, -- Wound Poison (Rank 2)
[13223] = 15, -- Wound Poison (Rank 3)
[13224] = 15, -- Wound Poison (Rank 4)
-- HUNTER
[3043] = 20, -- Scorpid Sting (Rank 1)
[14275] = 20, -- Scorpid Sting (Rank 2)
[14276] = 20, -- Scorpid Sting (Rank 3)
[14277] = 20, -- Scorpid Sting (Rank 4)
[1978] = 15, -- Serpent Sting (Rank 1)
[13549] = 15, -- Serpent Sting (Rank 2)
[13550] = 15, -- Serpent Sting (Rank 3)
[13551] = 15, -- Serpent Sting (Rank 4)
[13552] = 15, -- Serpent Sting (Rank 5)
[13553] = 15, -- Serpent Sting (Rank 6)
[13554] = 15, -- Serpent Sting (Rank 7)
[13555] = 15, -- Serpent Sting (Rank 8)
[25295] = 15, -- Serpent Sting (Rank 9)
[3034] = 8, -- Viper Sting (Rank 1)
[14279] = 8, -- Viper Sting (Rank 2)
[14280] = 8, -- Viper Sting (Rank 3)
[2974] = 10, -- Wing Clip (Rank 1)
[14267] = 10, -- Wing Clip (Rank 2)
[14268] = 10, -- Wing Clip (Rank 3)
[5116] = 4, -- Concussive Shot
[19386] = 12, -- Wyvern Sting (Rank 1)
[24132] = 12, -- Wyvern Sting (Rank 2)
[24133] = 12, -- Wyvern Sting (Rank 3)
[19306] = 5, -- Counterattack (Rank 1)
[20909] = 5, -- Counterattack (Rank 2)
[20910] = 5, -- Counterattack (Rank 3)
[1130] = 120, -- Hunter's Mark (Rank 1)
[14323] = 120, -- Hunter's Mark (Rank 2)
[14324] = 120, -- Hunter's Mark (Rank 3)
[14325] = 120, -- Hunter's Mark (Rank 4)
-- DRUID
[339] = 12, -- Entangling Roots (Rank 1)
[1062] = 15, -- Entangling Roots (Rank 2)
[5195] = 18, -- Entangling Roots (Rank 3)
[5196] = 21, -- Entangling Roots (Rank 4)
[9852] = 24, -- Entangling Roots (Rank 5)
[9853] = 27, -- Entangling Roots (Rank 6)
[700] = 20, -- Sleep (Rank 1)
[1090] = 30, -- Sleep (Rank 2)
[2937] = 40, -- Sleep (Rank 3)
[770] = 40, -- Faerie Fire (Rank 1)
[778] = 40, -- Faerie Fire (Rank 2)
[9749] = 40, -- Faerie Fire (Rank 3)
[9907] = 40, -- Faerie Fire (Rank 4)
[16855] = 40, -- Faerie Fire (Bear) (Rank 1)
[17387] = 40, -- Faerie Fire (Bear) (Rank 2)
[17388] = 40, -- Faerie Fire (Bear) (Rank 3)
[17389] = 40, -- Faerie Fire (Bear) (Rank 4)
[16857] = 40, -- Faerie Fire (Feral) (Rank 1)
[17390] = 40, -- Faerie Fire (Feral) (Rank 2)
[17391] = 40, -- Faerie Fire (Feral) (Rank 3)
[17392] = 40, -- Faerie Fire (Feral) (Rank 4)
[2637] = 20, -- Hibernate (Rank 1)
[18657] = 30, -- Hibernate (Rank 2)
[18658] = 40, -- Hibernate (Rank 3)
[5570] = 18, -- Insect Swarm (Rank 1) - 12s with talent
[24974] = 18, -- Insect Swarm (Rank 2)
[24975] = 18, -- Insect Swarm (Rank 3)
[24976] = 18, -- Insect Swarm (Rank 4)
[24977] = 18, -- Insect Swarm (Rank 5)
[8921] = 9, -- Moonfire (Rank 1) - 12s with talent
[8924] = 18, -- Moonfire (Rank 2) - 12s with talent
[8925] = 18, -- Moonfire (Rank 3)
[8926] = 18, -- Moonfire (Rank 4)
[8927] = 18, -- Moonfire (Rank 5)
[8928] = 18, -- Moonfire (Rank 6)
[8929] = 18, -- Moonfire (Rank 7)
[9833] = 18, -- Moonfire (Rank 8)
[9834] = 18, -- Moonfire (Rank 9)
[9835] = 18, -- Moonfire (Rank 10)
[1822] = 9, -- Rake (Rank 1)
[1823] = 9, -- Rake (Rank 2)
[1824] = 9, -- Rake (Rank 3)
[9904] = 9, -- Rake (Rank 4)
[1079] = 8, -- Rip (Rank 1) - base duration (scales with combo points)
[9492] = 8, -- Rip (Rank 2)
[9493] = 8, -- Rip (Rank 3)
[9752] = 8, -- Rip (Rank 4)
[9894] = 8, -- Rip (Rank 5)
[9896] = 8, -- Rip (Rank 6)
[2908] = 15, -- Soothe Animal (Rank 1)
[8955] = 15, -- Soothe Animal (Rank 2)
[9901] = 15, -- Soothe Animal (Rank 3)
[5211] = 2, -- Bash (Rank 1)
[6798] = 3, -- Bash (Rank 2)
[8983] = 4, -- Bash (Rank 3)
[99] = 30, -- Demoralizing Roar (Rank 1)
[1735] = 30, -- Demoralizing Roar (Rank 2)
[9490] = 30, -- Demoralizing Roar (Rank 3)
[9747] = 30, -- Demoralizing Roar (Rank 4)
[9898] = 30, -- Demoralizing Roar (Rank 5)
[5209] = 6, -- Challenging Roar
[9005] = 18, -- Pounce Bleed (Rank 1)
[9823] = 18, -- Pounce Bleed (Rank 2)
[9827] = 18, -- Pounce Bleed (Rank 3)
-- WARLOCK
[172] = 12, -- Corruption (Rank 1) - 18s with talent
[6222] = 15, -- Corruption (Rank 2) - 18s with talent
[6223] = 18, -- Corruption (Rank 3) - 18s with talent
[7648] = 18, -- Corruption (Rank 4)
[11671] = 18, -- Corruption (Rank 5)
[11672] = 18, -- Corruption (Rank 6)
[25311] = 18, -- Corruption (Rank 7)
[980] = 24, -- Curse of Agony (Rank 1)
[1014] = 24, -- Curse of Agony (Rank 2)
[6217] = 24, -- Curse of Agony (Rank 3)
[11711] = 24, -- Curse of Agony (Rank 4)
[11712] = 24, -- Curse of Agony (Rank 5)
[11713] = 24, -- Curse of Agony (Rank 6)
[18265] = 30, -- Siphon Life (Rank 1)
[18879] = 30, -- Siphon Life (Rank 2)
[18880] = 30, -- Siphon Life (Rank 3)
[18881] = 30, -- Siphon Life (Rank 4)
[52550] = 8, -- Dark Harvest (Rank 1)
[52551] = 8, -- Dark Harvest (Rank 2)
[52552] = 8, -- Dark Harvest (Rank 3)
[603] = 60, -- Curse of Doom
[704] = 120, -- Curse of Recklessness (Rank 1)
[7658] = 120, -- Curse of Recklessness (Rank 2)
[7659] = 120, -- Curse of Recklessness (Rank 3)
[11717] = 120, -- Curse of Recklessness (Rank 4)
[17862] = 300, -- Curse of Shadow (Rank 1)
[17937] = 300, -- Curse of Shadow (Rank 2)
[1490] = 300, -- Curse of Elements (Rank 1)
[11721] = 300, -- Curse of Elements (Rank 2)
[11722] = 300, -- Curse of Elements (Rank 3)
[1714] = 30, -- Curse of Tongues (Rank 1)
[11719] = 30, -- Curse of Tongues (Rank 2)
[702] = 120, -- Curse of Weakness (Rank 1)
[1108] = 120, -- Curse of Weakness (Rank 2)
[6205] = 120, -- Curse of Weakness (Rank 3)
[7646] = 120, -- Curse of Weakness (Rank 4)
[11707] = 120, -- Curse of Weakness (Rank 5)
[11708] = 120, -- Curse of Weakness (Rank 6)
[18223] = 12, -- Curse of Exhaustion
[348] = 15, -- Immolate (Rank 1)
[707] = 15, -- Immolate (Rank 2)
[1094] = 15, -- Immolate (Rank 3)
[2941] = 15, -- Immolate (Rank 4)
[11665] = 15, -- Immolate (Rank 5)
[11667] = 15, -- Immolate (Rank 6)
[11668] = 15, -- Immolate (Rank 7)
[25309] = 15, -- Immolate (Rank 8)
[6789] = 3, -- Death Coil (Rank 1)
[17925] = 3, -- Death Coil (Rank 2)
[17926] = 3, -- Death Coil (Rank 3)
[710] = 20, -- Banish (Rank 1)
[18647] = 30, -- Banish (Rank 2)
[5782] = 10, -- Fear (Rank 1)
[6213] = 15, -- Fear (Rank 2)
[6215] = 20, -- Fear (Rank 3)
-- MAGE
[118] = 20, -- Polymorph (Rank 1)
[12824] = 30, -- Polymorph (Rank 2)
[12825] = 40, -- Polymorph (Rank 3)
[12826] = 50, -- Polymorph (Rank 4)
[28270] = 50, -- Polymorph: Cow
[28271] = 50, -- Polymorph: Turtle
[28272] = 50, -- Polymorph: Pig
[116] = 5, -- Frostbolt slow
[120] = 5, -- Cone of Cold slow
[6136] = 15, -- Chilled
[12484] = 15, -- Improved Blizzard slow
[12486] = 15, -- Blizzard slow
-- PRIEST
[1425] = 30, -- Shackle Undead (Rank 1)
[9486] = 40, -- Shackle Undead (Rank 2)
[10956] = 50, -- Shackle Undead (Rank 3)
[453] = 15, -- Mind Soothe (Rank 1)
[8192] = 15, -- Mind Soothe (Rank 2)
[10953] = 15, -- Mind Soothe (Rank 3)
[605] = 60, -- Mind Control (Rank 1)
[10911] = 30, -- Mind Control (Rank 2)
[10912] = 30, -- Mind Control (Rank 3)
[2944] = 24, -- Devouring Plague (Rank 1)
[19276] = 24, -- Devouring Plague (Rank 2)
[19277] = 24, -- Devouring Plague (Rank 3)
[19278] = 24, -- Devouring Plague (Rank 4)
[19279] = 24, -- Devouring Plague (Rank 5)
[19280] = 24, -- Devouring Plague (Rank 6)
[9035] = 120, -- Hex of Weakness (Rank 1)
[19281] = 120, -- Hex of Weakness (Rank 2)
[19282] = 120, -- Hex of Weakness (Rank 3)
[19283] = 120, -- Hex of Weakness (Rank 4)
[19284] = 120, -- Hex of Weakness (Rank 5)
[19285] = 120, -- Hex of Weakness (Rank 6)
[589] = 24, -- Shadow Word: Pain (Rank 1) - 18s base, 24s with talent
[594] = 24, -- Shadow Word: Pain (Rank 2)
[970] = 24, -- Shadow Word: Pain (Rank 3)
[992] = 24, -- Shadow Word: Pain (Rank 4)
[2767] = 24, -- Shadow Word: Pain (Rank 5)
[10892] = 24, -- Shadow Word: Pain (Rank 6)
[10893] = 24, -- Shadow Word: Pain (Rank 7)
[10894] = 24, -- Shadow Word: Pain (Rank 8)
[15286] = 60, -- Vampiric Embrace
[14914] = 10, -- Holy Fire (Rank 1)
[15262] = 10, -- Holy Fire (Rank 2)
[15263] = 10, -- Holy Fire (Rank 3)
[15264] = 10, -- Holy Fire (Rank 4)
[15265] = 10, -- Holy Fire (Rank 5)
[15266] = 10, -- Holy Fire (Rank 6)
[15267] = 10, -- Holy Fire (Rank 7)
[15261] = 10, -- Holy Fire (Rank 8)
-- Mind Flay
[15407] = 3, -- Mind Flay (Rank 1)
[17311] = 3, -- Mind Flay (Rank 2)
[17312] = 3, -- Mind Flay (Rank 3)
[17313] = 3, -- Mind Flay (Rank 4)
[17314] = 3, -- Mind Flay (Rank 5)
[18807] = 3, -- Mind Flay (Rank 6)
-- PALADIN
[853] = 6, -- Hammer of Justice (Rank 1)
[5588] = 6, -- Hammer of Justice (Rank 2)
[5589] = 6, -- Hammer of Justice (Rank 3)
[10308] = 6, -- Hammer of Justice (Rank 4)
[20184] = 10, -- Judgement of Justice
[20185] = 10, -- Judgement of Light (Rank 1)
[20267] = 10, -- Judgement of Light (Rank 2)
[20268] = 10, -- Judgement of Light (Rank 3)
[20271] = 10, -- Judgement of Light (Rank 4)
[20186] = 10, -- Judgement of Wisdom (Rank 1)
[20354] = 10, -- Judgement of Wisdom (Rank 2)
[20355] = 10, -- Judgement of Wisdom (Rank 3)
-- SHAMAN
[8050] = 12, -- Flame Shock (Rank 1)
[8052] = 12, -- Flame Shock (Rank 2)
[8053] = 12, -- Flame Shock (Rank 3)
[10447] = 12, -- Flame Shock (Rank 4)
[10448] = 12, -- Flame Shock (Rank 5)
[29228] = 12, -- Flame Shock (Rank 6)
-- Frost Shock
[8056] = 8, -- Frost Shock (Rank 1)
[8058] = 8, -- Frost Shock (Rank 2)
[10472] = 8, -- Frost Shock (Rank 3)
[10473] = 8, -- Frost Shock (Rank 4)
}
-- Utility: effect duration lookup (can be expanded with rank/talent logic later)
function lib:GetDuration(effect, rank)
return self.durations[effect] or 0
lib.learnCastTimers = lib.learnCastTimers or {}
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
function lib:GetDuration(spellID, casterGUID)
if casterGUID and CleveRoids_LearnedDurations[spellID] then
local learned = CleveRoids_LearnedDurations[spellID][casterGUID]
if learned and learned > 0 then
return learned
end
end
return self.durations[spellID] or 0
end
-- Basic add/refresh of a debuff record
function lib:AddEffect(unitName, unitLevel, effect, duration, caster)
if not unitName or not effect then return end
unitLevel = unitLevel or 0
duration = duration or lib:GetDuration(effect)
function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster)
if not guid or not spellID then return end
duration = duration or lib:GetDuration(spellID, caster)
if duration <= 0 then return end
lib.objects[unitName] = lib.objects[unitName] or {}
lib.objects[unitName][unitLevel] = lib.objects[unitName][unitLevel] or {}
lib.objects[unitName][unitLevel][effect] = lib.objects[unitName][unitLevel][effect] or {}
lib.objects[guid] = lib.objects[guid] or {}
lib.guidToName[guid] = unitName
local rec = lib.objects[unitName][unitLevel][effect]
lastspell = rec
rec.effect = effect
rec.start_old = rec.start
local rec = lib.objects[guid][spellID] or {}
rec.spellID = spellID
rec.start = GetTime()
rec.duration = duration
rec.stacks = stacks or 0
rec.caster = caster
-- no UI refresh hook needed; your UI reads via conditionals
lib.objects[guid][spellID] = rec
end
function lib:RevertLastAction()
if lastspell and lastspell.start_old then
lastspell.start = lastspell.start_old
lastspell.start_old = nil
end
function lib:GetDuration(spellID)
return self.durations[spellID] or 0
end
function lib:AddPending(unitName, unitLevel, effect, duration, caster)
if not unitName or not effect then return end
if (duration or 0) <= 0 then duration = lib:GetDuration(effect) end
function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster)
if not guid or not spellID then return end
duration = duration or lib:GetDuration(spellID)
if duration <= 0 then return end
lib.pending[1] = unitName
lib.pending[2] = unitLevel or 0
lib.pending[3] = effect
lib.pending[4] = duration
lib.pending[5] = caster
lib.objects[guid] = lib.objects[guid] or {}
lib.guidToName[guid] = unitName
local rec = lib.objects[guid][spellID] or {}
rec.spellID = spellID
rec.start = GetTime()
rec.duration = duration
rec.stacks = stacks or 0
rec.caster = caster
lib.objects[guid][spellID] = rec
end
function lib:RemovePending()
lib.pending[1] = nil
lib.pending[2] = nil
lib.pending[3] = nil
lib.pending[4] = nil
lib.pending[5] = nil
end
function lib:PersistPending(effect)
-- if effect matches the pending one (or nil meaning “persist whatever is pending”)
if lib.pending[3] and (effect == nil or effect == lib.pending[3]) then
lib:AddEffect(lib.pending[1], lib.pending[2], lib.pending[3], lib.pending[4], lib.pending[5])
end
lib:RemovePending()
end
local function GenerateUniqueTooltipName()
local base = "CleveRoidsLibDebuffTT"
local suffix = string.format("%d%d", GetTime() * 1000, math.random(1000, 9999))
return base .. suffix
end
-- Read API similar to pfUI: returns effect, rank(nil), texture, stacks, dtype, duration, timeleft, caster
function lib:UnitDebuff(unit, id)
local unitName = UnitName(unit)
local unitLevel = UnitLevel(unit) or 0
local texture, stacks, dtype = UnitDebuff(unit, id)
local _, guid = UnitExists(unit)
if not guid then return nil end
local texture, stacks, dtype, spellID = UnitDebuff(unit, id)
if not texture or not spellID then return nil end
local name = SpellInfo(spellID)
local duration, timeleft, caster = nil, -1, nil
local effect
if texture then
-- Try to resolve a stable effect name using SuperWoW SpellInfo when available
local _, _, spellID
if CleveRoids.hasSuperwow then
-- Use tooltip scan alternative via UnitDebuff’s texture->name mapping if you have it,
-- or rely on GameTooltip scanning if desired (omitted for performance).
-- The most stable way here: read the name off the debuff button tooltip:
-- (Left out to keep it lightweight; we fetch name by scanning by index below)
end
local rec = lib.objects[guid] and lib.objects[guid][spellID]
-- Prefer reading name by setting the tooltip on this debuff index (cheap enough on access)
-- We avoid a full scanner dependency to keep this self-contained:
-- Build a temporary tooltip to fetch line 1 as the effect name.
if not lib._tt then
lib._tt = CreateFrame("GameTooltip", nil, UIParent, "GameTooltipTemplate")
lib._tt:SetOwner(UIParent, "ANCHOR_NONE")
lib._L1 = lib._tt:CreateFontString(nil, nil, "GameTooltipText")
lib._R1 = lib._tt:CreateFontString(nil, nil, "GameTooltipText")
lib._tt:AddFontStrings(lib._L1, lib._R1)
if rec and rec.duration and rec.start then
local remaining = rec.duration + rec.start - GetTime()
if remaining > 0 then
duration = rec.duration
timeleft = remaining
caster = rec.caster
stacks = rec.stacks or stacks
else
lib.objects[guid][spellID] = nil
end
local tt = lib._tt
tt:ClearLines()
tt:SetUnitDebuff(unit, id)
effect = lib._L1 and lib._L1:GetText() or ""
end
-- read level-scoped storage, try exact level then 0 fallback (pfUI pattern)
local data = lib.objects[unitName] and (lib.objects[unitName][unitLevel] or lib.objects[unitName][0])
local rec = data and effect and data[effect]
if rec and rec.duration and rec.start and (rec.duration + rec.start > GetTime()) then
duration = rec.duration
timeleft = rec.duration + rec.start - GetTime()
caster = rec.caster
elseif rec then
-- cleanup expired
data[effect] = nil
end
return effect, nil, texture, stacks, dtype, duration, timeleft, caster
return name, nil, texture, stacks, dtype, duration, timeleft, caster
end
-- Seed timers by scanning the current target/player auras (to avoid relying only on casts)
local function SeedUnit(unit)
if not UnitExists(unit) then return end
local _, guid = UnitExists(unit)
if not guid then return end
local unitName = UnitName(unit)
if not unitName or unitName == "" then return end
local unitLevel = UnitLevel(unit) or 0
for i=1, 16 do
local tex = (UnitDebuff(unit, i))
local tex, stacks, dtype, spellID = UnitDebuff(unit, i)
if not tex then break end
-- resolve effect name via tooltip line 1
if not lib._tt then
lib._tt = CreateFrame("GameTooltip", "CleveRoidsLibDebuffTT", UIParent, "GameTooltipTemplate")
lib._tt:SetOwner(UIParent, "ANCHOR_NONE")
end
local tt = lib._tt
tt:ClearLines()
tt:SetUnitDebuff(unit, i)
local effect = (getglobal("CleveRoidsLibDebuffTTTextLeft1") and getglobal("CleveRoidsLibDebuffTTTextLeft1"):GetText()) or ""
if effect ~= "" and lib.durations[effect] then
-- don’t overwrite a valid running timer
local data = lib.objects[unitName] and (lib.objects[unitName][unitLevel] or lib.objects[unitName][0])
if not (data and data[effect]) then
lib:AddEffect(unitName, unitLevel, effect, lib:GetDuration(effect))
if spellID and lib.durations[spellID] then
if not (lib.objects[guid] and lib.objects[guid][spellID]) then
lib:AddEffect(guid, unitName, spellID, lib:GetDuration(spellID), stacks)
end
end
end
end
-- Events & hooks (modeled after pfUI, simplified to avoid extra libs)
local ev = CreateFrame("Frame", "CleveRoidsLibDebuffFrame", UIParent)
ev:RegisterEvent("PLAYER_TARGET_CHANGED")
ev:RegisterEvent("UNIT_AURA")
ev:RegisterEvent("SPELLCAST_STOP")
ev:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER")
ev:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
-- (Optional) add more combat log events if you later want to detect “aura applied” via text
if CleveRoids.hasSuperwow then
ev:RegisterEvent("UNIT_CASTEVENT")
end
ev:SetScript("OnEvent", function()
if event == "PLAYER_TARGET_CHANGED" then
@@ -520,41 +829,112 @@ ev:SetScript("OnEvent", function()
elseif event == "UNIT_AURA" and arg1 == "target" then
SeedUnit("target")
elseif event == "SPELLCAST_STOP" then
lib:PersistPending() -- accept any pending on cast stop
elseif event == "UNIT_CASTEVENT" then
local casterGUID = arg1
local targetGUID = arg2
local eventType = arg3
local spellID = arg4
elseif event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" or event == "CHAT_MSG_SPELL_SELF_DAMAGE" then
-- best-effort pending cleanup/revert patterns can be added here if you parse failure strings
-- for now, we just drop the pending on generic failure patterns being detected elsewhere
if eventType == "CAST" and spellID then
local _, playerGUID = UnitExists("player")
if casterGUID == playerGUID and targetGUID then
local duration = lib:GetDuration(spellID, casterGUID)
if duration > 0 then
local targetName = lib.guidToName[targetGUID]
if not targetName then
local _, currentTargetGUID = UnitExists("target")
if currentTargetGUID == targetGUID then
targetName = UnitName("target")
lib.guidToName[targetGUID] = targetName
else
targetName = "Unknown"
end
end
lib:AddEffect(targetGUID, targetName, spellID, duration, 0, "player")
else
lib.learnCastTimers[targetGUID] = lib.learnCastTimers[targetGUID] or {}
lib.learnCastTimers[targetGUID][spellID] = {
start = GetTime(),
caster = casterGUID
}
lib.objects[targetGUID] = lib.objects[targetGUID] or {}
lib.objects[targetGUID][spellID] = {
spellID = spellID,
start = GetTime(),
duration = 999, -- Placeholder
stacks = 0,
caster = casterGUID
}
end
end
end
end
end)
-- Hooks to capture “pending” when you cast
hooksecurefunc("CastSpell", function(id, bookType)
-- try to resolve spell name via SuperWoW or vanilla
local name, rank
if type(SpellInfo) == "function" then
name, rank = SpellInfo(id)
elseif type(GetSpellName) == "function" then
name, rank = GetSpellName(id, bookType)
end
if name then
local dur = lib:GetDuration(name)
lib:AddPending(UnitName("target"), UnitLevel("target"), name, dur, "player")
local evLearn = CreateFrame("Frame", "CleveRoidsLibDebuffLearnFrame", UIParent)
evLearn:RegisterEvent("RAW_COMBATLOG")
evLearn:SetScript("OnEvent", function()
if event == "RAW_COMBATLOG" then
local raw = arg2
if not raw or not find(raw, "fades from") then return end
local _, _, spellName = find(raw, "^(.-) fades from ")
local _, _, targetGUID = find(raw, "from (.-).$")
if lower(targetGUID or "") == "you" then
_, targetGUID = UnitExists("player")
end
targetGUID = gsub(targetGUID or "", "^0x", "")
if not spellName or targetGUID == "" then return end
if not lib.objects[targetGUID] then return end
local timestamp = GetTime()
for spellID in pairs(lib.objects[targetGUID]) do
local name = SpellInfo(spellID)
if name then
name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "")
if name == spellName then
if lib.learnCastTimers[targetGUID] and
lib.learnCastTimers[targetGUID][spellID] then
local castTime = lib.learnCastTimers[targetGUID][spellID].start
local casterGUID = lib.learnCastTimers[targetGUID][spellID].caster
local actualDuration = timestamp - castTime
CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {}
CleveRoids_LearnedDurations[spellID][casterGUID] = floor(actualDuration + 0.5)
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
"|cff4b7dccCleveRoids:|r Learned " .. spellName ..
" (ID:" .. spellID .. ") = " .. floor(actualDuration + 0.5) .. "s"
)
end
lib.learnCastTimers[targetGUID][spellID] = nil
if not next(lib.learnCastTimers[targetGUID]) then
lib.learnCastTimers[targetGUID] = nil
end
end
if lib.objects[targetGUID][spellID].start +
lib.objects[targetGUID][spellID].duration <= timestamp then
lib.objects[targetGUID][spellID] = nil
end
if not next(lib.objects[targetGUID]) then
lib.objects[targetGUID] = nil
end
break
end
end
end
end
end)
hooksecurefunc("CastSpellByName", function(spell)
local name = spell
-- accept “Name(Rank X)” format; trim rank for table match
local base = string.match(spell, "^(.-)%s*%(") or spell
local dur = lib:GetDuration(base)
lib:AddPending(UnitName("target"), UnitLevel("target"), base, dur, "player")
end)
-- Simple action use hook (optional; safe no-op if Action API differs)
hooksecurefunc("UseAction", function(slot)
if GetActionText(slot) or not IsCurrentAction(slot) then return end
-- we can’t reliably get the effect name without a tooltip scanner; skip to keep it lightweight
end)
-- === end CleveRoids.libdebuff ==============================================