From 8542a051707d4b9eed71bd05a544adb398c1b4e8 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 21 Oct 2025 22:18:10 -0400 Subject: [PATCH] implement standalone debuff timer system --- Conditionals.lua | 57 ++- Core.lua | 108 ++++-- README.md | 79 +++- SuperCleveRoidMacros.toc | 4 +- Utility.lua | 756 +++++++++++++++++++++++++++++---------- 5 files changed, 753 insertions(+), 251 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index 3c57df6..7b986e5 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -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 diff --git a/Core.lua b/Core.lua index 8adfade..8d6c744 100644 --- a/Core.lua +++ b/Core.lua @@ -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 - 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 - 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 - Manually set spell duration") + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget - Forget learned duration(s)") + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages") + end end diff --git a/README.md b/README.md index 90aab21..e494ed5 100644 --- a/README.md +++ b/README.md @@ -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 ` - Manually set spell duration in seconds +* `/cleveroid forget ` - 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 diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 77b9739..87ea771 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -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 diff --git a/Utility.lua b/Utility.lua index 5529b7c..8cf28d3 100644 --- a/Utility.lua +++ b/Utility.lua @@ -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 ==============================================