diff --git a/Conditionals.lua b/Conditionals.lua index fdb40c5..29f37bf 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -448,6 +448,26 @@ local stat_checks = { nature_power = function() return GetSpellBonusDamage(2) end, shadow_power = function() return GetSpellBonusDamage(5) end, + -- Highest spell power across all schools + -- Uses Nampower v2.31+ GetSpellPower when available (single call), falls back to per-school + spell_power = function() + local API = CleveRoids.NampowerAPI + if API and API.features and API.features.hasGetSpellPower then + local p, h, fi, n, fr, s, a = API.GetSpellPower() + if p then + return math.max(p, h, fi, n, fr, s, a) + end + end + -- Fallback: max of per-school GetSpellBonusDamage calls + return math.max( + GetSpellBonusDamage(2) or 0, + GetSpellBonusDamage(3) or 0, + GetSpellBonusDamage(4) or 0, + GetSpellBonusDamage(5) or 0, + GetSpellBonusDamage(6) or 0 + ) + end, + -- Defensive Stats armor = function() local _, effective = UnitArmor("player"); return effective end, defense = function() @@ -1091,6 +1111,10 @@ autoAttackFrame:SetScript("OnEvent", function() this:RegisterEvent("AURA_CAST_ON_SELF") this:RegisterEvent("AURA_CAST_ON_OTHER") + -- Register removal events for instant AllCasterAuraTracking cleanup + this:RegisterEvent("BUFF_REMOVED_OTHER") + this:RegisterEvent("DEBUFF_REMOVED_OTHER") + -- Check if the CVar is enabled (required for all-caster buff tracking) local auraCastEnabled = GetCVar("NP_EnableAuraCastEvents") if auraCastEnabled ~= "1" then @@ -1109,32 +1133,42 @@ autoAttackFrame:SetScript("OnEvent", function() elseif event == "BUFF_UPDATE_DURATION_SELF" or event == "DEBUFF_UPDATE_DURATION_SELF" then -- v2.30+: Player aura duration was refreshed -- arg1 = auraSlot (0-based raw slot index) + -- arg2 = durationMs, arg3 = expirationTimeMs, arg4 = spellId (v2.33+) local auraSlot = arg1 - if auraSlot and API and API.GetPlayerAuraDuration then - local spellId, durationMs, expirationTimeMs = API.GetPlayerAuraDuration(auraSlot) - if spellId and spellId > 0 and durationMs and durationMs > 0 then - local _, playerGUID = UnitExists("player") - if playerGUID then - if not CleveRoids.AllCasterAuraTracking[playerGUID] then - CleveRoids.AllCasterAuraTracking[playerGUID] = {} - end - local durationSec = durationMs / 1000 - local now = GetTime() - local expirationSec = expirationTimeMs and expirationTimeMs / 1000 - local startTime = expirationSec and (expirationSec - durationSec) or now - CleveRoids.AllCasterAuraTracking[playerGUID][spellId] = { - start = startTime, - duration = durationSec, - casterGuid = playerGUID, - } + local spellId, durationMs, expirationTimeMs - if CleveRoids.debug then - local spellName = SpellInfo and SpellInfo(spellId) or "Unknown" - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cff88ff88[AuraDurUpdate]|r %s (slot:%d, ID:%d) dur=%.1fs", - spellName, auraSlot, spellId, durationSec - )) - end + -- v2.33+: spellId provided directly as arg4, with duration info in arg2/arg3 + if arg4 and arg4 > 0 then + spellId = arg4 + durationMs = arg2 + expirationTimeMs = arg3 + elseif auraSlot and API and API.GetPlayerAuraDuration then + -- Pre-v2.33 fallback: query duration by aura slot + spellId, durationMs, expirationTimeMs = API.GetPlayerAuraDuration(auraSlot) + end + + if spellId and spellId > 0 and durationMs and durationMs > 0 then + local _, playerGUID = UnitExists("player") + if playerGUID then + if not CleveRoids.AllCasterAuraTracking[playerGUID] then + CleveRoids.AllCasterAuraTracking[playerGUID] = {} + end + local durationSec = durationMs / 1000 + local now = GetTime() + local expirationSec = expirationTimeMs and expirationTimeMs / 1000 + local startTime = expirationSec and (expirationSec - durationSec) or now + CleveRoids.AllCasterAuraTracking[playerGUID][spellId] = { + start = startTime, + duration = durationSec, + casterGuid = playerGUID, + } + + if CleveRoids.debug then + local spellName = SpellInfo and SpellInfo(spellId) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cff88ff88[AuraDurUpdate]|r %s (slot:%d, ID:%d) dur=%.1fs", + spellName, auraSlot, spellId, durationSec + )) end end end @@ -1154,6 +1188,20 @@ autoAttackFrame:SetScript("OnEvent", function() elseif event == "AURA_CAST_ON_OTHER" then OnAuraCastOther(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) + + elseif event == "BUFF_REMOVED_OTHER" or event == "DEBUFF_REMOVED_OTHER" then + -- Instant cleanup of AllCasterAuraTracking when auras are removed + -- arg1=targetGuid, arg2=spellName, arg3=spellId, arg7=state (v2.32+: 0=added, 1=removed, 2=modified) + local guid = arg1 + local spellId = arg3 + local state = arg7 + if state == 2 then return end -- Stack change, not full removal + if guid and spellId and CleveRoids.AllCasterAuraTracking[guid] then + CleveRoids.AllCasterAuraTracking[guid][spellId] = nil + if not next(CleveRoids.AllCasterAuraTracking[guid]) then + CleveRoids.AllCasterAuraTracking[guid] = nil + end + end end end) @@ -3132,60 +3180,58 @@ function CleveRoids.ValidateAura(unit, args, isbuff) local searchID = args.name and tonumber(args.name) local searchName = not searchID and args.name and _string_lower(args.name) or nil - -- Primary search: BUFFS if isbuff==true, DEBUFFS if isbuff==false - while true do - local texture - local current_spellID = nil - - if isPlayer then - -- GetPlayerAura(index, isbuff) => texture, stacks, spellID, timeLeft - texture, stacks, current_spellID, remaining = CleveRoids.GetPlayerAura(i, isbuff) - else - if isbuff then - -- UnitBuff => texture, stacks, spellID - texture, stacks, current_spellID = UnitBuff(unit, i) - else - -- UnitDebuff => texture, stacks, debuffType, spellID - texture, stacks, _, current_spellID = UnitDebuff(unit, i) - end - remaining = nil - end - - if not texture then break end - - if current_spellID then - if searchID then - -- Spell ID matching: [mybuff:17941] - if current_spellID == searchID then - found = true - break - end - elseif searchName then - -- PERFORMANCE: Use cached lowercase spell name lookup - local lowerName = GetLowercaseSpellName(current_spellID) - if lowerName and lowerName == searchName then - found = true - break + -- Fast path: Use GetUnitField to search all 48 aura slots in 2 C-to-Lua calls + -- Only for non-player units (player uses GetPlayerAura which provides duration) + local skipSlowSearch = false + if not isPlayer then + local API = CleveRoids.NampowerAPI + if API and API.FindUnitAuraInfo then + local gufResult, gufSpellId, gufStacks, gufSlot = API.FindUnitAuraInfo(unit, searchID, searchName) + if gufResult ~= nil then -- GetUnitField was available and searched + skipSlowSearch = true + if gufResult then + if isbuff then + -- For buff checks: only accept buff slots (1-32) + if gufSlot <= 32 then + found = true + stacks = gufStacks or 0 + end + else + -- For debuff checks: accept any slot (debuffs overflow to buff slots) + found = true + stacks = gufStacks or 0 + end end end end - - i = i + 1 end - -- Overflow handling: when searching DEBUFFS on non-players, also scan BUFFS - if not isbuff and not isPlayer and not found and (searchID or searchName) then - i = 1 + -- Slow path: Per-slot iteration (player always, non-player when GetUnitField unavailable) + if not skipSlowSearch then + -- Primary search: BUFFS if isbuff==true, DEBUFFS if isbuff==false while true do local texture local current_spellID = nil - -- UnitBuff => texture, stacks, spellID - texture, stacks, current_spellID = UnitBuff(unit, i) + if isPlayer then + -- GetPlayerAura(index, isbuff) => texture, stacks, spellID, timeLeft + texture, stacks, current_spellID, remaining = CleveRoids.GetPlayerAura(i, isbuff) + else + if isbuff then + -- UnitBuff => texture, stacks, spellID + texture, stacks, current_spellID = UnitBuff(unit, i) + else + -- UnitDebuff => texture, stacks, debuffType, spellID + texture, stacks, _, current_spellID = UnitDebuff(unit, i) + end + remaining = nil + end + if not texture then break end if current_spellID then if searchID then + -- Spell ID matching: [mybuff:17941] if current_spellID == searchID then found = true break @@ -3202,6 +3248,37 @@ function CleveRoids.ValidateAura(unit, args, isbuff) i = i + 1 end + + -- Overflow handling: when searching DEBUFFS on non-players, also scan BUFFS + if not isbuff and not isPlayer and not found and (searchID or searchName) then + i = 1 + while true do + local texture + local current_spellID = nil + + -- UnitBuff => texture, stacks, spellID + texture, stacks, current_spellID = UnitBuff(unit, i) + if not texture then break end + + if current_spellID then + if searchID then + if current_spellID == searchID then + found = true + break + end + elseif searchName then + -- PERFORMANCE: Use cached lowercase spell name lookup + local lowerName = GetLowercaseSpellName(current_spellID) + if lowerName and lowerName == searchName then + found = true + break + end + end + end + + i = i + 1 + end + end end local ops = CleveRoids.operators @@ -3412,75 +3489,100 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- 2. Personal debuffs during the 0.2s pending delay after casting -- 3. Any debuffs not yet registered in tracking (edge cases) -- - -- For simple existence checks ([nodebuff]Spell), we want to find the debuff - -- regardless of whether it's in our tracking table. Time-remaining checks - -- will use tracking table data when available, but existence is from scan. - local isSimpleExistenceCheck = not args.operator and not args.amount + -- Includes stack/time checks (e.g., [debuff:Sunder_Armor>#3]) not just existence. + -- The isShared gate below ensures personal debuffs still require tracking table data. - if not found and CleveRoids.hasSuperwow and isSimpleExistenceCheck then + if not found and CleveRoids.hasSuperwow then -- FALLBACK: Only scan for SHARED debuffs (Sunder, Faerie Fire, etc.) -- Personal debuffs (Rip, Rake, Rupture, etc.) MUST be in tracking table -- to be considered "found" - this ensures [nodebuff] only finds YOUR debuffs -- after /reload when tracking is cleared. - -- Scan debuff slots - for i = 1, 16 do - local tex, debuffStacks, _, debuffSpellID = UnitDebuff(unit, i) - if not tex then break end - - if debuffSpellID then - local matched = false - if searchID then - matched = (debuffSpellID == searchID) - else - local baseName, fullName = GetSpellNames(debuffSpellID) - matched = baseName and (baseName == args.name or fullName == args.name) - end - if matched then - -- IMPORTANT: Only use fallback for SHARED debuffs - -- Personal debuffs must come from tracking table (caster check) - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(debuffSpellID) == false + -- Fast path: Use GetUnitField to search all 48 aura slots in 2 C-to-Lua calls + local API = CleveRoids.NampowerAPI + local searchNameLower = not searchID and args.name and _string_lower(args.name) or nil + if API and API.FindUnitAuraInfo then + local gufResult, gufSpellId, gufStacks, gufSlot = API.FindUnitAuraInfo(unit, searchID, searchNameLower) + if gufResult ~= nil then + -- GetUnitField was available and searched + if gufResult then + local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(gufSpellId) == false if isShared then found = true - texture = tex - stacks = debuffStacks or 0 - spellID = debuffSpellID - -- No duration tracking for shared debuffs + stacks = gufStacks or 0 + spellID = gufSpellId remaining = nil - break end end + else + -- GetUnitField unavailable, fall through to per-slot iteration + API = nil -- Signal slow path below end + else + API = nil -- Signal slow path below end - -- If still not found, check buff slots (overflow debuffs) - if not found then - for i = 1, 32 do - local tex, buffStacks, buffSpellID = UnitBuff(unit, i) + -- Slow path: Per-slot UnitDebuff + UnitBuff iteration (GetUnitField unavailable) + if not found and not API then + -- Scan debuff slots + for i = 1, 16 do + local tex, debuffStacks, _, debuffSpellID = UnitDebuff(unit, i) if not tex then break end - if buffSpellID then + if debuffSpellID then local matched = false if searchID then - matched = (buffSpellID == searchID) + matched = (debuffSpellID == searchID) else - local baseName, fullName = GetSpellNames(buffSpellID) + local baseName, fullName = GetSpellNames(debuffSpellID) matched = baseName and (baseName == args.name or fullName == args.name) end if matched then -- IMPORTANT: Only use fallback for SHARED debuffs - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false + -- Personal debuffs must come from tracking table (caster check) + local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(debuffSpellID) == false if isShared then found = true texture = tex - stacks = buffStacks or 0 - spellID = buffSpellID + stacks = debuffStacks or 0 + spellID = debuffSpellID + -- No duration tracking for shared debuffs remaining = nil break end end end end + + -- If still not found, check buff slots (overflow debuffs) + if not found then + for i = 1, 32 do + local tex, buffStacks, buffSpellID = UnitBuff(unit, i) + if not tex then break end + + if buffSpellID then + local matched = false + if searchID then + matched = (buffSpellID == searchID) + else + local baseName, fullName = GetSpellNames(buffSpellID) + matched = baseName and (baseName == args.name or fullName == args.name) + end + if matched then + -- IMPORTANT: Only use fallback for SHARED debuffs + local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false + if isShared then + found = true + texture = tex + stacks = buffStacks or 0 + spellID = buffSpellID + remaining = nil + break + end + end + end + end + end end end -- For player unit, use standard search (player only sees own debuffs on self) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index ed14097..d37a146 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -85,29 +85,341 @@ local VALID_COMMANDS = { ["/cleveroid"] = true, ["/cleveroidmacros"] = true, ["/macrocheck"] = true, - -- Chat / emotes / common WoW commands - ["/s"] = true, - ["/y"] = true, - ["/r"] = true, - ["/bg"] = true, - ["/e"] = true, - ["/w"] = true, - ["/g"] = true, - ["/p"] = true, - ["/invite"] = true, - ["/trade"] = true, + -- Turtle WoW / third-party addon commands ["/db"] = true, - ["/roll"] = true, - ["/bow"] = true, ["/rinse"] = true, ["/am"] = true, ["/aux"] = true, ["/instancetimers"] = true, ["/umacro"] = true, - ["/camp"] = true, - ["/logout"] = true, - ["/exit"] = true, + + -------------------------------------------------------------------------- + -- Standard WoW 1.12.1 Commands + -------------------------------------------------------------------------- + + -- Chat + ["/say"] = true, + ["/s"] = true, + ["/yell"] = true, + ["/y"] = true, + ["/shout"] = true, + ["/whisper"] = true, + ["/w"] = true, + ["/tell"] = true, + ["/reply"] = true, + ["/r"] = true, + ["/party"] = true, + ["/p"] = true, + ["/guild"] = true, + ["/g"] = true, + ["/officer"] = true, + ["/o"] = true, + ["/raid"] = true, + ["/ra"] = true, + ["/battleground"] = true, + ["/bg"] = true, + ["/emote"] = true, + ["/e"] = true, + ["/em"] = true, + ["/me"] = true, + ["/rw"] = true, + ["/announce"] = true, + + -- Channel + ["/join"] = true, + ["/leave"] = true, + ["/channel"] = true, + ["/chatlist"] = true, + ["/chatwho"] = true, + ["/chatinvite"] = true, + ["/ckick"] = true, + + -- Targeting + ["/tar"] = true, + ["/assist"] = true, + ["/a"] = true, + ["/targetenemy"] = true, + ["/targetfriend"] = true, + ["/targetnearestenemy"] = true, + ["/targetnearestfriend"] = true, + ["/targetlasttarget"] = true, + ["/targetlastenemy"] = true, + ["/targetlastfriend"] = true, + + -- Group / Raid + ["/invite"] = true, + ["/inv"] = true, + ["/uninvite"] = true, + ["/kick"] = true, ["/promote"] = true, + ["/leader"] = true, + ["/disband"] = true, + ["/trade"] = true, + ["/roll"] = true, + ["/random"] = true, + ["/raidinfo"] = true, + ["/readycheck"] = true, + ["/lfg"] = true, + ["/lfm"] = true, + + -- Loot + ["/masterloot"] = true, + ["/ffa"] = true, + ["/roundrobin"] = true, + ["/needbeforegreed"] = true, + ["/grouploot"] = true, + + -- Guild + ["/ginvite"] = true, + ["/guildinvite"] = true, + ["/gremove"] = true, + ["/guildremove"] = true, + ["/gpromote"] = true, + ["/guildpromote"] = true, + ["/gdemote"] = true, + ["/guilddemote"] = true, + ["/gquit"] = true, + ["/guildquit"] = true, + ["/gdisband"] = true, + ["/guilddisband"] = true, + ["/gmotd"] = true, + ["/guildmotd"] = true, + ["/ginfo"] = true, + ["/guildinfo"] = true, + ["/groster"] = true, + ["/guildroster"] = true, + ["/guildleader"] = true, + + -- System + ["/logout"] = true, + ["/camp"] = true, + ["/quit"] = true, + ["/exit"] = true, + ["/reload"] = true, + ["/reloadui"] = true, + ["/console"] = true, + ["/macro"] = true, + ["/played"] = true, + ["/time"] = true, + ["/who"] = true, + ["/afk"] = true, + ["/away"] = true, + ["/dnd"] = true, + ["/busy"] = true, + ["/help"] = true, + ["/pvp"] = true, + ["/combatlog"] = true, + ["/chatlog"] = true, + ["/clear"] = true, + ["/ignore"] = true, + ["/unignore"] = true, + ["/friend"] = true, + ["/friends"] = true, + ["/removefriend"] = true, + ["/gm"] = true, + ["/bug"] = true, + ["/suggest"] = true, + + -- Movement / Stance + ["/follow"] = true, + ["/f"] = true, + ["/dismount"] = true, + ["/cancelform"] = true, + + -- Pet (standard WoW extras) + ["/petabandon"] = true, + ["/petstay"] = true, + + -- Emotes + ["/agree"] = true, + ["/amaze"] = true, + ["/angry"] = true, + ["/apologize"] = true, + ["/applaud"] = true, + ["/attacktarget"] = true, + ["/bark"] = true, + ["/bashful"] = true, + ["/beckon"] = true, + ["/beg"] = true, + ["/bite"] = true, + ["/bleed"] = true, + ["/blink"] = true, + ["/blush"] = true, + ["/boggle"] = true, + ["/bonk"] = true, + ["/bored"] = true, + ["/bounce"] = true, + ["/bow"] = true, + ["/bravo"] = true, + ["/burp"] = true, + ["/bye"] = true, + ["/cackle"] = true, + ["/calm"] = true, + ["/cat"] = true, + ["/charge"] = true, + ["/cheer"] = true, + ["/chicken"] = true, + ["/chuckle"] = true, + ["/clap"] = true, + ["/cold"] = true, + ["/comfort"] = true, + ["/commend"] = true, + ["/confused"] = true, + ["/congratulate"] = true, + ["/congrats"] = true, + ["/cough"] = true, + ["/cower"] = true, + ["/crack"] = true, + ["/cringe"] = true, + ["/cry"] = true, + ["/cuddle"] = true, + ["/curious"] = true, + ["/curtsey"] = true, + ["/dance"] = true, + ["/disappointed"] = true, + ["/doom"] = true, + ["/drink"] = true, + ["/drool"] = true, + ["/duck"] = true, + ["/eat"] = true, + ["/embarrass"] = true, + ["/encourage"] = true, + ["/enemy"] = true, + ["/eye"] = true, + ["/fart"] = true, + ["/feast"] = true, + ["/fidget"] = true, + ["/flap"] = true, + ["/flee"] = true, + ["/flex"] = true, + ["/flirt"] = true, + ["/flop"] = true, + ["/gasp"] = true, + ["/gaze"] = true, + ["/giggle"] = true, + ["/glad"] = true, + ["/gloat"] = true, + ["/glare"] = true, + ["/golfclap"] = true, + ["/goodbye"] = true, + ["/greet"] = true, + ["/grin"] = true, + ["/groan"] = true, + ["/grovel"] = true, + ["/growl"] = true, + ["/guffaw"] = true, + ["/hail"] = true, + ["/happy"] = true, + ["/healme"] = true, + ["/hello"] = true, + ["/helpme"] = true, + ["/hug"] = true, + ["/hungry"] = true, + ["/impatient"] = true, + ["/incoming"] = true, + ["/insult"] = true, + ["/introduce"] = true, + ["/jk"] = true, + ["/kiss"] = true, + ["/kneel"] = true, + ["/laugh"] = true, + ["/laydown"] = true, + ["/lick"] = true, + ["/listen"] = true, + ["/lost"] = true, + ["/love"] = true, + ["/massage"] = true, + ["/moan"] = true, + ["/mock"] = true, + ["/moo"] = true, + ["/moon"] = true, + ["/mourn"] = true, + ["/no"] = true, + ["/nod"] = true, + ["/nosepick"] = true, + ["/oom"] = true, + ["/openfire"] = true, + ["/panic"] = true, + ["/pat"] = true, + ["/peek"] = true, + ["/peer"] = true, + ["/peon"] = true, + ["/pest"] = true, + ["/pick"] = true, + ["/pinch"] = true, + ["/pity"] = true, + ["/plead"] = true, + ["/point"] = true, + ["/poke"] = true, + ["/ponder"] = true, + ["/pounce"] = true, + ["/praise"] = true, + ["/pray"] = true, + ["/purr"] = true, + ["/puzzle"] = true, + ["/question"] = true, + ["/raise"] = true, + ["/rasp"] = true, + ["/ready"] = true, + ["/regret"] = true, + ["/roar"] = true, + ["/rofl"] = true, + ["/rude"] = true, + ["/ruffle"] = true, + ["/sad"] = true, + ["/salute"] = true, + ["/scared"] = true, + ["/scratch"] = true, + ["/sexy"] = true, + ["/shake"] = true, + ["/shimmy"] = true, + ["/shiver"] = true, + ["/shoo"] = true, + ["/shrug"] = true, + ["/shy"] = true, + ["/sigh"] = true, + ["/silly"] = true, + ["/sit"] = true, + ["/slap"] = true, + ["/sleep"] = true, + ["/smile"] = true, + ["/smirk"] = true, + ["/snarl"] = true, + ["/snicker"] = true, + ["/sniff"] = true, + ["/sob"] = true, + ["/soothe"] = true, + ["/sorry"] = true, + ["/spit"] = true, + ["/stand"] = true, + ["/stare"] = true, + ["/stink"] = true, + ["/strong"] = true, + ["/surprised"] = true, + ["/surrender"] = true, + ["/tap"] = true, + ["/tease"] = true, + ["/thank"] = true, + ["/thirsty"] = true, + ["/threaten"] = true, + ["/tickle"] = true, + ["/tired"] = true, + ["/train"] = true, + ["/truce"] = true, + ["/twiddle"] = true, + ["/veto"] = true, + ["/victory"] = true, + ["/violin"] = true, + ["/volunteer"] = true, + ["/wait"] = true, + ["/wave"] = true, + ["/welcome"] = true, + ["/whine"] = true, + ["/whistle"] = true, + ["/wink"] = true, + ["/work"] = true, + ["/yawn"] = true, + ["/yes"] = true, } -- Commands that can have conditionals without actions @@ -122,6 +434,10 @@ local COMMANDS_NO_ACTION_NEEDED = { ["/target"] = true, ["/cleartarget"] = true, ["/focus"] = true, + ["/follow"] = true, + ["/f"] = true, + ["/dismount"] = true, + ["/cancelform"] = true, ["/startattack"] = true, ["/stopattack"] = true, ["/stopcasting"] = true, diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 4f68705..1a8aca2 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -69,11 +69,23 @@ - BUFF_UPDATE_DURATION_SELF / DEBUFF_UPDATE_DURATION_SELF events - GetPlayerAuraDuration(auraSlot) - Get duration info for player aura by slot + Spell Miss Events (v2.31+): + - SPELL_MISS_SELF / SPELL_MISS_OTHER - Spell miss/resist/immune/dodge/etc. + - GetSpellPower([mode]) - Player mod damage done for all 7 schools + + Aura Event State Parameter (v2.32+): + - Buff/debuff events include 7th `state` parameter (0=added, 1=removed, 2=modified) + - REMOVED events now properly fire for stack decrements (state=2) + + Duration Event SpellId (v2.33+): + - BUFF/DEBUFF_UPDATE_DURATION_SELF include spellId as 4th parameter + - GetCastInfo() guid field fix (no code change needed) + Settings Integration: - Reads from NampowerSettings addon when available - Falls back to CVars when addon not present - Current version: v2.30.0 + Current version: v2.33.0 ]] local _G = _G or getfenv(0) @@ -219,6 +231,17 @@ API.VERSION_REQUIREMENTS = { ["AuraSlotParameter"] = { 2, 30, 0 }, ["AuraDurationEvents"] = { 2, 30, 0 }, ["GetPlayerAuraDuration"] = { 2, 30, 0, "GetPlayerAuraDuration" }, + + -- v2.31+ - Spell miss events and spell power query + ["SpellMissEvents"] = { 2, 31, 0 }, -- SPELL_MISS_SELF/OTHER events + ["GetSpellPower"] = { 2, 31, 0, "GetSpellPower" }, + + -- v2.32+ - Aura event state parameter and stack removal fix + ["AuraEventState"] = { 2, 32, 0 }, + + -- v2.33+ - UPDATE_DURATION spellId parameter and GetCastInfo guid fix + ["DurationEventSpellId"] = { 2, 33, 0 }, + ["GetCastInfoGuidFix"] = { 2, 33, 0 }, } -- Check if a specific feature is available @@ -348,6 +371,17 @@ local function InitializeFeatures() f.hasAuraDurationEvents = API.HasFeature("AuraDurationEvents") f.hasGetPlayerAuraDuration = API.HasFeature("GetPlayerAuraDuration") + -- v2.31+ Spell miss events and spell power + f.hasSpellMissEvents = API.HasFeature("SpellMissEvents") + f.hasGetSpellPower = API.HasFeature("GetSpellPower") + + -- v2.32+ Aura event state parameter + f.hasAuraEventState = API.HasFeature("AuraEventState") + + -- v2.33+ Duration event spellId and GetCastInfo guid fix + f.hasDurationEventSpellId = API.HasFeature("DurationEventSpellId") + f.hasGetCastInfoGuidFix = API.HasFeature("GetCastInfoGuidFix") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() @@ -1041,6 +1075,61 @@ function API.UnitHasAura(unitToken, spellId) return false end +-- Get unit's aura stack counts (auraApplications field) +-- Pass copy=1 to get an independent copy safe for storage (v2.20+) +function API.GetUnitAuraApplications(unitToken, copy) + return API.GetUnitFieldValue(unitToken, "auraApplications", copy) +end + +-- Composite helper: Search unit's auras for a spell by ID or lowercase name, +-- returning match info with stack count from auraApplications. +-- +-- Parameters: +-- unitToken - Unit token (e.g., "target", "focus") +-- searchSpellId - Spell ID to match (number), or nil for name search +-- searchNameLower - Lowercase spell name to match (string), or nil for ID search +-- +-- Returns: +-- nil - GetUnitField unavailable (caller should use slow path) +-- false - Searched but aura not found +-- true, matchedSpellId, stacks, slotIndex - Found; slotIndex 1-32 = buff, 33-48 = debuff +function API.FindUnitAuraInfo(unitToken, searchSpellId, searchNameLower) + if not unitToken then return nil end + if not searchSpellId and not searchNameLower then return nil end + + local auras = API.GetUnitAuras(unitToken) + if not auras then return nil end -- GetUnitField unavailable + + local applications = API.GetUnitAuraApplications(unitToken) + + for i = 1, 48 do + local auraId = auras[i] + if auraId and auraId ~= 0 then + local matched = false + + if searchSpellId then + matched = (auraId == searchSpellId) + elseif searchNameLower then + local name = SpellInfo and SpellInfo(auraId) + if name then + -- Strip rank suffix for consistent matching + local baseName = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + if string.lower(baseName) == searchNameLower then + matched = true + end + end + end + + if matched then + local stacks = applications and applications[i] or 0 + return true, auraId, stacks, i + end + end + end + + return false +end + -------------------------------------------------------------------------------- -- INVENTORY & EQUIPMENT API (v2.18+) -------------------------------------------------------------------------------- @@ -1930,7 +2019,8 @@ API.CAST_TYPE = { } -- Buff/debuff event names (v2.18+) --- Parameters: guid, slot, spellId, stackCount, auraLevel (v2.20+), auraSlot (v2.30+, raw 0-based) +-- Parameters: guid, slot, spellId, stackCount, auraLevel (v2.20+), auraSlot (v2.30+, raw 0-based), +-- state (v2.32+, 0=added/1=removed/2=modified) API.AURA_EVENTS = { "BUFF_ADDED_SELF", "BUFF_REMOVED_SELF", @@ -1958,9 +2048,17 @@ API.AURA_CAP_STATUS = { BOTH_FULL = 3, } +-- Aura event state constants (v2.32+) +-- The 7th parameter on BUFF/DEBUFF_ADDED/REMOVED events +API.AURA_STATE = { + ADDED = 0, -- Newly added aura + REMOVED = 1, -- Fully removed aura + MODIFIED = 2, -- Stack change (ADDED if increased, REMOVED if decreased) +} + -- Aura duration update event names (v2.30+) -- Fires when a buff/debuff duration is refreshed on the player --- Parameters: auraSlot (0-based raw slot index) +-- Parameters: auraSlot (0-based raw slot index), durationMs, expirationTimeMs, spellId (v2.33+) API.AURA_DURATION_EVENTS = { "BUFF_UPDATE_DURATION_SELF", -- Player buff duration refreshed "DEBUFF_UPDATE_DURATION_SELF", -- Player debuff duration refreshed @@ -2860,5 +2958,29 @@ function API.ClearSpellTypeCache() API.spellTypeCache = {} end +-------------------------------------------------------------------------------- +-- SPELL MISS CONSTANTS (v2.31+) +-------------------------------------------------------------------------------- + +-- MissInfo enum values from SPELL_MISS_SELF/OTHER events +API.MISS_INFO = { + NONE = 0, MISS = 1, RESIST = 2, DODGE = 3, PARRY = 4, BLOCK = 5, + EVADE = 6, IMMUNE = 7, IMMUNE2 = 8, DEFLECT = 9, ABSORB = 10, REFLECT = 11, +} + +-------------------------------------------------------------------------------- +-- SPELL POWER QUERY (v2.31+) +-------------------------------------------------------------------------------- + +-- Get spell power for all 7 damage schools (v2.31+) +-- mode: optional mode parameter passed to GetSpellPower +-- Returns: physical, holy, fire, nature, frost, shadow, arcane (or nil if unavailable) +function API.GetSpellPower(mode) + if not API.features.hasGetSpellPower or not _G.GetSpellPower then + return nil, nil, nil, nil, nil, nil, nil + end + return _G.GetSpellPower(mode) +end + -- Expose API globally for other addons _G.CleveRoidsNampowerAPI = API diff --git a/README.md b/README.md index ac7d33e..12238e6 100644 --- a/README.md +++ b/README.md @@ -346,7 +346,7 @@ Scans enemies and soft-casts without changing your target. Requires UnitXP_SP3. | `mainhand` / `mh` | Main-hand swing (lastswing only) | | `hit` | Successful hit (not miss/dodge/parry) | -**Stat Types** (for `[stat]`): `str`, `agi`, `stam`, `int`, `spi`, `ap`, `rap`, `healing`, `armor`, `defense`, `arcane_power`, `fire_power`, `frost_power`, `nature_power`, `shadow_power`, `arcane_res`, `fire_res`, `frost_res`, `nature_res`, `shadow_res` +**Stat Types** (for `[stat]`): `str`, `agi`, `stam`, `int`, `spi`, `ap`, `rap`, `healing`, `spell_power` (highest across all schools), `arcane_power`, `fire_power`, `frost_power`, `nature_power`, `shadow_power`, `armor`, `defense`, `arcane_res`, `fire_res`, `frost_res`, `nature_res`, `shadow_res` **Aura Capacity:** Player: 32 buffs, 16 debuffs. NPCs: 16 debuff slots + 32 overflow = 48 total. diff --git a/Utility.lua b/Utility.lua index bb3b8ab..7f034e4 100644 --- a/Utility.lua +++ b/Utility.lua @@ -4511,8 +4511,9 @@ ev:SetScript("OnEvent", function() -- Schedule immunity verification after a short delay -- This gives combat log time to report the miss reason (immune/reflect/evade) -- Optimization: Use shorter delay with Nampower v2.26+ since AURA_CAST events are faster - if isOurs and targetName then + if isOurs and targetName and not CleveRoids.usingSpellMissEvents then -- Delay: 150ms with Nampower v2.26+ (AURA_CAST confirms faster), 300ms fallback + -- Skipped when SPELL_MISS events handle immunity detection directly (v2.31+) local verifyDelay = lib.hasUnitDiedEvent and 0.15 or 0.3 local verifyFrame = CreateFrame("Frame") @@ -4760,6 +4761,7 @@ ev:SetScript("OnEvent", function() local spellId = arg3 local stacks = arg4 or 1 local auraSlot = arg6 -- v2.30+: raw 0-based aura slot index (stable, no shifting) + local state = arg7 -- v2.32+: 0=added, 1=removed, 2=modified (stack increase) if not guid or not slot or not spellId then return end guid = CleveRoids.NormalizeGUID(guid) @@ -4770,6 +4772,16 @@ ev:SetScript("OnEvent", function() local _, playerGUID = UnitExists("player") local isOurs = lib.ownDebuffs[guid] and lib.ownDebuffs[guid][spellName] ~= nil + -- v2.32+: state == 2 means stack increase - update stacks on existing entry + if state == 2 and isOurs then + lib.ownDebuffs[guid][spellName].stacks = stacks + lib.ownDebuffs[guid][spellName].slot = slot + if auraSlot then + lib.ownDebuffs[guid][spellName].auraSlot = auraSlot + end + return + end + -- Update slot info in ownDebuffs if this is our debuff if isOurs then lib.ownDebuffs[guid][spellName].slot = slot @@ -4813,13 +4825,24 @@ ev:SetScript("OnEvent", function() local guid = arg1 local slot = arg2 local spellId = arg3 - local auraSlot = arg6 -- v2.30+: raw 0-based aura slot index (stable, no shifting) + local stacks = arg4 -- v2.32+: current stack count after decrement + local auraSlot = arg6 -- v2.30+: raw 0-based aura slot index (stable, no shifting) + local state = arg7 -- v2.32+: 0=added, 1=removed, 2=modified (stack decrease) if not guid or not slot then return end guid = CleveRoids.NormalizeGUID(guid) local spellName = spellId and SpellInfo and SpellInfo(spellId) + -- v2.32+: state == 2 means stack decrease - update stacks, don't remove + if state == 2 and stacks and stacks > 0 then + if spellName and lib.ownDebuffs[guid] and lib.ownDebuffs[guid][spellName] then + lib.ownDebuffs[guid][spellName].stacks = stacks + end + return + end + + -- Full removal (state == 1, or nil for pre-v2.32) -- Remove from ownDebuffs if present if spellName and lib.ownDebuffs[guid] and lib.ownDebuffs[guid][spellName] then lib.ownDebuffs[guid][spellName] = nil @@ -8188,6 +8211,313 @@ function CleveRoids.ProcessAutoAttackEvent(isPlayerAttacker, attackerGuid, targe end end +-- NAMPOWER v2.31+ SPELL_MISS EVENT HANDLERS +-- Provides direct miss type information (immune, dodge, resist, etc.) with spellId + targetGuid, +-- eliminating the need for combat log text parsing and time-delayed correlation. + +-- MissInfo constants (local copies for performance) +local MISSINFO_MISS = 1 +local MISSINFO_RESIST = 2 +local MISSINFO_DODGE = 3 +local MISSINFO_PARRY = 4 +local MISSINFO_BLOCK = 5 +local MISSINFO_EVADE = 6 +local MISSINFO_IMMUNE = 7 +local MISSINFO_IMMUNE2 = 8 +local MISSINFO_REFLECT = 11 + +-- Track if we're using SPELL_MISS events (set during initialization) +CleveRoids.usingSpellMissEvents = false + +-- Process SPELL_MISS_SELF: player's spell missed/was resisted/immune/etc. +-- Parameters: spellId, targetGuid, missInfo, [unused] +local function ProcessSpellMissSelf(spellId, targetGuid, missInfo) + if not spellId or not targetGuid or not missInfo then return end + + -- Resolve target name from GUID cache or current target + local targetName = lib.guidToName[targetGuid] + if not targetName then + local _, currentTargetGUID = UnitExists("target") + if CleveRoids.NormalizeGUID(currentTargetGUID) == CleveRoids.NormalizeGUID(targetGuid) then + targetName = UnitName("target") + if targetName then + lib.guidToName[targetGuid] = targetName + end + end + end + + -- Resolve spell name + local spellName = SpellInfo and SpellInfo(spellId) + local baseName = spellName and string.gsub(spellName, "%s*%(.-%)%s*$", "") or nil + + -- ======================================================================== + -- IMMUNE / IMMUNE2 (missInfo 7/8) + -- ======================================================================== + if missInfo == MISSINFO_IMMUNE or missInfo == MISSINFO_IMMUNE2 then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff6600[SPELL_MISS]|r IMMUNE - %s on %s (spellId=%d, missInfo=%d)", + spellName or "?", targetName or "?", spellId, missInfo) + ) + end + + if targetName and spellName then + -- Cancel any pending delayed verification immediately + CancelPendingVerification(targetName, spellName) + + -- Skip split CC spells (physical damage + resistable CC) + if SPLIT_CC_SPELLS[spellId] or (baseName and SPLIT_CC_SPELL_NAMES[baseName]) then + if CleveRoids.debug then + CleveRoids.Print("|cff00aaff[SPELL_MISS Split CC Skip]|r " .. spellName .. " CC immune on " .. targetName) + end + return + end + + -- Skip if target has temporary immunity buff (Divine Shield, etc.) + if UnitExists("target") and UnitName("target") == targetName then + local immunityBuff = HasImmunityGrantingBuff("target") + if immunityBuff then + if CleveRoids.debug then + CleveRoids.Print("|cff00aaff[SPELL_MISS Temp Skip]|r " .. targetName .. " has " .. immunityBuff) + end + return + end + + -- Check for conditional buff-based immunity + local buffs = GetUnitBuffs("target") + 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 + -- Check CC immunity first + local ccType = GetSpellCCType(spellId) + if ccType then + RecordCCImmunity(targetName, ccType, singleBuff, spellName) + else + RecordImmunity(targetName, spellName, singleBuff, spellId) + end + return + end + end + end + + -- No buff detected - record as permanent immunity + local ccType = GetSpellCCType(spellId) + if ccType then + RecordCCImmunity(targetName, ccType, nil, spellName) + else + RecordImmunity(targetName, spellName, nil, spellId) + end + end + + -- Populate backward-compat tables for SPELL_GO correlation + if targetName and spellName then + lib.recentCombatLogReasons[targetName] = lib.recentCombatLogReasons[targetName] or {} + lib.recentCombatLogReasons[targetName][spellName] = { + time = GetTime(), + reason = "immune", + } + end + if targetGuid and spellName then + lib.recentMisses[targetGuid] = lib.recentMisses[targetGuid] or {} + lib.recentMisses[targetGuid][spellName] = { + time = GetTime(), + spellId = spellId, + targetGuid = targetGuid, + targetName = targetName, + reason = "immune", + } + end + return + end + + -- ======================================================================== + -- REFLECT (missInfo 11) + -- ======================================================================== + if missInfo == MISSINFO_REFLECT then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff00ff[SPELL_MISS]|r REFLECT - %s reflected by %s", + spellName or "?", targetName or "?") + ) + end + + if targetName and spellName then + lib.recentReflects = lib.recentReflects or {} + lib.recentReflects[targetName] = lib.recentReflects[targetName] or {} + lib.recentReflects[targetName][spellName] = { + time = GetTime(), + spellId = spellId, + } + + lib.recentCombatLogReasons[targetName] = lib.recentCombatLogReasons[targetName] or {} + lib.recentCombatLogReasons[targetName][spellName] = { + time = GetTime(), + reason = "reflect", + } + end + return + end + + -- ======================================================================== + -- EVADE (missInfo 6) + -- ======================================================================== + if missInfo == MISSINFO_EVADE then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffaaaaaa[SPELL_MISS]|r EVADE - %s evaded by %s (not immunity)", + spellName or "?", targetName or "?") + ) + end + + if targetName and spellName then + lib.recentCombatLogReasons[targetName] = lib.recentCombatLogReasons[targetName] or {} + lib.recentCombatLogReasons[targetName][spellName] = { + time = GetTime(), + reason = "evade", + } + end + return + end + + -- ======================================================================== + -- DODGE (missInfo 3) - Overpower proc for yellow abilities + -- ======================================================================== + if missInfo == MISSINFO_DODGE then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[SPELL_MISS]|r DODGE - %s dodged by %s - Overpower proc", + spellName or "?", targetName or "?") + ) + end + + -- Overpower proc (enemy dodged our yellow ability) + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Overpower"] then + CleveRoids.SetReactiveProc("Overpower", 4.0, targetGuid) + end + + -- Update LastSwing tracking + if CleveRoids.LastSwing then + CleveRoids.LastSwing.timestamp = GetTime() + CleveRoids.LastSwing.hitInfo = HITINFO_MISS + CleveRoids.LastSwing.victimState = VICTIMSTATE_DODGE + CleveRoids.LastSwing.damage = 0 + CleveRoids.LastSwing.blockedAmount = 0 + CleveRoids.LastSwing.absorbAmount = 0 + CleveRoids.LastSwing.resistAmount = 0 + CleveRoids.LastSwing.targetGuid = targetGuid + end + + CleveRoids.QueueActionUpdate() + return + end + + -- ======================================================================== + -- RESIST (missInfo 2) - Full resist tracking + -- ======================================================================== + if missInfo == MISSINFO_RESIST then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff9900[SPELL_MISS]|r RESIST - %s fully resisted by %s", + spellName or "?", targetName or "?") + ) + end + + CleveRoids.SetResistState("full", targetGuid) + return + end + + -- ======================================================================== + -- PARRY (missInfo 4) - Populate combat log reasons for completeness + -- ======================================================================== + if missInfo == MISSINFO_PARRY then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff8888ff[SPELL_MISS]|r PARRY - %s parried by %s", + spellName or "?", targetName or "?") + ) + end + + -- Update LastSwing tracking for parried yellow abilities + if CleveRoids.LastSwing then + CleveRoids.LastSwing.timestamp = GetTime() + CleveRoids.LastSwing.hitInfo = HITINFO_MISS + CleveRoids.LastSwing.victimState = VICTIMSTATE_PARRY + CleveRoids.LastSwing.damage = 0 + CleveRoids.LastSwing.blockedAmount = 0 + CleveRoids.LastSwing.absorbAmount = 0 + CleveRoids.LastSwing.resistAmount = 0 + CleveRoids.LastSwing.targetGuid = targetGuid + end + + CleveRoids.QueueActionUpdate() + return + end +end + +-- Process SPELL_MISS_OTHER: another unit's spell missed (player might be the target) +-- Parameters: spellId, casterGuid, targetGuid, missInfo +local function ProcessSpellMissOther(spellId, casterGuid, targetGuid, missInfo) + if not targetGuid or not missInfo then return end + + -- Only care when player is the target (victim) + local _, playerGUID = UnitExists("player") + if not playerGUID or targetGuid ~= playerGUID then return end + + -- DODGE: Enemy spell dodged by player → Revenge proc + if missInfo == MISSINFO_DODGE then + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Revenge"] then + CleveRoids.SetReactiveProc("Revenge", 4.0, nil) + CleveRoids.QueueActionUpdate() + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[SPELL_MISS]|r Revenge proc - player dodged enemy spell") + end + end + end + + -- PARRY: Enemy spell parried by player → Riposte + Revenge proc + if missInfo == MISSINFO_PARRY then + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Riposte"] then + CleveRoids.SetReactiveProc("Riposte", 4.0, casterGuid) + CleveRoids.QueueActionUpdate() + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[SPELL_MISS]|r Riposte proc - player parried enemy spell") + end + end + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Revenge"] then + CleveRoids.SetReactiveProc("Revenge", 4.0, nil) + CleveRoids.QueueActionUpdate() + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[SPELL_MISS]|r Revenge proc - player parried enemy spell") + end + end + end + + -- BLOCK: Enemy spell blocked by player → Revenge proc + if missInfo == MISSINFO_BLOCK then + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Revenge"] then + CleveRoids.SetReactiveProc("Revenge", 4.0, nil) + CleveRoids.QueueActionUpdate() + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[SPELL_MISS]|r Revenge proc - player blocked enemy spell") + end + end + end +end + -- Register combat log event for reactive proc tracking (FALLBACK) -- CHAT_MSG_SPELL_SELF_DAMAGE is needed for yellow ability dodge/parry/block detection -- (e.g., "Your Mortal Strike was dodged by Target.") - only auto-attack dodges come through COMBAT_SELF_MISSES @@ -8221,6 +8551,22 @@ if GetNampowerVersion then end end +-- Check for Nampower v2.31+ SPELL_MISS events (direct miss type information) +local hasSpellMissEvents = false +if GetNampowerVersion then + local npMajor, npMinor = GetNampowerVersion() + if npMajor > 2 or (npMajor == 2 and npMinor >= 31) then + hasSpellMissEvents = true + CleveRoids.usingSpellMissEvents = true + reactiveFrame:RegisterEvent("SPELL_MISS_SELF") + reactiveFrame:RegisterEvent("SPELL_MISS_OTHER") + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Using SPELL_MISS events for immunity/reactive detection (v2.31+)") + end + end +end + -- Check for Nampower v2.25+ SPELL_GO events (LastSwing tracking for yellow attack misses) -- NOTE: SPELL_GO only provides hit/miss counts, not miss types, so Overpower still needs -- combat log text parsing. SPELL_GO is used as a gate to avoid parsing combat log on every @@ -8261,6 +8607,21 @@ else end reactiveFrame:SetScript("OnEvent", function() + -- ======================================================================== + -- NAMPOWER v2.31+ SPELL_MISS EVENTS (preferred when available) + -- ======================================================================== + if event == "SPELL_MISS_SELF" then + -- Player's spell missed: arg1=spellId, arg2=targetGuid, arg3=missInfo + ProcessSpellMissSelf(arg1, arg2, arg3) + return + end + + if event == "SPELL_MISS_OTHER" then + -- Other unit's spell missed: arg1=spellId, arg2=casterGuid, arg3=targetGuid, arg4=missInfo + ProcessSpellMissOther(arg1, arg2, arg3, arg4) + return + end + -- ======================================================================== -- NAMPOWER v2.24+ AUTO_ATTACK EVENTS (preferred when available) -- ======================================================================== @@ -8358,7 +8719,10 @@ reactiveFrame:SetScript("OnEvent", function() -- Gate combat log parsing: set miss window so CHAT_MSG_SPELL_SELF_DAMAGE -- only calls ParseReactiveCombatLog when we know a yellow miss occurred. -- This avoids parsing every successful spell hit message. - CleveRoids._yellowMissWindow = GetTime() + -- Skip when SPELL_MISS handles dodge/parry detection directly (v2.31+) + if not hasSpellMissEvents then + CleveRoids._yellowMissWindow = GetTime() + end if CleveRoids.debug then local spellName = SpellInfo and SpellInfo(spellId) or tostring(spellId) @@ -8380,6 +8744,10 @@ reactiveFrame:SetScript("OnEvent", function() -- distinguish dodge (Overpower) from parry/block/resist. But we only parse when SPELL_GO -- told us a miss occurred, avoiding overhead on every successful spell hit. if event == "CHAT_MSG_SPELL_SELF_DAMAGE" then + -- With SPELL_MISS events (v2.31+): dodge/parry/block handled directly, skip combat log + if hasSpellMissEvents then + return + end -- Yellow ability avoidance (e.g., "Your Mortal Strike was dodged by Target") -- With SPELL_GO events: only parse when a yellow miss was recently detected. -- Without SPELL_GO: always parse (fallback for older Nampower).