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
+28 -29
View File
@@ -83,18 +83,34 @@ local function PFUI_HasLibDebuff()
and type(pfUI.api.libdebuff.UnitDebuff) == "function"
end
-- Returns seconds-left (number) or nil if unsupported/not found
local function GetDebuffTimeLeft_PFUI(unit, debuffName)
if not PFUI_HasLibDebuff() then return nil end
unit = unit or "target"
for i = 1, 16 do
local name, _, _, _, _, duration, timeleft = pfUI.api.libdebuff:UnitDebuff(unit, i)
if not name then break end
if name == debuffName and timeleft and timeleft >= 0 then
return timeleft, duration
-- Helper: Get debuff time-left (seconds) from CleveRoids.libdebuff only
local function _get_debuff_timeleft(unitToken, auraName)
-- SuperWoW path: GUID-based lookup
if CleveRoids.hasSuperwow then
local _, guid = UnitExists(unitToken)
if guid and CleveRoids.libdebuff and CleveRoids.libdebuff.objects[guid] then
for i = 1, 16 do
local effect, _, _, _, _, duration, timeleft = CleveRoids.libdebuff:UnitDebuff(unitToken, i)
if not effect then break end
if effect == auraName and timeleft and timeleft >= 0 then
return timeleft, duration
end
end
end
end
end
return nil
-- Non-SuperWoW fallback
if CleveRoids.libdebuff and CleveRoids.libdebuff.UnitDebuff then
for idx = 1, 16 do
local effect, _, _, _, _, duration, timeleft = CleveRoids.libdebuff:UnitDebuff(unitToken, idx)
if not effect then break end
if effect == auraName and timeleft and timeleft >= 0 then
return timeleft, duration
end
end
end
return nil, nil
end
-- Validates that the given target is either friend (if [help]) or foe (if [harm])
@@ -719,23 +735,6 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
return found
end
-- Helper: pfUI libdebuff time-left (seconds) if available
local function _pfui_timeleft(unitToken, auraName)
if type(pfUI) == "table"
and type(pfUI.api) == "table"
and type(pfUI.api.libdebuff) == "table"
and type(pfUI.api.libdebuff.UnitDebuff) == "function" then
for idx = 1, 16 do
local name, _, _, _, _, duration, timeleft = pfUI.api.libdebuff:UnitDebuff(unitToken, idx)
if not name then break end
if name == auraName and timeleft and timeleft >= 0 then
return timeleft, duration
end
end
end
return nil, nil
end
-- Case B: Numeric/stack condition exists.
if hasNumCheck then
-- Stacks compare path
@@ -756,7 +755,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
return cmp[args.operator](tl, args.amount)
else
-- Non-player: try pfUI → internal libdebuff → 0s
local tl = _pfui_timeleft(unit, args.name)
local tl = _get_debuff_timeleft(unit, args.name)
if tl ~= nil then
return cmp[args.operator](tl or 0, args.amount)
end
+84 -24
View File
@@ -2506,12 +2506,8 @@ CleveRoids.RegisterMouseOverResolver = function(fn)
end
end
-- Bandaid so pfUI doesn't need to be edited
-- pfUI/modules/thirdparty-vanilla.lua:914
CleverMacro = true
---- START of pfUI Focus Fix ----
do
local f = CreateFrame("Frame")
f:SetScript("OnEvent", function(self, event, arg1)
@@ -2579,28 +2575,26 @@ do
end
end)
f:RegisterEvent("PLAYER_LOGIN")
end
---- END of pfUI Focus Fix ----
end
SLASH_CLEVEROID1 = "/cleveroid"
SLASH_CLEVEROID2 = "/cleveroidmacros"
SlashCmdList["CLEVEROID"] = function(msg)
if type(msg) ~= "string" then
msg = ""
end
local cmd, val
local s, e, a, b = string.find(msg, "^(%S*)%s*(%S*)$")
local cmd, val, val2
local s, e, a, b, c = string.find(msg, "^(%S*)%s*(%S*)%s*(%S*)$")
if a then cmd = a else cmd = "" end
if b then val = b else val = "" end
if c then val2 = c else val2 = "" end
-- No command: show current value
if cmd == "" then
CleveRoids.Print("Current Settings:")
DEFAULT_CHAT_FRAME:AddMessage("realtime (force fast updates, CPU intensive) = " .. CleveRoidMacros.realtime .. " (Default: 0)")
DEFAULT_CHAT_FRAME:AddMessage("refresh (updates per second) = " .. CleveRoidMacros.refresh .. " (Default: 5)")
DEFAULT_CHAT_FRAME:AddMessage("debug (show learning messages) = " .. (CleveRoids.debug and "1" or "0") .. " (Default: 0)")
return
end
@@ -2612,26 +2606,92 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids.Print("realtime set to " .. num)
else
CleveRoids.Print("Usage: /cleveroid realtime 0 or 1 - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load.)")
CleveRoids.Print("Current realtime = " .. tostring(CleveRoidMacros.realtime))
CleveRoids.Print("Current realtime = " .. tostring(CleveRoidMacros.realtime))
end
return
end
-- refresh
if cmd == "refresh" then
local num = tonumber(val)
if num and num >= 1 and num <= 10 then
CleveRoidMacros.refresh = num
CleveRoids.Print("refresh set to " .. num .. " times per second")
else
CleveRoids.Print("Usage: /cleveroid refresh X - Set refresh rate. (1 to 10 updates per second. Default: 5)")
CleveRoids.Print("Current refresh = " .. tostring(CleveRoidMacros.refresh) .. " times per second")
end
return
end
local num = tonumber(val)
if num and num >= 1 and num <= 10 then
CleveRoidMacros.refresh = num
CleveRoids.Print("refresh set to " .. num .. " times per second")
else
CleveRoids.Print("Usage: /cleveroid refresh X - Set refresh rate. (1 to 10 updates per second. Default: 5)")
CleveRoids.Print("Current refresh = " .. tostring(CleveRoidMacros.refresh) .. " times per second")
end
return
end
-- learn (manual set duration)
if cmd == "learn" then
if not CleveRoids.hasSuperwow then
CleveRoids.Print("Learning system requires SuperWoW client!")
return
end
local spellID = tonumber(val)
local duration = tonumber(val2)
if spellID and duration then
local _, playerGUID = UnitExists("player")
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {}
CleveRoids_LearnedDurations[spellID][playerGUID] = duration
local spellName = SpellInfo(spellID) or "Unknown"
CleveRoids.Print("Set " .. spellName .. " (ID:" .. spellID .. ") duration to " .. duration .. "s")
else
CleveRoids.Print("Usage: /cleveroid learn <spellID> <duration> - Manually set spell duration")
CleveRoids.Print("Example: /cleveroid learn 11597 30")
end
return
end
-- forget (delete learned duration)
if cmd == "forget" or cmd == "unlearn" then
if not CleveRoids.hasSuperwow then
CleveRoids.Print("Learning system requires SuperWoW client!")
return
end
if val == "all" then
CleveRoids_LearnedDurations = {}
CleveRoids.Print("Forgot all learned spell durations")
else
local spellID = tonumber(val)
if spellID and CleveRoids_LearnedDurations and CleveRoids_LearnedDurations[spellID] then
local spellName = SpellInfo(spellID) or "Unknown"
CleveRoids_LearnedDurations[spellID] = nil
CleveRoids.Print("Forgot " .. spellName .. " (ID:" .. spellID .. ") duration")
elseif spellID then
CleveRoids.Print("No learned duration for spell ID " .. spellID)
else
CleveRoids.Print("Usage: /cleveroid forget <spellID> - Forget learned duration")
CleveRoids.Print(" /cleveroid forget all - Forget all learned durations")
end
end
return
end
-- debug (toggle learning messages)
if cmd == "debug" then
local num = tonumber(val)
if num == 0 or num == 1 then
CleveRoids.debug = (num == 1)
CleveRoids.Print("debug set to " .. num)
else
CleveRoids.debug = not CleveRoids.debug
CleveRoids.Print("debug " .. (CleveRoids.debug and "enabled" or "disabled"))
end
return
end
-- Unknown command fallback
CleveRoids.Print("Usage:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load.)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate. (1 to 10 updates per second. Default: 5)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates (Default: 0. 1 = on, increases CPU load)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1 to 10 updates per second. Default: 5)")
if CleveRoids.hasSuperwow then
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages")
end
end
+71 -8
View File
@@ -20,15 +20,14 @@ Check slash command and all conditional lists for new usages!
* [Nampower](https://github.com/pepopo978/nampower) dll mod is required
### Turtle WoW Launcher / GitAddonsManager
1. Open either application
2. Click the Add button
3. Paste the url `https://github.com/jrc13245/SuperCleveRoidMacros`
4. Download and keep up to date
### SuperCleveRoidMacros Settings
* /cleveroid - view settings
* /cleveroid realtime 0 or 1 - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load.)
* /cleveroid refresh X - Set refresh rate. (1 to 10 updates per second. Default: 5)
* `/cleveroid` - View current settings
* `/cleveroid realtime 0 or 1` - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load)
* `/cleveroid refresh X` - Set refresh rate (1 to 10 updates per second. Default: 5)
* `/cleveroid learn <spellID> <duration>` - Manually set spell duration in seconds
* `/cleveroid forget <spellID|all>` - Forget learned spell duration(s)
* `/cleveroid debug [0|1]` - Toggle debug messages for spell duration learning
---
## Known Issues
@@ -187,6 +186,70 @@ Check slash command and all conditional lists for new usages!
| /cast | * | Casts a spell by name |
| /stopmacro | * | prevent any lines under /stopmacro from being run unless conditionals are met |
## Debuff Timer System (SuperWoW)
SuperCleveRoidMacros includes a built-in **debuff timer tracking system** that accurately tracks your debuff durations using SuperWoW's advanced features:
### Features
* ✅ **Auto-Learning** - Automatically learns debuff durations as you cast them
* ✅ **Accurate Tracking** - Only tracks debuffs that successfully land (no false timers on misses)
* ✅ **329+ Debuffs Pre-configured** - Extensive database of vanilla 1.12.1 spell IDs
* ✅ **Per-Caster Storage** - Handles talent variations by tracking per player GUID
* ✅ **GUID-Based** - Correctly handles multiple mobs with the same name
* ✅ **No pfUI Required** - Fully independent debuff tracking system
### How It Works
1. When you cast a debuff, the system records the timestamp
2. When the debuff fades, it calculates the actual duration
3. Duration is saved and used for all future casts of that spell
4. You can check debuff timers in macros using conditionals like `[debuff:Sunder Armor>25]`
### Manual Control
```lua
/cleveroid learn 11597 30 -- Set Sunder Armor (ID 11597) to 30 seconds
/cleveroid forget 11597 -- Forget Sunder Armor duration
/cleveroid forget all -- Clear all learned durations
/cleveroid debug 1 -- Enable learning messages in chat
```
### Macro Examples
```lua
-- Maintain Sunder Armor with 25s buffer
/cast [debuff:Sunder Armor>25] Heroic Strike; Sunder Armor
-- Refresh Corruption when <4s remain
/cast [debuff:Corruption<4] Corruption; Shadow Bolt
-- Apply Serpent Sting if missing or <5s
/cast [nodebuff:Serpent Sting] Serpent Sting; [debuff:Serpent Sting<5] Serpent Sting; Steady Shot
-- Multi-DoT priority system
/cast [nodebuff:Moonfire] Moonfire
/cast [debuff:Moonfire<4] Moonfire
/cast [nodebuff:Insect Swarm] Insect Swarm
/cast Wrath
```
### Supported Debuffs
The system includes pre-configured durations for 329+ debuffs across all classes:
- **Warrior:** Sunder Armor, Rend, Hamstring, Thunder Clap, Demoralizing Shout, etc.
- **Rogue:** Rupture, Garrote, Expose Armor, Poisons, etc.
- **Hunter:** Serpent Sting, Hunter's Mark, Wing Clip, Wyvern Sting, etc.
- **Druid:** Rip, Rake, Moonfire, Insect Swarm, Faerie Fire, Entangling Roots, etc.
- **Warlock:** Corruption, Curses, Immolate, Siphon Life, etc.
- **Mage:** Polymorph (all variants), Slow effects, etc.
- **Priest:** Shadow Word: Pain, Devouring Plague, Mind Flay, etc.
- **Paladin:** Judgements, Hammer of Justice, etc.
- **Shaman:** Flame Shock, Frost Shock, etc.
### Technical Details
- Uses **UNIT_CASTEVENT** for precise cast detection (only tracks successful hits)
- Uses **RAW_COMBATLOG** to detect when debuffs fade
- Stores durations in **SavedVariables** (persists between sessions)
- Falls back to static database for unknown spells
- No dependency on pfUI or other addons
---
## Cast Sequence
```lua
#showtooltip
+2 -2
View File
@@ -2,9 +2,9 @@
## Author: DWG, _brain, Torio, yani9o
## Title: [Super]CleveRoidMacros
## Notes: /cleveroid for settings
## Version: 1.2
## Version: 1.3
## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar
## SavedVariables: CleveRoidMacros
## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures
Localization.lua
Init.lua
Utility.lua
+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 ==============================================