Files
SuperCleveRoidMacros/Utility.lua
T

2392 lines
87 KiB
Lua

--[[
Author: Dennis Werner Garske (DWG) / brian / Mewtiny
License: MIT License
]]
local _G = _G or getfenv(0)
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
if type(hooksecurefunc) ~= "function" then
function hooksecurefunc(arg1, arg2, arg3)
local tgt, fname, post
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(...)
local args = arg or {}
local n = (type(args) == "table" and args.n) or 0
local r = { orig(unpack(args, 1, n)) }
post(unpack(args, 1, n))
return unpack(r, 1, table.getn(r))
end
return
end
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(...)
local args = arg or {}
local n = (type(args) == "table" and args.n) or 0
local r = { orig(unpack(args, 1, n)) }
post(unpack(args, 1, n))
return unpack(r, 1, table.getn(r))
end
return
end
end
end
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 c1 ~= nil then return c1, c2, c3, c4, c5 end
return string.sub(s, i, j)
end
end
if type(string.gmatch) ~= "function" then
function string.gmatch(s, pattern)
local pos = 1
return function()
if s == nil or pattern == nil then return nil end
local i, j, c1, c2, c3, c4, c5 = string.find(s, pattern, pos)
if not i then return nil end
pos = j + 1
if c1 ~= nil then return c1, c2, c3, c4, c5 end
return string.sub(s, i, j)
end
end
end
function CleveRoids.Seq(_, i)
return (i or 0) + 1
end
function CleveRoids.Trim(str)
if not str then
return nil
end
return string.gsub(str,"^%s*(.-)%s*$", "%1")
end
do
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
_G.CleveRoids = CleveRoids
CleveRoids.__mo = CleveRoids.__mo or { sources = {}, current = nil }
local PRIORITY = {
pfui = 3,
blizz = 3,
tooltip = 1,
}
local function getBest()
local bestSource, bestUnit, bestP = nil, nil, -1
for source, unit in CleveRoids.__mo.sources do
if unit and unit ~= "" then
local p = PRIORITY[source] or 0
if p > bestP then
bestP, bestSource, bestUnit = p, source, unit
end
end
end
return bestSource, bestUnit
end
local function apply(unit)
if CleveRoids.hasSuperwow and _G.SetMouseoverUnit then
_G.SetMouseoverUnit(unit)
else
CleveRoids.mouseoverUnit = unit
end
if CleveRoids.QueueActionUpdate then CleveRoids.QueueActionUpdate() end
end
function CleveRoids.SetMouseoverFrom(source, unit)
if not source then return end
CleveRoids.__mo.sources[source] = unit
local _, bestUnit = getBest()
if bestUnit ~= CleveRoids.__mo.current then
CleveRoids.__mo.current = bestUnit
apply(bestUnit)
end
end
function CleveRoids.ClearMouseoverFrom(source, unitIfMatch)
if not source then return end
if unitIfMatch and CleveRoids.__mo.sources[source] ~= unitIfMatch then
return
end
CleveRoids.__mo.sources[source] = nil
local _, bestUnit = getBest()
if bestUnit ~= CleveRoids.__mo.current then
CleveRoids.__mo.current = bestUnit
apply(bestUnit)
end
end
end
-- TODO: Get rid of one Split function. CleveRoids.splitString is ~10% slower
function CleveRoids.Split(s, p, trim)
local r, o = {}, 1
if not p or p == "" then
if trim then
s = CleveRoids.Trim(s)
end
for i = 1, string.len(s) do
table.insert(r, string.sub(s, i, 1))
end
return r
end
repeat
local b, e = string.find(s, p, o)
if b == nil then
local sub = string.sub(s, o)
table.insert(r, trim and CleveRoids.Trim(sub) or sub)
return r
end
if b > 1 then
local sub = string.sub(s, o, b - 1)
table.insert(r, trim and CleveRoids.Trim(sub) or sub)
else
table.insert(r, "")
end
o = e + 1
until false
end
function CleveRoids.splitString(str, seperatorPattern)
local tbl = {}
if not str then
return tbl
end
local pattern = "(.-)" .. seperatorPattern
local lastEnd = 1
local s, e, cap = string.find(str, pattern, 1)
while s do
if s ~= 1 or cap ~= "" then
table.insert(tbl,cap)
end
lastEnd = e + 1
s, e, cap = string.find(str, pattern, lastEnd)
end
if lastEnd <= string.len(str) then
cap = string.sub(str, lastEnd)
table.insert(tbl, cap)
end
return tbl
end
function CleveRoids.splitStringIgnoringQuotes(str, separator)
local result = {}
local temp = ""
local insideQuotes = false
local separators = {}
if type(separator) == "table" then
for _, s in separator do
separators[s] = s
end
else
separators[separator or ";"] = separator or ";"
end
for i = 1, string.len(str) do
local char = string.sub(str, i, i)
if char == "\"" then
insideQuotes = not insideQuotes
temp = temp .. char
elseif char == separators[char] and not insideQuotes then
temp = CleveRoids.Trim(temp)
if temp ~= "" then table.insert(result, temp) end
temp = ""
else
temp = temp .. char
end
end
if temp ~= "" then
temp = CleveRoids.Trim(temp)
table.insert(result, temp)
end
return (next(result) and result or {""})
end
function CleveRoids.Print(...)
local c = "|cFF4477FFCleveR|r|cFFFFFFFFoid :: |r"
local out = ""
for i=1, arg.n, 1 do
out = out..tostring(arg[i]).." "
end
if WowLuaFrameOutput then
WowLuaFrameOutput:AddMessage(out)
else
if not DEFAULT_CHAT_FRAME:IsVisible() then
FCF_SelectDockFrame(DEFAULT_CHAT_FRAME)
end
DEFAULT_CHAT_FRAME:AddMessage(c..out)
end
end
function CleveRoids.PrintI(msg, depth)
depth = depth or 0
msg = string.rep(" ", depth) .. tostring(msg)
CleveRoids.Print(msg)
end
function CleveRoids.PrintT(t, depth)
depth = depth or 0
local cs = "|cffc8c864"
local ce = "|r"
if t == nil or type(t) ~= "table" then
CleveRoids.PrintI(t, depth)
else
for k, v in pairs(t) do
if type(v) == "table" then
CleveRoids.PrintI(cs..tostring(k)..":"..ce.." <TABLE>", depth)
CleveRoids.PrintT(v, depth + 1)
else
CleveRoids.PrintI(cs..tostring(k)..ce..": "..tostring(v), depth)
end
end
end
end
CleveRoids.kmods = {
ctrl = IsControlKeyDown,
alt = IsAltKeyDown,
shift = IsShiftKeyDown,
mod = function() return (IsControlKeyDown() or IsAltKeyDown() or IsShiftKeyDown()) end,
nomod = function() return (not IsControlKeyDown() and not IsAltKeyDown() and not IsShiftKeyDown()) end,
}
CleveRoids.operators = {
["<"] = "lt",
["lt"] = "<",
[">"] = "gt",
["gt"] = ">",
["="] = "eq",
["eq"] = "=",
["<="] = "lte",
["lte"] = "<=",
[">="] = "gte",
["gte"] = ">=",
["~="] = "ne",
["ne"] = "~=",
}
CleveRoids.comparators = {
lt = function(a, b) return (a < b) end,
gt = function(a, b) return (a > b) end,
eq = function(a, b) return (a == b) end,
lte = function(a, b) return (a <= b) end,
gte = function(a, b) return (a >= b) end,
ne = function(a, b) return (a ~= b) end,
}
CleveRoids.comparators["<"] = CleveRoids.comparators.lt
CleveRoids.comparators[">"] = CleveRoids.comparators.gt
CleveRoids.comparators["="] = CleveRoids.comparators.eq
CleveRoids.comparators["<="] = CleveRoids.comparators.lte
CleveRoids.comparators[">="] = CleveRoids.comparators.gte
CleveRoids.comparators["~="] = CleveRoids.comparators.ne
_G["CleveRoids"] = CleveRoids
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
_G.CleveRoids = CleveRoids
CleveRoids.libdebuff = CleveRoids.libdebuff or {}
local lib = CleveRoids.libdebuff
lib.objects = lib.objects or {}
lib.guidToName = lib.guidToName or {}
-- PERSONAL DEBUFFS: Each player can have their own instance of these debuffs on the same target
-- These are DoTs, poisons, stings, and most CC effects
lib.personalDebuffs = lib.personalDebuffs or {
-- 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)
[7372] = 15, -- Hamstring (Rank 1)
[7373] = 15, -- Hamstring (Rank 2)
[1715] = 15, -- Hamstring (Rank 3)
[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) - Scales with talent
[1777] = 4, -- Gouge (Rank 2)
[8629] = 4, -- Gouge (Rank 3)
[11285] = 4, -- Gouge (Rank 4)
[11286] = 4, -- Gouge (Rank 5)
-- NOTE: Rupture removed - handled by ComboPointTracker (base 8s + 2s per CP)
-- NOTE: Kidney Shot removed - handled by ComboPointTracker (base 1s/2s + 1s per CP)
[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)
[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)
[8924] = 18, -- Moonfire (Rank 2)
[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)
-- NOTE: Rip removed - handled by ComboPointTracker (base 10s + 2s per CP)
[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)
[9005] = 18, -- Pounce Bleed (Rank 1)
[9823] = 18, -- Pounce Bleed (Rank 2)
[9827] = 18, -- Pounce Bleed (Rank 3)
-- WARLOCK
[172] = 12, -- Corruption (Rank 1)
[6222] = 15, -- Corruption (Rank 2)
[6223] = 18, -- Corruption (Rank 3)
[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] = 18, -- Shadow Word: Pain (Rank 1)
[594] = 18, -- Shadow Word: Pain (Rank 2)
[970] = 18, -- Shadow Word: Pain (Rank 3)
[992] = 18, -- Shadow Word: Pain (Rank 4)
[2767] = 18, -- Shadow Word: Pain (Rank 5)
[10892] = 18, -- Shadow Word: Pain (Rank 6)
[10893] = 18, -- Shadow Word: Pain (Rank 7)
[10894] = 18, -- 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)
[21183] = 10, -- Judgement of the Crusader (Rank 1)
[20183] = 10, -- Judgement of the Crusader (Rank 2)
[20300] = 10, -- Judgement of the Crusader (Rank 3)
[20301] = 10, -- Judgement of the Crusader (Rank 4)
[20302] = 10, -- Judgement of the Crusader (Rank 5)
[20303] = 10, -- Judgement of the Crusader (Rank 6)
-- 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)
}
-- SHARED DEBUFFS: Only one instance exists on a target, shared/refreshed by all players
-- These are armor reductions, attack power reductions, and marks
lib.sharedDebuffs = lib.sharedDebuffs or {
-- WARRIOR
[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)
[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)
-- ROGUE
[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)
-- HUNTER
[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
[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)
[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
}
-- Combined table for backwards compatibility (will be deprecated)
lib.durations = lib.durations or {}
for k, v in pairs(lib.personalDebuffs) do
lib.durations[k] = v
end
for k, v in pairs(lib.sharedDebuffs) do
lib.durations[k] = v
end
-- Helper function to check if a debuff is personal (returns true) or shared (returns false)
function lib:IsPersonalDebuff(spellID)
if lib.personalDebuffs[spellID] then
return true
elseif lib.sharedDebuffs[spellID] then
return false
end
-- Unknown debuffs are treated as personal by default (safer for multi-player scenarios)
return true
end
lib.learnCastTimers = lib.learnCastTimers or {}
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
function lib:GetDuration(spellID, casterGUID, comboPoints)
-- Check combo-specific learned durations first if this is a combo spell
if comboPoints and CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then
local comboDuration = CleveRoids_ComboDurations[spellID][comboPoints]
if comboDuration and comboDuration > 0 then
if CleveRoids.debug then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffccccff[DEBUG GetDuration]|r %s (ID:%d) CP:%d -> %ds (from learned combo)",
spellName, spellID, comboPoints, comboDuration)
)
end
return comboDuration
end
end
-- Check caster-specific learned durations
if casterGUID and CleveRoids_LearnedDurations[spellID] then
local learned = CleveRoids_LearnedDurations[spellID][casterGUID]
if learned and learned > 0 then
if CleveRoids.debug then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffccccff[DEBUG GetDuration]|r %s (ID:%d) -> %ds (from learned caster)",
spellName, spellID, learned)
)
end
return learned
end
end
-- Fall back to static database
local staticDur = self.durations[spellID] or 0
if CleveRoids.debug and staticDur > 0 then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffccccff[DEBUG GetDuration]|r %s (ID:%d) -> %ds (from static DB)",
spellName, spellID, staticDur)
)
end
return staticDur
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, caster)
if duration <= 0 then return end
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
-- DEBUG: Show what we stored
if CleveRoids.debug then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffff[DEBUG AddEffect]|r %s (ID:%d) stored duration:%ds on %s",
spellName, spellID, duration, unitName or "Unknown")
)
end
end
function lib:UnitDebuff(unit, id, filterCaster)
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
texture, stacks, spellID = UnitBuff(unit, id)
-- Only accept buffs that are known debuffs (either static or learned durations, including combo durations)
if texture and spellID and lib:GetDuration(spellID) <= 0 then
return nil
end
end
if not texture or not spellID then return nil end
local name = SpellInfo(spellID)
local duration, timeleft, caster = nil, -1, nil
local rec = lib.objects[guid] and lib.objects[guid][spellID]
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
-- Filter by caster if requested
if filterCaster and caster ~= filterCaster then
return nil
end
else
lib.objects[guid][spellID] = nil
end
end
return name, nil, texture, stacks, dtype, duration, timeleft, caster
end
-- Query buff data with duration and caster tracking (buff slots only)
function lib:UnitBuff(unit, id, filterCaster)
local _, guid = UnitExists(unit)
if not guid then return nil end
-- Only check buff slots
local texture, stacks, spellID = UnitBuff(unit, id)
if not texture or not spellID then return nil end
local name = SpellInfo(spellID)
local duration, timeleft, caster = nil, -1, nil
local rec = lib.objects[guid] and lib.objects[guid][spellID]
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
-- Filter by caster if requested
if filterCaster and caster ~= filterCaster then
return nil
end
else
lib.objects[guid][spellID] = nil
end
end
return name, nil, texture, stacks, nil, duration, timeleft, caster
end
-- Find a player-cast debuff by spell ID (searches all slots including buff slots)
function lib:FindPlayerDebuff(unit, spellID)
local _, guid = UnitExists(unit)
if not guid then return nil end
-- Check if we're tracking this spell for this unit
local rec = lib.objects[guid] and lib.objects[guid][spellID]
if not rec then return nil end
-- Only return if it was cast by player
if rec.caster ~= "player" then return nil end
-- Check if it's still active
local remaining = rec.duration + rec.start - GetTime()
if remaining <= 0 then
lib.objects[guid][spellID] = nil
return nil
end
-- Find the texture by searching debuff and buff slots
local texture, stacks = nil, rec.stacks
-- Search debuff slots first
for i = 1, 16 do
local tex, st, dtype, sid = UnitDebuff(unit, i)
if not tex then break end
if sid == spellID then
texture = tex
stacks = st or stacks
break
end
end
-- If not found in debuffs, search buff slots
if not texture then
for i = 1, 32 do
local tex, st, sid = UnitBuff(unit, i)
if not tex then break end
if sid == spellID then
texture = tex
stacks = st or stacks
break
end
end
end
if not texture then return nil end
local name = SpellInfo(spellID)
return name, nil, texture, stacks, nil, rec.duration, remaining, rec.caster
end
-- Find a player-cast buff by spell ID (searches buff slots only)
function lib:FindPlayerBuff(unit, spellID)
local _, guid = UnitExists(unit)
if not guid then return nil end
-- Check if we're tracking this spell for this unit
local rec = lib.objects[guid] and lib.objects[guid][spellID]
if not rec then return nil end
-- Only return if it was cast by player
if rec.caster ~= "player" then return nil end
-- Check if it's still active
local remaining = rec.duration + rec.start - GetTime()
if remaining <= 0 then
lib.objects[guid][spellID] = nil
return nil
end
-- Find the texture by searching buff slots only
local texture, stacks = nil, rec.stacks
for i = 1, 32 do
local tex, st, sid = UnitBuff(unit, i)
if not tex then break end
if sid == spellID then
texture = tex
stacks = st or stacks
break
end
end
if not texture then return nil end
local name = SpellInfo(spellID)
return name, nil, texture, stacks, nil, rec.duration, remaining, rec.caster
end
local function SeedUnit(unit)
local _, guid = UnitExists(unit)
if not guid then return end
local unitName = UnitName(unit)
for i=1, 16 do
local tex, stacks, dtype, spellID = UnitDebuff(unit, i)
if not tex then break end
if spellID then
local duration = lib:GetDuration(spellID)
if duration > 0 then
if not (lib.objects[guid] and lib.objects[guid][spellID]) then
-- For combo spells, try to use the highest learned duration as a fallback
if CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID) and
CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then
-- Use the longest learned duration (assume 5 CP)
for cp = 5, 1, -1 do
if CleveRoids_ComboDurations[spellID][cp] then
duration = CleveRoids_ComboDurations[spellID][cp]
if CleveRoids.debug then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaff00[DEBUG SeedUnit Debuff]|r %s (ID:%d) using learned %dCP duration:%ds",
spellName, spellID, cp, duration)
)
end
break
end
end
end
duration = duration or lib:GetDuration(spellID)
lib:AddEffect(guid, unitName, spellID, duration, stacks)
end
end
end
end
for i=1, 32 do
local tex, stacks, spellID = UnitBuff(unit, i)
if not tex then break end
if spellID then
local duration = lib:GetDuration(spellID)
if duration > 0 then
if not (lib.objects[guid] and lib.objects[guid][spellID]) then
-- For combo spells, try to use the highest learned duration as a fallback
if CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID) and
CleveRoids_ComboDurations and CleveRoids_ComboDurations[spellID] then
-- Use the longest learned duration (assume 5 CP)
for cp = 5, 1, -1 do
if CleveRoids_ComboDurations[spellID][cp] then
duration = CleveRoids_ComboDurations[spellID][cp]
if CleveRoids.debug then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaff00[DEBUG SeedUnit Buff]|r %s (ID:%d) using learned %dCP duration:%ds",
spellName, spellID, cp, duration)
)
end
break
end
end
end
duration = duration or lib:GetDuration(spellID)
lib:AddEffect(guid, unitName, spellID, duration, stacks)
end
end
end
end
end
local ev = CreateFrame("Frame", "CleveRoidsLibDebuffFrame", UIParent)
ev:RegisterEvent("PLAYER_TARGET_CHANGED")
ev:RegisterEvent("UNIT_AURA")
if CleveRoids.hasSuperwow then
ev:RegisterEvent("UNIT_CASTEVENT")
end
ev:SetScript("OnEvent", function()
if event == "PLAYER_TARGET_CHANGED" then
SeedUnit("target")
elseif event == "UNIT_AURA" and arg1 == "target" then
SeedUnit("target")
elseif event == "UNIT_CASTEVENT" then
local casterGUID = arg1
local targetGUID = arg2
local eventType = arg3
local spellID = arg4
-- Capture combo points when cast STARTS (before they're consumed)
if (eventType == "START" or eventType == "CHANNEL") and spellID then
local _, playerGUID = UnitExists("player")
if casterGUID == playerGUID and targetGUID then
-- If this is a combo scaling spell OR Ferocious Bite, capture combo points NOW (before consumption)
local isComboSpell = CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID)
local isFerociousBite = CleveRoids.FerociousBiteSpellIDs and CleveRoids.FerociousBiteSpellIDs[spellID]
if isComboSpell or isFerociousBite then
local currentCP = CleveRoids.GetComboPoints()
if currentCP and currentCP > 0 then
CleveRoids.lastComboPoints = currentCP
if CleveRoids.debug then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaaaff[UNIT_CASTEVENT START]|r Captured %d CP before casting %s (ID:%d)",
currentCP, spellName, spellID)
)
end
end
end
end
end
if eventType == "CAST" and spellID then
local _, playerGUID = UnitExists("player")
if casterGUID == playerGUID and targetGUID then
-- DRUID CARNAGE TALENT: Check if this is Ferocious Bite with 5 CP
-- If so, refresh the last Rip and Rake durations (only if debuffs are active)
if CleveRoids.FerociousBiteSpellIDs and CleveRoids.FerociousBiteSpellIDs[spellID] then
-- Get combo points used (capture before consumption)
local biteComboPoints = CleveRoids.lastComboPoints or 0
-- Check if player has Carnage talent at rank 2
-- Carnage: Tab 2 (Feral Combat), Talent 17
local carnageRank = 0
local _, _, _, _, rank = GetTalentInfo(2, 17)
carnageRank = tonumber(rank) or 0
-- Carnage 2/2+: Ferocious Bite at 5 CP refreshes Rip and Rake back to their original duration
if carnageRank >= 2 and biteComboPoints == 5 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
-- Only refresh debuffs if they're currently active on the target
if lib.objects[targetGUID] then
-- Try to refresh Rip
if CleveRoids.lastRipCast and CleveRoids.lastRipCast.duration and
CleveRoids.lastRipCast.targetGUID == targetGUID then
-- Find which Rip rank is currently active
for ripSpellID, _ in pairs(CleveRoids.RipSpellIDs) do
if lib.objects[targetGUID][ripSpellID] then
-- Found an active Rip, refresh it with the saved duration
local ripDuration = CleveRoids.lastRipCast.duration
local ripComboPoints = CleveRoids.lastRipCast.comboPoints or 5
-- Get spell name for pfUI
local ripSpellName = SpellInfo(ripSpellID)
local baseName = ripSpellName and string.gsub(ripSpellName, "%s*%(Rank %d+%)", "") or "Rip"
-- Update tracking tables for pfUI hooks
if CleveRoids.ComboPointTracking then
CleveRoids.ComboPointTracking[baseName] = {
combo_points = ripComboPoints,
duration = ripDuration,
cast_time = GetTime(),
target = targetName,
confirmed = true
}
end
-- Store duration override for pfUI hooks
if not CleveRoids.carnageDurationOverrides then
CleveRoids.carnageDurationOverrides = {}
end
CleveRoids.carnageDurationOverrides[ripSpellID] = {
duration = ripDuration,
timestamp = GetTime(),
targetGUID = targetGUID
}
-- Update pfUI's debuff timer directly
if pfUI and pfUI.api and pfUI.api.libdebuff then
local pflib = pfUI.api.libdebuff
if pflib.objects and pflib.objects[targetName] then
local updated = false
local currentLevel = UnitLevel("target") or 0
-- Update at all levels where the debuff exists
for level, effects in pairs(pflib.objects[targetName]) do
if type(effects) == "table" and effects[baseName] then
local entry = effects[baseName]
entry.duration = ripDuration
entry.start = GetTime()
entry.caster = "player"
if entry.tick then entry.tick = GetTime() end
updated = true
end
end
-- Ensure entry exists at current UnitLevel
if updated then
if not pflib.objects[targetName][currentLevel] then
pflib.objects[targetName][currentLevel] = {}
end
if not pflib.objects[targetName][currentLevel][baseName] then
pflib.objects[targetName][currentLevel][baseName] = {
effect = baseName,
duration = ripDuration,
start = GetTime(),
caster = "player"
}
end
if pflib.UpdateUnits then pflib:UpdateUnits() end
elseif pflib.AddEffect then
pflib:AddEffect(targetName, currentLevel, baseName, ripDuration, "player")
end
end
end
-- Update internal tracker
if lib.objects[targetGUID] and lib.objects[targetGUID][ripSpellID] then
lib.objects[targetGUID][ripSpellID].duration = ripDuration
lib.objects[targetGUID][ripSpellID].start = GetTime()
lib.objects[targetGUID][ripSpellID].expiry = GetTime() + ripDuration
end
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Carnage]|r Refreshed Rip: %ds on %s",
ripDuration, targetName or "Unknown")
)
end
break
end
end
end
-- Try to refresh Rake
if CleveRoids.lastRakeCast and CleveRoids.lastRakeCast.duration and
CleveRoids.lastRakeCast.targetGUID == targetGUID then
-- Find which Rake rank is currently active
for rakeSpellID, _ in pairs(CleveRoids.RakeSpellIDs) do
if lib.objects[targetGUID][rakeSpellID] then
-- Found an active Rake, refresh it with the saved duration
local rakeDuration = CleveRoids.lastRakeCast.duration
local rakeComboPoints = CleveRoids.lastRakeCast.comboPoints or 5
-- Get spell name for pfUI
local rakeSpellName = SpellInfo(rakeSpellID)
local baseName = rakeSpellName and string.gsub(rakeSpellName, "%s*%(Rank %d+%)", "") or "Rake"
-- Update tracking tables for pfUI hooks
if CleveRoids.ComboPointTracking then
CleveRoids.ComboPointTracking[baseName] = {
combo_points = rakeComboPoints,
duration = rakeDuration,
cast_time = GetTime(),
target = targetName,
confirmed = true
}
end
-- Store duration override for pfUI hooks
if not CleveRoids.carnageDurationOverrides then
CleveRoids.carnageDurationOverrides = {}
end
CleveRoids.carnageDurationOverrides[rakeSpellID] = {
duration = rakeDuration,
timestamp = GetTime(),
targetGUID = targetGUID
}
-- Update pfUI's debuff timer directly
if pfUI and pfUI.api and pfUI.api.libdebuff then
local pflib = pfUI.api.libdebuff
if pflib.objects and pflib.objects[targetName] then
local updated = false
local currentLevel = UnitLevel("target") or 0
-- Update at all levels where the debuff exists
for level, effects in pairs(pflib.objects[targetName]) do
if type(effects) == "table" and effects[baseName] then
local entry = effects[baseName]
entry.duration = rakeDuration
entry.start = GetTime()
entry.caster = "player"
if entry.tick then entry.tick = GetTime() end
updated = true
end
end
-- Ensure entry exists at current UnitLevel
if updated then
if not pflib.objects[targetName][currentLevel] then
pflib.objects[targetName][currentLevel] = {}
end
if not pflib.objects[targetName][currentLevel][baseName] then
pflib.objects[targetName][currentLevel][baseName] = {
effect = baseName,
duration = rakeDuration,
start = GetTime(),
caster = "player"
}
end
if pflib.UpdateUnits then pflib:UpdateUnits() end
elseif pflib.AddEffect then
pflib:AddEffect(targetName, currentLevel, baseName, rakeDuration, "player")
end
end
end
-- Update internal tracker
if lib.objects[targetGUID] and lib.objects[targetGUID][rakeSpellID] then
lib.objects[targetGUID][rakeSpellID].duration = rakeDuration
lib.objects[targetGUID][rakeSpellID].start = GetTime()
lib.objects[targetGUID][rakeSpellID].expiry = GetTime() + rakeDuration
end
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Carnage]|r Refreshed Rake: %ds on %s",
rakeDuration, targetName or "Unknown")
)
end
break
end
end
end
end
end
end
-- Check if this is a combo point scaling spell first
local duration = nil
local comboPoints = nil
if CleveRoids.TrackComboPointCastByID then
duration = CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
-- Get combo points used from tracking
if CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking.byID and
CleveRoids.ComboPointTracking.byID[spellID] then
comboPoints = CleveRoids.ComboPointTracking.byID[spellID].combo_points
end
end
-- If not a combo scaling spell, use normal duration lookup
if not duration then
duration = lib:GetDuration(spellID, casterGUID)
-- Apply talent modifiers for non-combo spells
-- (combo spells already have talent modifiers applied in CalculateComboScaledDurationByID)
if duration and CleveRoids.ApplyTalentModifier then
duration = CleveRoids.ApplyTalentModifier(spellID, duration)
end
-- Apply equipment modifiers for non-combo spells
-- (combo spells already have equipment modifiers applied in CalculateComboScaledDurationByID)
if duration and CleveRoids.ApplyEquipmentModifier then
duration = CleveRoids.ApplyEquipmentModifier(spellID, duration)
end
end
-- DEBUG: Show what duration we calculated
if CleveRoids.debug and duration then
local spellName = SpellInfo(spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[DEBUG CAST]|r %s (ID:%d) CP:%s duration:%ds",
spellName, spellID, tostring(comboPoints or "nil"), duration)
)
end
if duration and 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
-- For combo spells, populate name-based tracking for pfUI compatibility
if comboPoints and comboPoints > 0 then
local spellName = SpellInfo(spellID)
if spellName and CleveRoids.ComboPointTracking then
-- Remove rank from spell name to match pfUI's format
local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "")
CleveRoids.ComboPointTracking[baseName] = {
combo_points = comboPoints,
duration = duration,
cast_time = GetTime(),
target = targetName,
confirmed = true -- This is from actual UNIT_CASTEVENT, always confirmed
}
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffff[Name-based tracking]|r Set %s: %d CP, %ds duration (for pfUI)",
baseName, comboPoints, duration)
)
end
-- CRITICAL: Update pfUI's duration database directly
if pfUI and pfUI.api and pfUI.api.libdebuff and pfUI.api.libdebuff.debuffs then
-- pfUI stores durations by spell name in its debuffs table
pfUI.api.libdebuff.debuffs[baseName] = duration
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[pfUI Duration Inject]|r Set pfUI.api.libdebuff.debuffs['%s'] = %ds",
baseName, duration)
)
end
end
end
end
lib:AddEffect(targetGUID, targetName, spellID, duration, 0, "player")
-- DRUID CARNAGE TALENT: Save Rip cast duration for later refresh by Ferocious Bite
if CleveRoids.RipSpellIDs and CleveRoids.RipSpellIDs[spellID] then
if CleveRoids.lastRipCast then
CleveRoids.lastRipCast.duration = duration
CleveRoids.lastRipCast.targetGUID = targetGUID
CleveRoids.lastRipCast.comboPoints = comboPoints or 0
CleveRoids.lastRipCast.timestamp = GetTime()
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[Carnage]|r Saved Rip cast: %ds duration (%d CP) on target %s",
duration, comboPoints or 0, targetName or "Unknown")
)
end
end
end
-- DRUID CARNAGE TALENT: Save Rake cast duration for later refresh by Ferocious Bite
if CleveRoids.RakeSpellIDs and CleveRoids.RakeSpellIDs[spellID] then
if CleveRoids.lastRakeCast then
CleveRoids.lastRakeCast.duration = duration
CleveRoids.lastRakeCast.targetGUID = targetGUID
CleveRoids.lastRakeCast.comboPoints = comboPoints or 0
CleveRoids.lastRakeCast.timestamp = GetTime()
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[Carnage]|r Saved Rake cast: %ds duration (%d CP) on target %s",
duration, comboPoints or 0, targetName or "Unknown")
)
end
end
end
-- Sync combo duration to pfUI if it's loaded
if comboPoints and CleveRoids.Compatibility_pfUI and
CleveRoids.Compatibility_pfUI.SyncComboDurationToPfUI then
CleveRoids.Compatibility_pfUI.SyncComboDurationToPfUI(targetGUID, spellID, duration)
end
-- ALWAYS set up learning for combo spells (even if we have calculated duration)
if comboPoints then
lib.learnCastTimers[targetGUID] = lib.learnCastTimers[targetGUID] or {}
lib.learnCastTimers[targetGUID][spellID] = {
start = GetTime(),
caster = casterGUID,
comboPoints = comboPoints -- Store CP count for learning
}
end
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)
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
-- Check if this is a combo point spell - if so, learn it with combo point context
local comboPoints = lib.learnCastTimers[targetGUID][spellID].comboPoints
if comboPoints and CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellID) then
-- Learn combo spell duration
CleveRoids_ComboDurations = CleveRoids_ComboDurations or {}
CleveRoids_ComboDurations[spellID] = CleveRoids_ComboDurations[spellID] or {}
CleveRoids_ComboDurations[spellID][comboPoints] = floor(actualDuration + 0.5)
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
"|cff4b7dccCleveRoids:|r Learned combo spell " .. spellName ..
" (ID:" .. spellID .. ") at " .. comboPoints .. " CP = " .. floor(actualDuration + 0.5) .. "s"
)
end
else
-- Learn normal spell duration
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
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)
-- Cleanup dead/invalid targets
local evCleanup = CreateFrame("Frame", "CleveRoidsLibDebuffCleanupFrame", UIParent)
evCleanup:RegisterEvent("PLAYER_TARGET_CHANGED")
evCleanup:RegisterEvent("PLAYER_DEAD")
evCleanup:RegisterEvent("PLAYER_ENTERING_WORLD")
local lastCleanup = 0
local CLEANUP_THROTTLE = 2 -- Only run cleanup every 2 seconds max
evCleanup:SetScript("OnEvent", function()
local timestamp = GetTime()
-- Cleanup on zone change / login / death
if event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_DEAD" then
-- Keep only current target's data
local _, currentGUID = UnitExists("target")
if currentGUID then
local temp = lib.objects[currentGUID]
lib.objects = {}
if temp then
lib.objects[currentGUID] = temp
end
else
lib.objects = {} -- Clear everything
end
lastCleanup = timestamp
return
end
-- Throttle cleanup on target change
if event == "PLAYER_TARGET_CHANGED" then
if (timestamp - lastCleanup) < CLEANUP_THROTTLE then
return -- Don't cleanup too frequently
end
lastCleanup = timestamp
-- Remove expired effects from all GUIDs
for guid, effects in pairs(lib.objects) do
local _, targetGUID = UnitExists("target")
local isCurrentTarget = (targetGUID == guid)
-- Check if current target is dead
if isCurrentTarget and UnitIsDead("target") then
lib.objects[guid] = nil
else
-- Remove expired effects
for spellID, effect in pairs(effects) do
if effect.start + effect.duration < timestamp then
effects[spellID] = nil
end
end
-- Remove GUID if no effects remain (but keep current target)
if not next(effects) and not isCurrentTarget then
lib.objects[guid] = nil
end
end
end
end
end)
-- ============================================================================
-- TALENT MODIFIER SYSTEM
-- ============================================================================
-- Database of talent modifiers for debuff durations
-- Structure: [spellID] = {
-- talent = "Talent Name" (optional, for name-based lookup),
-- tab = talentTab (preferred, for direct position lookup),
-- id = talentID (preferred, for direct position lookup),
-- modifier = function(baseDuration, talentRank)
-- }
CleveRoids.talentModifiers = CleveRoids.talentModifiers or {}
-- ROGUE talent modifiers
-- Taste for Blood: Increases Rupture duration by 2 seconds per rank (3 ranks max)
-- Talent Position: Tab 1, Talent 10 (Assassination tree)
-- Talent Spell IDs: 14174 (Rank 1), 14175 (Rank 2), 14176 (Rank 3)
CleveRoids.talentModifiers[1943] = { tab = 1, id = 10, talent = "Taste for Blood", modifier = function(base, rank) return base + (rank * 2) end } -- Rupture Rank 1
CleveRoids.talentModifiers[8639] = { tab = 1, id = 10, talent = "Taste for Blood", modifier = function(base, rank) return base + (rank * 2) end } -- Rupture Rank 2
CleveRoids.talentModifiers[8640] = { tab = 1, id = 10, talent = "Taste for Blood", modifier = function(base, rank) return base + (rank * 2) end } -- Rupture Rank 3
CleveRoids.talentModifiers[11273] = { tab = 1, id = 10, talent = "Taste for Blood", modifier = function(base, rank) return base + (rank * 2) end } -- Rupture Rank 4
CleveRoids.talentModifiers[11274] = { tab = 1, id = 10, talent = "Taste for Blood", modifier = function(base, rank) return base + (rank * 2) end } -- Rupture Rank 5
CleveRoids.talentModifiers[11275] = { tab = 1, id = 10, talent = "Taste for Blood", modifier = function(base, rank) return base + (rank * 2) end } -- Rupture Rank 6
-- Improved Gouge: Increases Gouge duration by 0.5 seconds per rank (3 ranks max)
-- Talent Position: Tab 3, Talent 3 (Subtlety tree)
-- Talent Spell IDs: 13741 (Rank 1), 13793 (Rank 2), 13792 (Rank 3)
CleveRoids.talentModifiers[1776] = { tab = 3, id = 3, talent = "Improved Gouge", modifier = function(base, rank) return base + (rank * 0.5) end } -- Gouge Rank 1
CleveRoids.talentModifiers[1777] = { tab = 3, id = 3, talent = "Improved Gouge", modifier = function(base, rank) return base + (rank * 0.5) end } -- Gouge Rank 2
CleveRoids.talentModifiers[8629] = { tab = 3, id = 3, talent = "Improved Gouge", modifier = function(base, rank) return base + (rank * 0.5) end } -- Gouge Rank 3
CleveRoids.talentModifiers[11285] = { tab = 3, id = 3, talent = "Improved Gouge", modifier = function(base, rank) return base + (rank * 0.5) end } -- Gouge Rank 4
CleveRoids.talentModifiers[11286] = { tab = 3, id = 3, talent = "Improved Gouge", modifier = function(base, rank) return base + (rank * 0.5) end } -- Gouge Rank 5
-- PRIEST talent modifiers
-- Improved Shadow Word: Pain: Increases SW:P duration by 3 seconds per rank (2 ranks max)
-- Talent Position: Tab 3 (Shadow), Talent 4
-- Talent Spell IDs: 15275 (Rank 1), 15317 (Rank 2)
CleveRoids.talentModifiers[589] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 1
CleveRoids.talentModifiers[594] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 2
CleveRoids.talentModifiers[970] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 3
CleveRoids.talentModifiers[992] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 4
CleveRoids.talentModifiers[2767] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 5
CleveRoids.talentModifiers[10892] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 6
CleveRoids.talentModifiers[10893] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 7
CleveRoids.talentModifiers[10894] = { tab = 3, id = 4, talent = "Improved Shadow Word: Pain", modifier = function(base, rank) return base + (rank * 3) end } -- SW:P Rank 8
-- DRUID talent modifiers
-- Brutal Impact: Increases Bash and Pounce stun duration by 0.5 seconds per rank (2 ranks max)
-- Talent Position: Tab 2 (Feral Combat), Talent 4
-- Talent Spell IDs: 16940 (Rank 1), 16941 (Rank 2)
CleveRoids.talentModifiers[5211] = { tab = 2, id = 4, talent = "Brutal Impact", modifier = function(base, rank) return base + (rank * 0.5) end } -- Bash Rank 1
CleveRoids.talentModifiers[6798] = { tab = 2, id = 4, talent = "Brutal Impact", modifier = function(base, rank) return base + (rank * 0.5) end } -- Bash Rank 2
CleveRoids.talentModifiers[8983] = { tab = 2, id = 4, talent = "Brutal Impact", modifier = function(base, rank) return base + (rank * 0.5) end } -- Bash Rank 3
CleveRoids.talentModifiers[9005] = { tab = 2, id = 4, talent = "Brutal Impact", modifier = function(base, rank) return base + (rank * 0.5) end } -- Pounce Rank 1
CleveRoids.talentModifiers[9823] = { tab = 2, id = 4, talent = "Brutal Impact", modifier = function(base, rank) return base + (rank * 0.5) end } -- Pounce Rank 2
CleveRoids.talentModifiers[9827] = { tab = 2, id = 4, talent = "Brutal Impact", modifier = function(base, rank) return base + (rank * 0.5) end } -- Pounce Rank 3
-- NOTE: Carnage talent (Tab 2, ID 17) is NOT a duration modifier!
-- Carnage is a refresh mechanic: Ferocious Bite at 5 CP refreshes Rip/Rake to their original duration
-- This is handled separately in the Carnage refresh logic (lines 1125-1192)
-- Function to get talent rank
-- Supports both position-based (tab, id) and name-based lookup
-- Position-based is preferred as it's more reliable and locale-independent (like Cursive addon)
-- Parameters: talentName OR (tab, id)
-- Returns: Talent rank (0 if not found or not learned)
function CleveRoids.GetTalentRank(talentName, tab, id)
-- Method 1: Direct position lookup (preferred, like Cursive)
if tab and id then
local _, _, _, _, rank = GetTalentInfo(tab, id)
return tonumber(rank) or 0
end
-- Method 2: Name-based lookup (fallback for backwards compatibility)
if not talentName then return 0 end
-- Ensure talents are indexed
if not CleveRoids.Talents or table.getn(CleveRoids.Talents) == 0 then
if CleveRoids.IndexTalents then
CleveRoids.IndexTalents()
end
end
local rank = CleveRoids.Talents[talentName]
return (rank and tonumber(rank)) or 0
end
-- Apply talent modifiers to a debuff duration
-- Parameters:
-- spellID: The spell ID
-- baseDuration: The base duration (after combo points if applicable)
-- Returns: Modified duration, or baseDuration if no talent modifier applies
function CleveRoids.ApplyTalentModifier(spellID, baseDuration)
if not spellID or not baseDuration then
return baseDuration
end
local modifier = CleveRoids.talentModifiers[spellID]
if not modifier then
return baseDuration
end
local talentRank = 0
local lookupMethod = "none"
-- Method 1: Position-based lookup (preferred, locale-independent)
if modifier.tab and modifier.id then
local _, _, _, _, rank = GetTalentInfo(modifier.tab, modifier.id)
talentRank = tonumber(rank) or 0
if talentRank > 0 then
lookupMethod = "position"
end
end
-- Method 2: Name-based lookup (fallback for backwards compatibility)
if talentRank == 0 and modifier.talent then
talentRank = CleveRoids.GetTalentRank(modifier.talent)
if talentRank > 0 then
lookupMethod = "name"
end
end
if talentRank == 0 then
return baseDuration
end
local modifiedDuration = modifier.modifier(baseDuration, talentRank)
if CleveRoids.debug then
local talentName = modifier.talent or ("Tab " .. modifier.tab .. " ID " .. modifier.id)
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Talent Modifier]|r %s (ID:%d): %ds -> %ds (talent: %s rank %d, %s)",
SpellInfo(spellID) or "Unknown", spellID, baseDuration, modifiedDuration,
talentName, talentRank, lookupMethod)
)
end
return modifiedDuration
end
-- Helper function to register a talent modifier
-- Usage: CleveRoids.RegisterTalentModifier(spellID, talentName, modifierFunction)
function CleveRoids.RegisterTalentModifier(spellID, talentName, modifierFunc)
if not spellID or not talentName or not modifierFunc then
return false
end
CleveRoids.talentModifiers[spellID] = {
talent = talentName,
modifier = modifierFunc
}
return true
end
-- Diagnostic function to check talent modifier system
-- Usage: CleveRoids.DiagnoseTalentModifier(spellID, baseDuration)
function CleveRoids.DiagnoseTalentModifier(spellID, baseDuration)
baseDuration = baseDuration or 10 -- Default test duration
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00=== Talent Modifier Diagnostic ===|r")
-- Check if spellID is valid
local spellName = SpellInfo(spellID)
if not spellName then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR: Invalid spell ID " .. tostring(spellID) .. "|r")
return
end
DEFAULT_CHAT_FRAME:AddMessage("Spell: " .. spellName .. " (ID: " .. spellID .. ")")
-- Check if talents are indexed
if not CleveRoids.Talents then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR: Talents table doesn't exist!|r")
DEFAULT_CHAT_FRAME:AddMessage("Attempting to index talents...")
if CleveRoids.IndexTalents then
CleveRoids.IndexTalents()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Talents indexed.|r")
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR: IndexTalents function not found!|r")
return
end
end
local talentCount = 0
for k, v in pairs(CleveRoids.Talents) do
if type(k) == "string" and type(v) == "number" and v > 0 then
talentCount = talentCount + 1
end
end
DEFAULT_CHAT_FRAME:AddMessage("Talents indexed: " .. talentCount .. " talents found")
-- Check if modifier is registered
local modifier = CleveRoids.talentModifiers[spellID]
if not modifier then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000No talent modifier registered for this spell|r")
return
end
local talentDesc = modifier.talent or ("Tab " .. tostring(modifier.tab) .. " ID " .. tostring(modifier.id))
DEFAULT_CHAT_FRAME:AddMessage("Modifier registered: " .. talentDesc)
-- Check talent rank using BOTH methods
local talentRank = 0
local lookupMethod = "none"
if modifier.tab and modifier.id then
-- Position-based lookup (preferred)
local _, name, _, _, rank = GetTalentInfo(modifier.tab, modifier.id)
talentRank = tonumber(rank) or 0
lookupMethod = "position"
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Position lookup (Tab " .. modifier.tab .. ", ID " .. modifier.id .. "):|r")
DEFAULT_CHAT_FRAME:AddMessage(" Talent name: " .. tostring(name))
DEFAULT_CHAT_FRAME:AddMessage(" Rank: " .. talentRank)
end
if modifier.talent then
-- Name-based lookup (fallback)
local nameRank = CleveRoids.GetTalentRank(modifier.talent)
DEFAULT_CHAT_FRAME:AddMessage("|cffaaaa00Name lookup (" .. modifier.talent .. "):|r")
DEFAULT_CHAT_FRAME:AddMessage(" Rank: " .. nameRank)
-- Use name rank if position didn't work
if talentRank == 0 and nameRank > 0 then
talentRank = nameRank
lookupMethod = "name"
end
-- Check if talent exists in table
local directLookup = CleveRoids.Talents[modifier.talent]
DEFAULT_CHAT_FRAME:AddMessage(" Direct table lookup: " .. tostring(directLookup))
end
DEFAULT_CHAT_FRAME:AddMessage("Final rank: " .. talentRank .. " (via " .. lookupMethod .. ")")
if talentRank == 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000You don't have " .. talentDesc .. "!|r")
-- Show available talents for debugging
DEFAULT_CHAT_FRAME:AddMessage("Available talents with ranks:")
local shown = 0
for name, rank in pairs(CleveRoids.Talents) do
if type(name) == "string" and type(rank) == "number" and rank > 0 then
DEFAULT_CHAT_FRAME:AddMessage(" - " .. name .. ": " .. rank)
shown = shown + 1
if shown >= 10 then
DEFAULT_CHAT_FRAME:AddMessage(" ... (showing first 10)")
break
end
end
end
else
-- Test the modifier
local modifiedDuration = modifier.modifier(baseDuration, talentRank)
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Test calculation:|r")
DEFAULT_CHAT_FRAME:AddMessage(" Base: " .. baseDuration .. "s")
DEFAULT_CHAT_FRAME:AddMessage(" Modified: " .. modifiedDuration .. "s")
DEFAULT_CHAT_FRAME:AddMessage(" Bonus: +" .. (modifiedDuration - baseDuration) .. "s")
-- Test actual application
local applied = CleveRoids.ApplyTalentModifier(spellID, baseDuration)
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Via ApplyTalentModifier:|r " .. applied .. "s")
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00=== End Diagnostic ===|r")
end
-- ============================================================================
-- EQUIPMENT MODIFIER SYSTEM
-- ============================================================================
-- Database of equipment modifiers for debuff durations
-- Structure: [spellID] = { slot = invSlotID, items = { [itemID] = modifierFunction } }
CleveRoids.equipmentModifiers = CleveRoids.equipmentModifiers or {}
-- DRUID equipment modifiers
-- Idol of Savagery (Black Morass Idol): Reduces Rip and Rake duration by 10% (multiplicative)
-- Item ID: 61699, Slot: 18 (Ranged/Relic)
local ripRakeIdolModifier = function(duration, itemID)
if itemID == 61699 then
-- Idol of Savagery: 10% reduction (multiply by 0.9)
return duration * 0.9
end
return duration
end
-- Rip (all ranks)
CleveRoids.equipmentModifiers[1079] = { slot = 18, modifier = ripRakeIdolModifier } -- Rip Rank 1
CleveRoids.equipmentModifiers[9492] = { slot = 18, modifier = ripRakeIdolModifier } -- Rip Rank 2
CleveRoids.equipmentModifiers[9493] = { slot = 18, modifier = ripRakeIdolModifier } -- Rip Rank 3
CleveRoids.equipmentModifiers[9752] = { slot = 18, modifier = ripRakeIdolModifier } -- Rip Rank 4
CleveRoids.equipmentModifiers[9894] = { slot = 18, modifier = ripRakeIdolModifier } -- Rip Rank 5
CleveRoids.equipmentModifiers[9896] = { slot = 18, modifier = ripRakeIdolModifier } -- Rip Rank 6
-- Rake (all ranks)
CleveRoids.equipmentModifiers[1822] = { slot = 18, modifier = ripRakeIdolModifier } -- Rake Rank 1
CleveRoids.equipmentModifiers[1823] = { slot = 18, modifier = ripRakeIdolModifier } -- Rake Rank 2
CleveRoids.equipmentModifiers[1824] = { slot = 18, modifier = ripRakeIdolModifier } -- Rake Rank 3
CleveRoids.equipmentModifiers[9904] = { slot = 18, modifier = ripRakeIdolModifier } -- Rake Rank 4
-- Function to get equipped item ID in a specific slot
function CleveRoids.GetEquippedItemID(slotID)
local itemLink = GetInventoryItemLink("player", slotID)
if not itemLink then return nil end
local _, _, itemID = string.find(itemLink, "item:(%d+)")
return tonumber(itemID)
end
-- Apply equipment modifiers to a debuff duration
-- Parameters:
-- spellID: The spell ID
-- baseDuration: The base duration (after combo points and talents)
-- Returns: Modified duration, or baseDuration if no equipment modifier applies
function CleveRoids.ApplyEquipmentModifier(spellID, baseDuration)
if not spellID or not baseDuration then
return baseDuration
end
local modifier = CleveRoids.equipmentModifiers[spellID]
if not modifier then
return baseDuration
end
local itemID = CleveRoids.GetEquippedItemID(modifier.slot)
if not itemID then
return baseDuration
end
local modifiedDuration = modifier.modifier(baseDuration, itemID)
if modifiedDuration ~= baseDuration and CleveRoids.debug then
local itemName = "Unknown"
local itemLink = GetInventoryItemLink("player", modifier.slot)
if itemLink then
itemName = string.match(itemLink, "%[(.-)%]") or "Unknown"
end
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[Equipment Modifier]|r %s (ID:%d): %ds -> %ds (item: %s [%d])",
SpellInfo(spellID) or "Unknown", spellID, baseDuration, modifiedDuration,
itemName, itemID)
)
end
return modifiedDuration
end
-- Helper function to register an equipment modifier
-- Usage: CleveRoids.RegisterEquipmentModifier(spellID, slotID, modifierFunction)
function CleveRoids.RegisterEquipmentModifier(spellID, slotID, modifierFunc)
if not spellID or not slotID or not modifierFunc then
return false
end
CleveRoids.equipmentModifiers[spellID] = {
slot = slotID,
modifier = modifierFunc
}
return true
end
-- ============================================================================
-- IMMUNITY TRACKING SYSTEM
-- ============================================================================
-- Initialize SavedVariables for immunity tracking
CleveRoids_ImmunityData = CleveRoids_ImmunityData or {}
-- Spell school constants
local IMMUNITY_SCHOOLS = {
physical = 1,
holy = 2,
fire = 3,
nature = 4,
frost = 5,
shadow = 6,
arcane = 7,
}
-- Cache for spell school lookups
local spellSchoolCache = {}
-- Get the damage school of a spell
local function GetSpellSchool(spellName)
if not spellName then return nil end
-- Remove rank information for cache consistency
local baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
-- Check cache first
if spellSchoolCache[baseName] then
return spellSchoolCache[baseName]
end
-- Try to find spell in player's spellbook
local spell = CleveRoids.GetSpell(baseName)
if not spell then
return nil
end
-- Create tooltip if needed
if not CleveRoidsSchoolTooltip then
CreateFrame("GameTooltip", "CleveRoidsSchoolTooltip", nil, "GameTooltipTemplate")
CleveRoidsSchoolTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
end
CleveRoidsSchoolTooltip:ClearLines()
CleveRoidsSchoolTooltip:SetSpell(spell.spellSlot, spell.bookType)
-- Scan tooltip for school keywords
local school = nil
for i = 1, CleveRoidsSchoolTooltip:NumLines() do
local line = _G["CleveRoidsSchoolTooltipTextLeft" .. i]
if line then
local text = string.lower(line:GetText() or "")
if string.find(text, "fire") or string.find(text, "flame") then
school = "fire"
break
elseif string.find(text, "frost") or string.find(text, "ice") then
school = "frost"
break
elseif string.find(text, "nature") or string.find(text, "poison") then
school = "nature"
break
elseif string.find(text, "shadow") or string.find(text, "dark") then
school = "shadow"
break
elseif string.find(text, "arcane") then
school = "arcane"
break
elseif string.find(text, "holy") or string.find(text, "divine") then
school = "holy"
break
end
end
end
-- Fallback: Use spell name patterns for common spells
if not school then
local lower = string.lower(baseName)
if string.find(lower, "fire") or string.find(lower, "flame") or string.find(lower, "immolat") or string.find(lower, "scorch") then
school = "fire"
elseif string.find(lower, "frost") or string.find(lower, "ice") or string.find(lower, "blizzard") then
school = "frost"
elseif string.find(lower, "nature") or string.find(lower, "poison") or string.find(lower, "sting") then
school = "nature"
elseif string.find(lower, "shadow") or string.find(lower, "curse") or string.find(lower, "corruption") then
school = "shadow"
elseif string.find(lower, "arcane") then
school = "arcane"
elseif string.find(lower, "holy") or string.find(lower, "smite") or string.find(lower, "exorcism") then
school = "holy"
else
school = "physical"
end
end
-- Cache the result
spellSchoolCache[baseName] = school
return school
end
-- Get current buffs on a unit
local function GetUnitBuffs(unit)
local buffs = {}
if not CleveRoids.hasSuperwow then return buffs end
for i = 1, 32 do
local texture, stacks, spellID = UnitBuff(unit, i)
if not texture then break end
if spellID then
local buffName = SpellInfo(spellID)
if buffName then
buffs[buffName] = true
end
end
end
return buffs
end
-- Record an immunity (permanent or buff-based)
local function RecordImmunity(npcName, spellName, conditionalBuff)
if not npcName or not spellName or npcName == "" then
return
end
local school = GetSpellSchool(spellName)
if not school then
return
end
-- Initialize school table
if not CleveRoids_ImmunityData[school] then
CleveRoids_ImmunityData[school] = {}
end
-- Record immunity
if conditionalBuff then
-- Buff-based immunity
CleveRoids_ImmunityData[school][npcName] = {
buff = conditionalBuff
}
if CleveRoids.debug then
CleveRoids.Print("|cffff6600Immunity:|r " .. npcName .. " is immune to " .. school .. " when buffed with: " .. conditionalBuff)
end
else
-- Permanent immunity
if CleveRoids_ImmunityData[school][npcName] ~= true then
CleveRoids_ImmunityData[school][npcName] = true
if CleveRoids.debug then
CleveRoids.Print("|cffff6600Immunity:|r " .. npcName .. " is permanently immune to " .. school)
end
end
end
end
-- Combat log parser for immunity detection
local function ParseImmunityCombatLog()
local message = arg1 -- Formatted message
local rawMessage = arg2 -- Raw message
if not rawMessage then return end
-- Pattern: "X's SpellName fails. Y is immune."
local spellName = nil
local targetName = nil
-- Extract spell name from raw message
local _, _, extractedSpell = string.find(rawMessage, "'s%s+(.-)%s+fails%.")
if not extractedSpell then
_, _, extractedSpell = string.find(rawMessage, "Your%s+(.-)%s+fails%.")
end
-- Extract target name from formatted message (more reliable)
if message then
local _, _, extractedTarget = string.find(message, "fails%.%s+(.-)%s+is immune")
if extractedTarget then
targetName = extractedTarget
end
end
-- Fallback: Try to get target from current target
if not targetName and UnitExists("target") then
local _, targetGUID = UnitExists("target")
if targetGUID and rawMessage and string.find(rawMessage, targetGUID) then
targetName = UnitName("target")
end
end
if extractedSpell and targetName then
-- Check if target has any buffs (for conditional immunity)
local buffs = nil
if UnitExists("target") and UnitName("target") == targetName then
buffs = GetUnitBuffs("target")
end
-- If target has exactly one buff, assume it's causing the immunity
if buffs and next(buffs) then
local buffCount = 0
local singleBuff = nil
for buff, _ in pairs(buffs) do
buffCount = buffCount + 1
singleBuff = buff
if buffCount > 1 then
singleBuff = nil
break
end
end
if singleBuff then
RecordImmunity(targetName, extractedSpell, singleBuff)
return
end
end
-- No single buff detected, record as permanent immunity
RecordImmunity(targetName, extractedSpell, nil)
end
end
-- Check if a unit is immune to a spell or damage school
-- Supports: CheckImmunity(unitId, "Flame Shock") or CheckImmunity(unitId, "fire")
function CleveRoids.CheckImmunity(unitId, spellOrSchool)
if not unitId or not UnitExists(unitId) then
return false
end
-- Only works on NPCs
if UnitIsPlayer(unitId) then
return false
end
local targetName = UnitName(unitId)
if not targetName or targetName == "" then
return false
end
if not spellOrSchool or spellOrSchool == "" then
return false
end
-- Check if input is a spell school name directly
local inputLower = string.lower(spellOrSchool)
local school = nil
if IMMUNITY_SCHOOLS[inputLower] then
-- Input is a damage school name (fire, frost, nature, etc.)
school = inputLower
else
-- Input is a spell name, need to determine its school
school = GetSpellSchool(spellOrSchool)
if not school then
return false
end
end
-- Check immunity data for this school
if not CleveRoids_ImmunityData[school] then
return false
end
local immunityData = CleveRoids_ImmunityData[school][targetName]
-- No immunity data for this NPC
if not immunityData then
return false
end
-- Permanent immunity
if immunityData == true then
return true
end
-- Buff-based immunity (check if NPC has the required buff)
if type(immunityData) == "table" and immunityData.buff then
local requiredBuff = immunityData.buff
-- Check target's buffs
if CleveRoids.hasSuperwow then
for i = 1, 32 do
local texture, stacks, spellID = UnitBuff(unitId, i)
if not texture then break end
if spellID then
local buffName = SpellInfo(spellID)
if buffName and buffName == requiredBuff then
return true
end
end
end
end
-- Buff not found, not currently immune
return false
end
return false
end
-- Management functions for immunity data
function CleveRoids.ListImmunities(school)
if school then
school = string.lower(school)
if not CleveRoids_ImmunityData[school] then
CleveRoids.Print("No immunity data for school: " .. school)
return
end
CleveRoids.Print("|cff00ff00" .. school .. " immunities:|r")
local count = 0
for npc, data in pairs(CleveRoids_ImmunityData[school]) do
if data == true then
CleveRoids.Print(" - " .. npc .. " (permanent)")
elseif type(data) == "table" and data.buff then
CleveRoids.Print(" - " .. npc .. " (when buffed: " .. data.buff .. ")")
end
count = count + 1
end
CleveRoids.Print("Total: " .. count)
else
-- List all schools
CleveRoids.Print("|cff00ff00Immunity Data by School:|r")
for schoolName, npcs in pairs(CleveRoids_ImmunityData) do
local count = 0
for _ in pairs(npcs) do
count = count + 1
end
if count > 0 then
CleveRoids.Print(" " .. schoolName .. ": " .. count .. " NPCs")
end
end
end
end
function CleveRoids.ClearImmunities(school)
if school then
school = string.lower(school)
CleveRoids_ImmunityData[school] = {}
CleveRoids.Print("Cleared " .. school .. " immunity data")
else
CleveRoids_ImmunityData = {}
CleveRoids.Print("Cleared all immunity data")
end
end
function CleveRoids.AddImmunity(npcName, school, buffName)
if not npcName or not school then
CleveRoids.Print("Usage: /cleveroid addimmune <npc name> <school> [buff name]")
CleveRoids.Print("Schools: fire, frost, nature, shadow, arcane, holy, physical")
return
end
school = string.lower(school)
if not IMMUNITY_SCHOOLS[school] then
CleveRoids.Print("Invalid school. Use: fire, frost, nature, shadow, arcane, holy, physical")
return
end
if not CleveRoids_ImmunityData[school] then
CleveRoids_ImmunityData[school] = {}
end
if buffName and buffName ~= "" then
CleveRoids_ImmunityData[school][npcName] = { buff = buffName }
CleveRoids.Print("Added: " .. npcName .. " is immune to " .. school .. " when buffed with: " .. buffName)
else
CleveRoids_ImmunityData[school][npcName] = true
CleveRoids.Print("Added: " .. npcName .. " is permanently immune to " .. school)
end
end
function CleveRoids.RemoveImmunity(npcName, school)
if not npcName or not school then
CleveRoids.Print("Usage: /cleveroid removeimmune <npc name> <school>")
return
end
school = string.lower(school)
if CleveRoids_ImmunityData[school] and CleveRoids_ImmunityData[school][npcName] then
CleveRoids_ImmunityData[school][npcName] = nil
CleveRoids.Print("Removed: " .. npcName .. " from " .. school .. " immunities")
else
CleveRoids.Print("Not found: " .. npcName .. " in " .. school .. " immunities")
end
end
-- Register combat log event for immunity tracking
local immunityFrame = CreateFrame("Frame", "CleveRoidsImmunityFrame")
immunityFrame:RegisterEvent("RAW_COMBATLOG")
immunityFrame:SetScript("OnEvent", function()
if event == "RAW_COMBATLOG" then
ParseImmunityCombatLog()
end
end)