From 4c00ade625b9bf88a453d433262bebe26232c1be Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sat, 7 Feb 2026 12:54:34 -0500 Subject: [PATCH 1/3] enhance immunity tracking with SPELL_GO cross-reference and improve combo point capture Use SPELL_GO hit/miss data to skip redundant debuff scanning for CC/shared immunity verification. Add stale entry cleanup (>2s) for pending debuffs. Capture combo points at SPELL_CAST_EVENT (client-side, pre-server) for reliable finisher duration tracking. Add SPELL_GO reactive ability detection for yellow attack dodges replacing combat log text parsing. --- ComboPointTracker.lua | 14 ++- Conditionals.lua | 8 +- Core.lua | 36 ++++++ Utility.lua | 276 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 297 insertions(+), 37 deletions(-) diff --git a/ComboPointTracker.lua b/ComboPointTracker.lua index a670a0a..8219c67 100644 --- a/ComboPointTracker.lua +++ b/ComboPointTracker.lua @@ -467,8 +467,18 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID) -- If combo points are 0, try multiple fallback sources if comboPoints == 0 then - -- First, try lastComboPoints (don't reset immediately - let it persist) - if CleveRoids.lastComboPoints > 0 then + -- PRIMARY: Check SPELL_CAST_EVENT capture (most reliable - client-side pre-server) + local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellID] + if pending and pending.comboPoints and pending.comboPoints > 0 then + comboPoints = pending.comboPoints + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff88[TrackComboByID]|r Using SPELL_CAST_EVENT CP: %d for spell ID %d", + comboPoints, spellID) + ) + end + -- Fallback: lastComboPoints (don't reset immediately - let it persist) + elseif CleveRoids.lastComboPoints > 0 then comboPoints = CleveRoids.lastComboPoints -- Don't reset here - let it persist for multiple events if CleveRoids.debug then diff --git a/Conditionals.lua b/Conditionals.lua index 4be9e92..ce4c8e6 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -867,10 +867,16 @@ local HITINFO_GLANCING = 16384 -- 0x4000 local HITINFO_CRUSHING = 32768 -- 0x8000 local HITINFO_LEFTSWING = 4 -- 0x4 (Off-hand attack) --- VictimState values +-- VictimState values (from AUTO_ATTACK / SPELL_GO events) +local VICTIMSTATE_UNAFFECTED = 0 -- Generic miss (seen with HITINFO_MISS) +local VICTIMSTATE_NORMAL = 1 -- Hit landed local VICTIMSTATE_DODGE = 2 local VICTIMSTATE_PARRY = 3 +local VICTIMSTATE_INTERRUPT = 4 local VICTIMSTATE_BLOCKS = 5 +local VICTIMSTATE_EVADES = 6 +local VICTIMSTATE_IS_IMMUNE = 7 +local VICTIMSTATE_DEFLECTS = 8 -- Aura cap status bitfield local AURA_CAP_BUFF_FULL = 1 diff --git a/Core.lua b/Core.lua index 4b7916f..ec809e7 100644 --- a/Core.lua +++ b/Core.lua @@ -5533,6 +5533,8 @@ function CleveRoids.Frame:SPELL_CAST_EVENT() if event == "SPELL_CAST_EVENT" then local success = arg1 local spellId = arg2 + local castType = arg3 + local targetGuid = arg4 -- BUGFIX: Update casting state on spell cast events (for [casting] conditional) if CleveRoids.UpdateCastingState then @@ -5542,6 +5544,8 @@ function CleveRoids.Frame:SPELL_CAST_EVENT() if success == 1 then CleveRoids.lastCastSpell = { spellId = spellId, + castType = castType, + targetGuid = targetGuid, timestamp = GetTime() } if SpellInfo then @@ -5550,6 +5554,38 @@ function CleveRoids.Frame:SPELL_CAST_EVENT() CleveRoids.lastCastSpell.spellName = name end end + + -- Track pending cast for SPELL_GO correlation (reactive ability detection) + -- Keyed by spellId so concurrent casts don't overwrite each other + CleveRoids.pendingCasts = CleveRoids.pendingCasts or {} + CleveRoids.pendingCasts[spellId] = { + castType = castType, + targetGuid = targetGuid, + timestamp = GetTime(), + comboPoints = nil, + } + + -- Capture combo points NOW (before server consumes them) + -- SPELL_CAST_EVENT fires client-side, so CP are guaranteed available + if (CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellId)) or + (CleveRoids.FerociousBiteSpellIDs and CleveRoids.FerociousBiteSpellIDs[spellId]) then + local cp = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0 + if cp > 0 then + CleveRoids.pendingCasts[spellId].comboPoints = cp + if CleveRoids.debug then + local castSpellName = SpellInfo and SpellInfo(spellId) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff88[SPELL_CAST_EVENT]|r Captured %d CP for %s (ID:%d)", + cp, castSpellName, spellId) + ) + end + end + end + else + -- Cast failed - clear pending entry for this spell + if CleveRoids.pendingCasts then + CleveRoids.pendingCasts[spellId] = nil + end end end end diff --git a/Utility.lua b/Utility.lua index e34f93c..175f6cb 100644 --- a/Utility.lua +++ b/Utility.lua @@ -3280,7 +3280,17 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Check after delay: 0.4s for hidden CC (no visible debuff), 0.2s for normal CC -- Hidden CC (e.g., Pounce stun) needs longer delay to wait for "afflicted by" messages local verifyDelay = pending.isHiddenCC and 0.4 or 0.2 - if elapsed >= verifyDelay then + + -- Drop stale entries (>2s) without recording immunity - inconclusive due to severe lag + if elapsed > 2.0 then + if debug then + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffff6600[CC Stale]|r Dropping stale CC entry for %s on %s (%.1fs old)", + pending.ccType or "CC", pending.targetName or "Unknown", elapsed) + ) + end + -- Don't add to newPendingList - silently discard + elseif elapsed >= verifyDelay then local ccVerified = false local totalDebuffs = 0 @@ -3290,10 +3300,24 @@ delayedTrackingFrame:SetScript("OnUpdate", function() ccVerified = true end + -- SPELL_GO early-exit: Use definitive hit/miss data when available + -- This is faster and more reliable than debuff scanning, and handles debuff cap correctly + if not ccVerified and pending.spellGoHit then + ccVerified = true + if debug then + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cff00ff00[CC Verified via SPELL_GO]|r %s landed on %s - skipping debuff scan", + pending.ccType or "CC", pending.targetName or "Unknown") + ) + end + end + -- CC IMMUNITY VERIFICATION: Check if CC effect actually landed -- Uses hybrid approach: direct spell ID match OR mechanic-based validation -- (CC debuff IDs often differ from cast IDs, e.g., Pounce cast ≠ Pounce Stun debuff) - if not ccVerified and hasSuperwow and pending.targetGUID then + -- Guard: skip debuff scanning if SPELL_GO already determined outcome + if not ccVerified and not pending.spellGoHit and not pending.spellGoMissed + and hasSuperwow and pending.targetGUID then -- Skip verification if target is dead (debuffs are removed on death) if _UnitIsDead(pending.targetGUID) then ccVerified = true -- Assume CC landed, can't verify on dead target @@ -3350,6 +3374,13 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end end + if not ccVerified and pending.spellGoMissed and debug then + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffff6600[CC Missed via SPELL_GO]|r %s missed on %s - checking immunity", + pending.ccType or "CC", pending.targetName or "Unknown") + ) + end + -- If CC didn't land, check if it's immunity or debuff cap if not ccVerified and not _UnitIsDead(pending.targetGUID) then -- totalDebuffs already counted above, reuse it @@ -3392,7 +3423,8 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- SPLIT CC SPELLS: Skip immunity recording for spells with physical damage + resistable CC -- (e.g., Master Strike) - physical damage lands but CC can be resisted independently - local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] + -- spellGoHit: SPELL_GO confirmed hit, so CC was resisted independently (not true immunity) + local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] or pending.spellGoHit if isSplitCCSpell then if debug then local spellNameDebug = pending.spellName or (_SpellInfo(pending.spellID) or "Unknown") @@ -3485,13 +3517,36 @@ delayedTrackingFrame:SetScript("OnUpdate", function() ) end - -- Check after 0.2s delay (enough time for server sync) - if elapsed >= 0.2 then + -- Drop stale entries (>2s) without recording immunity - inconclusive due to severe lag + if elapsed > 2.0 then + if debug then + local spellNameDebug = pending.spellID and _SpellInfo(pending.spellID) or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffff6600[Shared Stale]|r Dropping stale shared entry for %s on %s (%.1fs old)", + spellNameDebug, pending.targetName or "Unknown", elapsed) + ) + end + -- Don't add to newPendingList - silently discard + elseif elapsed >= 0.2 then local debuffVerified = false local totalDebuffs = 0 + -- SPELL_GO early-exit: Use definitive hit/miss data when available + if pending.spellGoHit then + debuffVerified = true + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Shared Debuff" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cff00ff00[Shared Verified via SPELL_GO]|r %s landed on %s - skipping debuff scan", + spellNameDebug, pending.targetName or "Unknown") + ) + end + end + -- Skip verification if target is dead (debuffs are removed on death) - if hasSuperwow and pending.targetGUID then + -- Guard: skip debuff scanning if SPELL_GO already determined outcome + if not debuffVerified and not pending.spellGoHit and not pending.spellGoMissed + and hasSuperwow and pending.targetGUID then if _UnitIsDead(pending.targetGUID) then -- Target died - can't verify immunity, assume debuff landed debuffVerified = true @@ -3545,7 +3600,8 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- SPLIT CC SPELLS: Skip immunity recording for spells with physical damage + resistable CC -- (e.g., Master Strike) - the physical damage lands but CC can be resisted independently - local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] or SPLIT_CC_SPELLS[pending.castSpellID] + -- spellGoHit: SPELL_GO confirmed hit, so debuff was resisted independently (not true immunity) + local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] or SPLIT_CC_SPELLS[pending.castSpellID] or pending.spellGoHit if isSplitCCSpell then if debug then local spellNameDebug = _SpellInfo(pending.castSpellID or pending.spellID) or "Unknown" @@ -4389,6 +4445,34 @@ ev:SetScript("OnEvent", function() local _, playerGUID = UnitExists("player") local isOurs = (casterGuid == playerGUID) + -- Annotate pending CC/shared debuffs with SPELL_GO hit/miss outcome + -- This data is consumed by the OnUpdate verification loop for early-exit paths + if isOurs then + -- Annotate pendingCCDebuffs + for _, pending in ipairs(lib.pendingCCDebuffs) do + if pending and pending.spellID == spellId and pending.targetGUID == targetGuid then + if numHit > 0 then + pending.spellGoHit = true + elseif numMissed > 0 then + pending.spellGoMissed = true + end + break + end + end + -- Annotate pendingSharedDebuffs (match against both castSpellID and spellID) + for _, pending in ipairs(lib.pendingSharedDebuffs) do + if pending and pending.targetGUID == targetGuid + and (pending.castSpellID == spellId or pending.spellID == spellId) then + if numHit > 0 then + pending.spellGoHit = true + elseif numMissed > 0 then + pending.spellGoMissed = true + end + break + end + end + end + -- Spell missed - clear pending, mark as failed, track for immunity detection if numHit == 0 and numMissed > 0 then if lib.pendingCasts[targetGuid] then @@ -4482,14 +4566,26 @@ ev:SetScript("OnEvent", function() -- time AURA_CAST fires. Store in pendingCasts for AURA_CAST to use. if isOurs and lib.combopointAbilities and lib.combopointAbilities[spellName] then -- Capture combo points from multiple sources - local comboPoints = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0 + -- Priority: SPELL_CAST_EVENT (client-side, pre-server) > GetComboPoints > lastComboPoints > ComboPointTracking + local comboPoints = 0 - -- If already consumed, use lastComboPoints fallback + -- PRIMARY: Check SPELL_CAST_EVENT capture (most reliable - captured before server consumed CP) + local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId] + if pending and pending.comboPoints and pending.comboPoints > 0 then + comboPoints = pending.comboPoints + end + + -- Fallback: Try GetComboPoints (may still be available) + if comboPoints == 0 then + comboPoints = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0 + end + + -- Fallback: lastComboPoints (from pre-cast hooks or UNIT_CASTEVENT START) if comboPoints == 0 and CleveRoids.lastComboPoints and CleveRoids.lastComboPoints > 0 then comboPoints = CleveRoids.lastComboPoints end - -- Also check ComboPointTracking for recent data (from /cast hook pre-population) + -- Fallback: ComboPointTracking for recent data (from /cast hook pre-population) if comboPoints == 0 and CleveRoids.ComboPointTracking then local tracking = CleveRoids.ComboPointTracking[spellName] if tracking and tracking.combo_points and tracking.combo_points > 0 then @@ -4551,24 +4647,35 @@ ev:SetScript("OnEvent", function() -- Handle combo point abilities for our casts if isOurs and lib.combopointAbilities[spellName] then -- Priority order for combo points: - -- 1. pendingCasts (captured by SPELL_GO - most reliable) - -- 2. CleveRoids.lastComboPoints (from /cast hook pre-population) - -- 3. GetComboPoints() (might be 0 if already consumed) + -- 1. SPELL_CAST_EVENT capture (client-side, before server consumes CP) + -- 2. pendingCasts from SPELL_GO (captured on spell landing) + -- 3. CleveRoids.lastComboPoints (from pre-cast hooks) + -- 4. GetComboPoints() (might be 0 if already consumed) local comboPoints = 0 local comboSource = "none" - -- Check pendingCasts first (populated by SPELL_GO) - if lib.pendingCasts[targetGuid] and lib.pendingCasts[targetGuid][spellName] then + -- PRIMARY: Check SPELL_CAST_EVENT capture (most reliable - client-side pre-server) + local castPending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId] + if castPending and castPending.comboPoints and castPending.comboPoints > 0 then + comboPoints = castPending.comboPoints + comboSource = "SPELL_CAST_EVENT" + end + + -- Fallback: Check lib.pendingCasts (populated by SPELL_GO) + if comboPoints == 0 and lib.pendingCasts[targetGuid] and lib.pendingCasts[targetGuid][spellName] then local pending = lib.pendingCasts[targetGuid][spellName] local age = GetTime() - (pending.capturedAt or 0) if pending.comboPoints and pending.comboPoints > 0 and age < 2.0 then comboPoints = pending.comboPoints comboSource = "SPELL_GO" - -- Clean up after use - lib.pendingCasts[targetGuid][spellName] = nil end end + -- Clean up lib.pendingCasts after reading (regardless of which source won) + if lib.pendingCasts[targetGuid] and lib.pendingCasts[targetGuid][spellName] then + lib.pendingCasts[targetGuid][spellName] = nil + end + -- Fallback to lastComboPoints if comboPoints == 0 and CleveRoids.lastComboPoints and CleveRoids.lastComboPoints > 0 then comboPoints = CleveRoids.lastComboPoints @@ -7897,9 +8004,10 @@ function CleveRoids.ParseReactiveCombatLog() -- Check each reactive ability's trigger patterns for spellName, config in pairs(reactivePatterns) do - -- Check if this is a known reactive spell (skip hasSpell check - spellbook may not be indexed yet) - -- If player uses [reactive:SpellName], they have the ability - if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells[spellName] then + -- Skip outgoing dodge patterns when SPELL_GO_SELF handles yellow attack miss detection + if CleveRoids.usingSpellGoEvents and config.type == "enemy_dodge" then + -- Overpower handled by SPELL_GO_SELF (hit/miss binary, no text parsing needed) + elseif CleveRoids.reactiveSpells and CleveRoids.reactiveSpells[spellName] then for _, pattern in ipairs(config.patterns) do if strfind(message, pattern) then -- Found a trigger event (works in any stance) @@ -7908,7 +8016,7 @@ function CleveRoids.ParseReactiveCombatLog() CleveRoids.SetReactiveProc(spellName, duration, guid) -- For Overpower (enemy dodge), also update LastSwing so [lastswing:dodge] works - -- This handles ability dodges that AUTO_ATTACK events don't capture + -- (Only reached when SPELL_GO is NOT available - combat log fallback) if config.type == "enemy_dodge" and CleveRoids.LastSwing then CleveRoids.LastSwing.timestamp = GetTime() CleveRoids.LastSwing.victimState = VICTIMSTATE_DODGE_REACTIVE @@ -7964,10 +8072,19 @@ end -- Uses native events for dodge/parry/block detection when available. -- Falls back to combat log parsing for older Nampower versions. --- VictimState constants from NampowerAPI (copied for performance) +-- VictimState constants (from AUTO_ATTACK / SPELL_GO events) +local VICTIMSTATE_UNAFFECTED = 0 -- Generic miss (seen with HITINFO_MISS) +local VICTIMSTATE_NORMAL = 1 -- Hit landed local VICTIMSTATE_DODGE = 2 local VICTIMSTATE_PARRY = 3 +local VICTIMSTATE_INTERRUPT = 4 local VICTIMSTATE_BLOCKS = 5 +local VICTIMSTATE_EVADES = 6 +local VICTIMSTATE_IS_IMMUNE = 7 +local VICTIMSTATE_DEFLECTS = 8 + +-- HitInfo constant for SPELL_GO miss tracking +local HITINFO_MISS = 16 -- 0x10 -- Track if we're using Nampower events (set during initialization) CleveRoids.usingNampowerAutoAttack = false @@ -8081,12 +8198,36 @@ if GetNampowerVersion then end end --- Always register combat log events as fallback (or primary if no Nampower) -reactiveFrame:RegisterEvent("RAW_COMBATLOG") -reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES") -reactiveFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") -reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES") -reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS") +-- Check for Nampower v2.25+ SPELL_GO events (replaces combat log for yellow attack miss detection) +local hasSpellGoEvents = false +if GetNampowerVersion then + local npMajor, npMinor = GetNampowerVersion() + if npMajor > 2 or (npMajor == 2 and npMinor >= 25) then + hasSpellGoEvents = true + CleveRoids.usingSpellGoEvents = true + + reactiveFrame:RegisterEvent("SPELL_GO_SELF") + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Using SPELL_GO events for yellow attack miss detection (v2.25+)") + end + end +end + +-- Register combat log events for reactive abilities +-- When SPELL_GO + AUTO_ATTACK are both available, outgoing dodges are handled natively; +-- combat log only needed for incoming avoidance (Riposte/Revenge from enemy abilities) +if hasAutoAttackEvents and hasSpellGoEvents then + reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES") + reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS") +else + -- Full combat log fallback + reactiveFrame:RegisterEvent("RAW_COMBATLOG") + reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES") + reactiveFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") + reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES") + reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS") +end reactiveFrame:SetScript("OnEvent", function() -- ======================================================================== @@ -8122,13 +8263,80 @@ reactiveFrame:SetScript("OnEvent", function() end -- ======================================================================== - -- COMBAT LOG PARSING FOR REACTIVE ABILITIES + -- NAMPOWER v2.25+ SPELL_GO EVENTS (yellow attack hit/miss detection) -- ======================================================================== - -- NOTE: This must ALWAYS run, even when Nampower AUTO_ATTACK events are available. - -- AUTO_ATTACK events only fire for white (auto-attack) swings, NOT for yellow - -- (ability) damage like Mortal Strike, Heroic Strike, etc. being dodged/parried. - -- Combat log parsing catches ability dodges that AUTO_ATTACK misses. - -- The AUTO_ATTACK handlers above already `return` early, so no double-processing. + -- Two-step: SPELL_CAST_EVENT (success=1) → SPELL_GO_SELF (hit/miss outcome) + -- Replaces combat log text parsing for outgoing yellow ability misses. + -- SPELL_GO_SELF params: itemId, spellId, casterGuid, targetGuid, castFlags, numTargetsHit, numTargetsMissed + if event == "SPELL_GO_SELF" then + local spellId = arg2 + local targetGuid = arg4 + local numHit = arg6 or 0 + local numMissed = arg7 or 0 + + -- Step 1: Validate against SPELL_CAST_EVENT (confirms player-initiated, successful cast) + local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId] + if not pending then + return -- No matching SPELL_CAST_EVENT with success=1, skip + end + + -- Clear the pending cast (consumed) + CleveRoids.pendingCasts[spellId] = nil + + -- Skip channels and targeting spells (not melee/ranged attacks) + -- CastType: NORMAL=1, NON_GCD=2, ON_SWING=3, CHANNEL=4, TARGETING=5, TARGETING_NON_GCD=6 + if pending.castType and pending.castType >= 4 then + return + end + + -- Step 2: Check hit/miss outcome - binary check covers dodge, parry, resist, etc. + if numMissed >= 1 and numHit == 0 then + -- Use current target GUID if SPELL_GO targetGuid is empty + local procTarget = targetGuid + if not procTarget or procTarget == "0x0000000000000000" then + procTarget = pending.targetGuid + if not procTarget or procTarget == "0x0000000000000000" then + local _, currentTargetGUID = UnitExists("target") + procTarget = currentTargetGUID + end + end + + -- Overpower: enemy avoided our yellow attack + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Overpower"] then + CleveRoids.SetReactiveProc("Overpower", 4.0, procTarget) + CleveRoids.QueueActionUpdate() + + if CleveRoids.debug then + local spellName = SpellInfo and SpellInfo(spellId) or tostring(spellId) + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[SPELL_GO]|r Overpower proc - %s missed (hit=%d, miss=%d, castType=%s)", + spellName, numHit, numMissed, tostring(pending.castType)) + ) + end + end + + -- Update LastSwing for yellow miss (victimState unknown from SPELL_GO, use UNAFFECTED) + -- Don't overwrite AUTO_ATTACK data from the same frame (on-swing abilities fire both) + if CleveRoids.LastSwing and CleveRoids.LastSwing.timestamp ~= GetTime() then + CleveRoids.LastSwing.timestamp = GetTime() + CleveRoids.LastSwing.hitInfo = HITINFO_MISS + CleveRoids.LastSwing.victimState = VICTIMSTATE_UNAFFECTED + CleveRoids.LastSwing.damage = 0 + CleveRoids.LastSwing.blockedAmount = 0 + CleveRoids.LastSwing.absorbAmount = 0 + CleveRoids.LastSwing.resistAmount = 0 + CleveRoids.LastSwing.targetGuid = procTarget + end + end + + return + end + + -- ======================================================================== + -- COMBAT LOG FALLBACK FOR REACTIVE ABILITIES + -- ======================================================================== + -- Only used for incoming avoidance (Riposte/Revenge) from enemy abilities. + -- Outgoing dodges handled by SPELL_GO_SELF (v2.25+) or AUTO_ATTACK (white swings). if event == "RAW_COMBATLOG" or event == "CHAT_MSG_COMBAT_SELF_MISSES" or event == "CHAT_MSG_SPELL_SELF_DAMAGE" or From edeec89e7ff5a574492405b59ecdc3e371353982 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sat, 7 Feb 2026 22:00:10 -0500 Subject: [PATCH 2/3] fixes --- Conditionals.lua | 111 ++++++++++++++++++++++++++++++++++++----------- Core.lua | 9 ++++ Utility.lua | 110 +++++++++++++++++++++++++++++----------------- 3 files changed, 164 insertions(+), 66 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index ce4c8e6..2e9e4bc 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -835,6 +835,23 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName) if not targetData then return nil, nil end local now = GetTime() + + -- Direct spell ID lookup: O(1) hash table access instead of iterating all entries + local searchID = tonumber(searchName) + if searchID then + local auraData = targetData[searchID] + if auraData and auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > 0 then + return remaining, auraData.casterGuid + else + targetData[searchID] = nil + end + end + return nil, nil + end + + -- Name-based search (existing behavior) local searchLower = string.lower(searchName) for spellId, auraData in pairs(targetData) do @@ -2997,7 +3014,9 @@ function CleveRoids.ValidateAura(unit, args, isbuff) end -- PERFORMANCE: Cache lowercased search name to avoid repeated string.lower calls - local searchName = args.name and _string_lower(args.name) + -- Support spell ID matching: [mybuff:17941] matches by spell ID instead of name + 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 @@ -3020,12 +3039,20 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if not texture then break end - if current_spellID and searchName then - -- PERFORMANCE: Use cached lowercase spell name lookup - local lowerName = GetLowercaseSpellName(current_spellID) - if lowerName and lowerName == searchName then - found = true - break + 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 + end end end @@ -3033,7 +3060,7 @@ function CleveRoids.ValidateAura(unit, args, isbuff) end -- Overflow handling: when searching DEBUFFS on non-players, also scan BUFFS - if not isbuff and not isPlayer and not found and searchName then + if not isbuff and not isPlayer and not found and (searchID or searchName) then i = 1 while true do local texture @@ -3044,11 +3071,18 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if not texture then break end if current_spellID then - -- PERFORMANCE: Use cached lowercase spell name lookup - local lowerName = GetLowercaseSpellName(current_spellID) - if lowerName and lowerName == searchName then - found = true - break + 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 @@ -3154,12 +3188,15 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap") args.name = string.gsub(args.name, "_", " ") + -- Support spell ID matching: [debuff:9904] matches by spell ID instead of name + local searchID = tonumber(args.name) + local found = false local texture, stacks, spellID, remaining local i -- PERFORMANCE: For non-SuperWoW, early return if no texture registered - if not CleveRoids.hasSuperwow and not CleveRoids.auraTextures[args.name] then + if not searchID and not CleveRoids.hasSuperwow and not CleveRoids.auraTextures[args.name] then return false end @@ -3176,7 +3213,13 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not guid then return false end -- PERFORMANCE: Use cached spell name -> ID mapping instead of iterating every call - local matchingSpellIDs = GetSpellIDsForName(args.name) or {} + -- For spell ID input, use the ID directly instead of name->ID lookup + local matchingSpellIDs + if searchID then + matchingSpellIDs = {searchID} + else + matchingSpellIDs = GetSpellIDsForName(args.name) or {} + end -- Check tracking table for ANY rank of this spell: Did player cast this? Is timer valid? if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then @@ -3272,9 +3315,14 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not tex then break end if debuffSpellID then - -- PERFORMANCE: Use cached spell name lookup - local baseName, fullName = GetSpellNames(debuffSpellID) - if baseName and (baseName == args.name or fullName == args.name) 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 @@ -3298,9 +3346,14 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not tex then break end if buffSpellID then - -- PERFORMANCE: Use cached spell name lookup - local baseName, fullName = GetSpellNames(buffSpellID) - if baseName and (baseName == args.name or fullName == args.name) 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 @@ -3324,8 +3377,12 @@ function CleveRoids.ValidateUnitDebuff(unit, args) texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, false) if not texture then break end - if CleveRoids.hasSuperwow then - -- PERFORMANCE: Use cached spell name lookup + if searchID then + if spellID == searchID then + found = true + break + end + elseif CleveRoids.hasSuperwow then local baseName, fullName = GetSpellNames(spellID) if baseName and (baseName == args.name or fullName == args.name) then found = true @@ -3345,8 +3402,12 @@ function CleveRoids.ValidateUnitDebuff(unit, args) texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, true) if not texture then break end - if CleveRoids.hasSuperwow then - -- PERFORMANCE: Use cached spell name lookup + if searchID then + if spellID == searchID then + found = true + break + end + elseif CleveRoids.hasSuperwow then local baseName, fullName = GetSpellNames(spellID) if baseName and (baseName == args.name or fullName == args.name) then found = true diff --git a/Core.lua b/Core.lua index ec809e7..613280f 100644 --- a/Core.lua +++ b/Core.lua @@ -5558,6 +5558,15 @@ function CleveRoids.Frame:SPELL_CAST_EVENT() -- Track pending cast for SPELL_GO correlation (reactive ability detection) -- Keyed by spellId so concurrent casts don't overwrite each other CleveRoids.pendingCasts = CleveRoids.pendingCasts or {} + + -- Clean up consumed entries older than 5 seconds (lightweight, runs per-cast) + local cleanupTime = GetTime() - 5 + for id, entry in pairs(CleveRoids.pendingCasts) do + if entry.consumed and entry.consumedAt and entry.consumedAt < cleanupTime then + CleveRoids.pendingCasts[id] = nil + end + end + CleveRoids.pendingCasts[spellId] = { castType = castType, targetGuid = targetGuid, diff --git a/Utility.lua b/Utility.lua index 175f6cb..0ac6afb 100644 --- a/Utility.lua +++ b/Utility.lua @@ -8003,11 +8003,11 @@ function CleveRoids.ParseReactiveCombatLog() local _, targetGUID = UnitExists("target") -- Check each reactive ability's trigger patterns + -- NOTE: Overpower (enemy_dodge) ALWAYS uses combat log text parsing, even when SPELL_GO + -- events are available. SPELL_GO only provides hit/miss COUNTS, not miss TYPES, so it + -- cannot distinguish dodge (Overpower proc) from parry/block/resist (no Overpower proc). for spellName, config in pairs(reactivePatterns) do - -- Skip outgoing dodge patterns when SPELL_GO_SELF handles yellow attack miss detection - if CleveRoids.usingSpellGoEvents and config.type == "enemy_dodge" then - -- Overpower handled by SPELL_GO_SELF (hit/miss binary, no text parsing needed) - elseif CleveRoids.reactiveSpells and CleveRoids.reactiveSpells[spellName] then + if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells[spellName] then for _, pattern in ipairs(config.patterns) do if strfind(message, pattern) then -- Found a trigger event (works in any stance) @@ -8016,7 +8016,6 @@ function CleveRoids.ParseReactiveCombatLog() CleveRoids.SetReactiveProc(spellName, duration, guid) -- For Overpower (enemy dodge), also update LastSwing so [lastswing:dodge] works - -- (Only reached when SPELL_GO is NOT available - combat log fallback) if config.type == "enemy_dodge" and CleveRoids.LastSwing then CleveRoids.LastSwing.timestamp = GetTime() CleveRoids.LastSwing.victimState = VICTIMSTATE_DODGE_REACTIVE @@ -8198,28 +8197,36 @@ if GetNampowerVersion then end end --- Check for Nampower v2.25+ SPELL_GO events (replaces combat log for yellow attack miss detection) +-- 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 +-- successful spell hit - only parse when SPELL_GO reports a miss. local hasSpellGoEvents = false if GetNampowerVersion then local npMajor, npMinor = GetNampowerVersion() if npMajor > 2 or (npMajor == 2 and npMinor >= 25) then hasSpellGoEvents = true - CleveRoids.usingSpellGoEvents = true - reactiveFrame:RegisterEvent("SPELL_GO_SELF") if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Using SPELL_GO events for yellow attack miss detection (v2.25+)") + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Using SPELL_GO events for yellow attack LastSwing tracking (v2.25+)") end end end -- Register combat log events for reactive abilities --- When SPELL_GO + AUTO_ATTACK are both available, outgoing dodges are handled natively; --- combat log only needed for incoming avoidance (Riposte/Revenge from enemy abilities) -if hasAutoAttackEvents and hasSpellGoEvents then +-- Outgoing dodge detection (Overpower) uses combat log text parsing because SPELL_GO only +-- provides hit/miss counts, not miss types (dodge vs parry vs block vs resist). However, +-- with SPELL_GO available, CHAT_MSG_SPELL_SELF_DAMAGE parsing is gated behind a miss window +-- so we skip parsing on every successful spell hit (see OnEvent handler below). +-- AUTO_ATTACK events handle white swing avoidance; combat log handles yellow ability dodges. +if hasAutoAttackEvents then + -- Incoming avoidance (Riposte/Revenge) reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES") reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS") + -- Outgoing dodge detection (Overpower) - gated by SPELL_GO miss window when available + reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES") + reactiveFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") else -- Full combat log fallback reactiveFrame:RegisterEvent("RAW_COMBATLOG") @@ -8263,10 +8270,11 @@ reactiveFrame:SetScript("OnEvent", function() end -- ======================================================================== - -- NAMPOWER v2.25+ SPELL_GO EVENTS (yellow attack hit/miss detection) + -- NAMPOWER v2.25+ SPELL_GO EVENTS (yellow attack LastSwing tracking) -- ======================================================================== -- Two-step: SPELL_CAST_EVENT (success=1) → SPELL_GO_SELF (hit/miss outcome) - -- Replaces combat log text parsing for outgoing yellow ability misses. + -- Updates LastSwing state for yellow ability misses. Overpower detection still uses + -- combat log text parsing (SPELL_GO only has hit/miss counts, not types like dodge). -- SPELL_GO_SELF params: itemId, spellId, casterGuid, targetGuid, castFlags, numTargetsHit, numTargetsMissed if event == "SPELL_GO_SELF" then local spellId = arg2 @@ -8276,12 +8284,16 @@ reactiveFrame:SetScript("OnEvent", function() -- Step 1: Validate against SPELL_CAST_EVENT (confirms player-initiated, successful cast) local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId] - if not pending then - return -- No matching SPELL_CAST_EVENT with success=1, skip + if not pending or pending.consumed then + return -- No matching SPELL_CAST_EVENT with success=1, or already processed end - -- Clear the pending cast (consumed) - CleveRoids.pendingCasts[spellId] = nil + -- Mark pending cast as consumed with timestamp instead of deleting immediately. + -- Other handlers (libdebuff SPELL_GO, AURA_CAST, ComboPointTracker) on different + -- frames may still need to read this data. Stale entries are cleaned up on next + -- SPELL_CAST_EVENT for the same spellId (overwritten) or expire naturally. + pending.consumed = true + pending.consumedAt = GetTime() -- Skip channels and targeting spells (not melee/ranged attacks) -- CastType: NORMAL=1, NON_GCD=2, ON_SWING=3, CHANNEL=4, TARGETING=5, TARGETING_NON_GCD=6 @@ -8289,7 +8301,11 @@ reactiveFrame:SetScript("OnEvent", function() return end - -- Step 2: Check hit/miss outcome - binary check covers dodge, parry, resist, etc. + -- Step 2: Check hit/miss outcome for LastSwing tracking + -- NOTE: SPELL_GO only provides hit/miss COUNTS, not miss TYPES (dodge vs parry vs + -- block vs resist). Overpower detection is handled by combat log text parsing which + -- can distinguish "dodged" from other avoidance types. We only update LastSwing here + -- as a generic miss indicator (combat log overwrites with specific type if dodge). if numMissed >= 1 and numHit == 0 then -- Use current target GUID if SPELL_GO targetGuid is empty local procTarget = targetGuid @@ -8301,22 +8317,9 @@ reactiveFrame:SetScript("OnEvent", function() end end - -- Overpower: enemy avoided our yellow attack - if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells["Overpower"] then - CleveRoids.SetReactiveProc("Overpower", 4.0, procTarget) - CleveRoids.QueueActionUpdate() - - if CleveRoids.debug then - local spellName = SpellInfo and SpellInfo(spellId) or tostring(spellId) - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[SPELL_GO]|r Overpower proc - %s missed (hit=%d, miss=%d, castType=%s)", - spellName, numHit, numMissed, tostring(pending.castType)) - ) - end - end - -- Update LastSwing for yellow miss (victimState unknown from SPELL_GO, use UNAFFECTED) -- Don't overwrite AUTO_ATTACK data from the same frame (on-swing abilities fire both) + -- Combat log handler will overwrite with specific type (dodge/parry/etc.) if available if CleveRoids.LastSwing and CleveRoids.LastSwing.timestamp ~= GetTime() then CleveRoids.LastSwing.timestamp = GetTime() CleveRoids.LastSwing.hitInfo = HITINFO_MISS @@ -8327,21 +8330,46 @@ reactiveFrame:SetScript("OnEvent", function() CleveRoids.LastSwing.resistAmount = 0 CleveRoids.LastSwing.targetGuid = procTarget end + + -- 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() + + if CleveRoids.debug then + local spellName = SpellInfo and SpellInfo(spellId) or tostring(spellId) + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[SPELL_GO]|r Yellow miss detected - %s (hit=%d, miss=%d, castType=%s) - deferring to combat log for type", + spellName, numHit, numMissed, tostring(pending.castType)) + ) + end end return end -- ======================================================================== - -- COMBAT LOG FALLBACK FOR REACTIVE ABILITIES + -- COMBAT LOG TEXT PARSING FOR REACTIVE ABILITIES -- ======================================================================== - -- Only used for incoming avoidance (Riposte/Revenge) from enemy abilities. - -- Outgoing dodges handled by SPELL_GO_SELF (v2.25+) or AUTO_ATTACK (white swings). - if event == "RAW_COMBATLOG" or - event == "CHAT_MSG_COMBAT_SELF_MISSES" or - event == "CHAT_MSG_SPELL_SELF_DAMAGE" or - event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES" or - event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS" then + -- Handles: Overpower (outgoing dodge), Riposte (incoming parry), Revenge (incoming avoid). + -- SPELL_GO provides hit/miss counts but not types, so combat log is still needed to + -- 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 + -- 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). + if hasSpellGoEvents then + local missWindow = CleveRoids._yellowMissWindow + if not missWindow or (GetTime() - missWindow) > 1.0 then + return -- No recent yellow miss from SPELL_GO, skip parsing + end + end + CleveRoids.ParseReactiveCombatLog() + elseif event == "RAW_COMBATLOG" or + event == "CHAT_MSG_COMBAT_SELF_MISSES" or + event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES" or + event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS" then CleveRoids.ParseReactiveCombatLog() end end) From c2c8af7d2dadc73007aee18067c7b46d081fa39e Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sun, 8 Feb 2026 17:42:16 -0500 Subject: [PATCH 3/3] pfui 7_6 update compatibility and reformat readme --- Compatibility/pfUI.lua | 23 +- Core.lua | 5 +- README.md | 850 ++++++++++++++--------------------------- Utility.lua | 36 +- 4 files changed, 322 insertions(+), 592 deletions(-) diff --git a/Compatibility/pfUI.lua b/Compatibility/pfUI.lua index 0c343be..981720f 100644 --- a/Compatibility/pfUI.lua +++ b/Compatibility/pfUI.lua @@ -126,9 +126,14 @@ function Extension.HookPfUILibdebuff() -- If so, pfUI handles combo durations and Carnage internally - we only override on mismatch local hasPfUI76 = CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil) + -- pfUI 7.6+ handles all durations internally - no hooks needed + if hasPfUI76 then + Extension.DLOG("Skipped all libdebuff hooks (pfUI 7.6+ handles internally)") + return false + end + -- Hook GetDuration if it exists -- pfUI's GetDuration signature: function(effect, rank) where effect is spell NAME - -- pfUI 7.6+ already handles combo durations via GetStoredComboPoints(), so only override on mismatch if pflib.GetDuration and not Extension.pfLibDebuffHooked then local originalGetDuration = pflib.GetDuration @@ -171,10 +176,8 @@ function Extension.HookPfUILibdebuff() Extension.DLOG("Hooked pfUI.api.libdebuff.GetDuration (mismatch-only mode)") end - -- Hook AddEffect if it exists - -- NOTE: In pfUI 7.6+, AddEffect is LEGACY (rarely called - events handle tracking) - -- We only hook for pre-7.6 compatibility and rank checking fallback - if pflib.AddEffect and not Extension.pfLibAddEffectHooked and not hasPfUI76 then + -- Hook AddEffect if it exists (pre-7.6 only - 7.6+ returns early above) + if pflib.AddEffect and not Extension.pfLibAddEffectHooked then local originalAddEffect = pflib.AddEffect pflib.AddEffect = function(self, unit, unitlevel, effect, duration, caster) @@ -258,9 +261,7 @@ function Extension.HookPfUILibdebuff() end Extension.pfLibAddEffectHooked = true - Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect (pre-7.6 legacy mode)") - elseif hasPfUI76 then - Extension.DLOG("Skipped AddEffect hook (pfUI 7.6+ handles internally)") + Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect") end -- Hook UnitDebuff to return Carnage override duration to display code @@ -343,11 +344,17 @@ function Extension.HookPfUILibdebuff() end -- Synchronize combo durations to pfUI's libdebuff objects +-- NOTE: pfUI 7.6+ handles combo durations internally - skip sync entirely function Extension.SyncComboDurationToPfUI(guid, spellID, duration) if not pfUI or not pfUI.api or not pfUI.api.libdebuff then return end + -- pfUI 7.6+ handles all durations internally + if CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil) then + return + end + -- Get unit name from GUID local unitName = nil local unitLevel = 0 diff --git a/Core.lua b/Core.lua index 613280f..aaf395f 100644 --- a/Core.lua +++ b/Core.lua @@ -4894,13 +4894,12 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time ) end - -- Also sync to pfUI if it's loaded - if pfUI and pfUI.api and pfUI.api.libdebuff then + -- Also sync to pfUI if it's loaded (pre-7.6 only) + if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target") local targetLevel = UnitLevel("target") or 0 if targetName then - -- Refresh in pfUI's tracking (by name) pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player") if CleveRoids.debug then diff --git a/README.md b/README.md index d37dc04..ac7d33e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,14 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna 1. Download and extract to `Interface/AddOns/SuperCleveRoidMacros` 2. **Recommended pfUI:** Use [jrc13245/pfUI](https://github.com/jrc13245/pfUI) for full compatibility with macro spell scanning and action bar features +## Known Issues + +- Unique macro names required (no blanks, duplicates, or spell names) +- Reactive abilities must be on action bars for detection +- Debuff time-left conditionals only work on own debuffs unless pfUI libdebuff or Cursive has data +- HealComm requires MarcelineVQ's [LunaUnitFrames](https://github.com/MarcelineVQ/LunaUnitFrames) for SuperWoW compatibility +- Macro line length: 261 characters max (MacroLengthWarn extension prevents crashes) + --- ## Quick Start @@ -24,7 +32,6 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna #showtooltip /cast [mod:alt] Frostbolt; [mod:ctrl] Fire Blast; Blink ``` - - **Conditionals** in `[]` brackets, space or comma separated - **Arguments** use colon: `[mod:alt]`, `[hp:>50]` - **Negation** with `no` prefix: `[nobuff]`, `[nomod:alt]` @@ -32,17 +39,27 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna - **Spell names** with spaces: `"Mark of the Wild"` or `Mark_of_the_Wild` ### Multi-Value Logic + +| Syntax | Logic | Example | Meaning | +|--------|-------|---------|---------| +| `[buff:X/Y]` | OR | `[buff:Renew/Rejuvenation]` | Has Renew **or** Rejuvenation | +| `[buff:X&Y]` | AND | `[buff:Fortitude&Mark_of_the_Wild]` | Has Fort **and** MotW | +| `[nobuff:X/Y]` | NOR | `[nobuff:Renew/Rejuvenation]` | Missing **both** Renew **and** Rejuvenation | +| `[nobuff:X&Y]` | NAND | `[nobuff:Fortitude&Mark_of_the_Wild]` | Missing **at least one** of Fort or MotW | + +> **De Morgan's Law:** Negation flips the operator. `no` + `/`(OR) → AND (must lack all). `no` + `&`(AND) → OR (must lack at least one). + +**Multiple instances = AND between groups:** ```lua -[buff:X/Y] -- X OR Y (has either) -[buff:X&Y] -- X AND Y (has both) -[nobuff:X/Y] -- NOT X AND NOT Y (missing both) - operators flip for negation +[mybuff:X/Y mybuff:Z] -- (has X OR Y) AND (has Z) ``` ### Comparisons ```lua -[hp:>50] -- Health above 50% -[buff:"Name"<5] -- Less than 5 seconds remaining -[debuff:"Name">#3] -- 3+ stacks (use ># for stacks) +[hp:>50] -- Health above 50% +[hp:>20&<50] -- Health between 20% and 50% +[buff:"Name"<5] -- Less than 5 seconds remaining +[debuff:"Name">#3] -- 3+ stacks (use # for stacks) ``` ### Special Prefixes @@ -54,112 +71,213 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna --- +## Slash Commands + +| Command | Cond | Example | Description | +|---------|:----:|---------|-------------| +| `/cast` | ✅ | `/cast [mod:alt] Frostbolt` | Cast spell | +| `/use` | ✅ | `/use [hp:<30] Healing Potion` | Use item by name/ID/slot | +| `/equip` | ✅ | `/equip [nocombat] Fishing Pole` | Equip item | +| `/target` | ✅ | `/target [harm,nodead]` | Target with UnitXP 3D scanning | +| `/castsequence` | ✅ | `/castsequence reset=3/target Spell1, Spell2` | Cast spells in sequence | +| `/castpet` | ✅ | `/castpet [combat] Claw` | Cast pet spell | +| `/startattack` | ✅ | `/startattack [harm]` | Start auto-attack | +| `/stopattack` | ✅ | `/stopattack [noothertag]` | Stop auto-attack | +| `/stopcasting` | ✅ | `/stopcasting [mycc:silence]` | Stop casting | +| `/unqueue` | ✅ | `/unqueue [mod:shift]` | Cancel queued spell (Nampower) | +| `/cleartarget` | ✅ | `/cleartarget [dead]` | Clear target | +| `/cancelaura` | ✅ | `/cancelaura [combat] Ice Block` | Cancel buff (alias: `/unbuff`) | +| `/unshift` | ✅ | `/unshift [myhp:<30]` | Cancel shapeshift form | +| `/stopmacro` | ✅ | `/stopmacro [nocombat]` | Stop ALL macro execution | +| `/skipmacro` | ✅ | `/skipmacro [nogroup]` | Stop current submacro only | +| `/firstaction` | ✅ | `/firstaction [group]` | Priority: stop on first successful `/cast` | +| `/nofirstaction` | ✅ | `/nofirstaction` | Re-enable multi-queue after `/firstaction` | +| `/quickheal` | ✅ | `/quickheal [combat]` | Smart heal (alias: `/qh`, requires QuickHeal) | +| `/petattack` | ✅ | `/petattack [harm]` | Pet attack | +| `/petfollow` | ✅ | `/petfollow [nocombat]` | Pet follow | +| `/petwait` | ✅ | `/petwait [mod:ctrl]` | Pet stay | +| `/petpassive` | ✅ | `/petpassive` | Pet passive mode | +| `/petdefensive` | ✅ | `/petdefensive` | Pet defensive mode | +| `/petaggressive` | ✅ | `/petaggressive [group]` | Pet aggressive mode | +| `/equipmh` | ✅ | `/equipmh [nocombat] Dagger` | Equip to main hand | +| `/equipoh` | ✅ | `/equipoh [nocombat] Dagger` | Equip to off hand | +| `/equip11`-`14` | ✅ | `/equip13 [combat] Trinket` | Equip to ring/trinket slot | +| `/applymain` | ✅ | `/applymain [nomhimbue] Instant Poison` | Apply poison/oil to main hand | +| `/applyoff` | ✅ | `/applyoff [noohimbue] Crippling Poison` | Apply poison/oil to off hand | +| `/retarget` | — | `/retarget` | Clear invalid target, target nearest enemy | +| `/runmacro` | — | `/runmacro MyMacro` | Execute macro by name | +| `/rl` | — | `/rl` | Reload UI | +| `/clearequipqueue` | — | `/clearequipqueue` | Clear equipment swap queue | +| `/equipqueuestatus` | — | `/equipqueuestatus` | Show equipment queue status | + +### Priority-Based Macro Evaluation + +By default, all `/cast` lines evaluate and may queue spells with Nampower. `/firstaction` enables "first successful cast wins" mode. + +```lua +/firstaction +/cast [myrawpower:>48] Shred -- Priority: only one of these +/cast [myrawpower:>40] Claw +/nofirstaction +/cast Tiger's Fury -- Always evaluates alongside +/startattack +``` + +`/firstaction [cond]` accepts conditionals. Child macros (via `{MacroName}`) inherit priority state. + +### UnitXP 3D Enemy Scanning + +`/target` with any conditionals automatically uses UnitXP 3D scanning to find enemies in line of sight. Exception: `[help]` without `[harm]` (friendly-only). If no match is found, your original target is preserved. + +```lua +/target [name:Onyxia] -- Scan for exact name +/target [nodead,harm] -- Scan for living enemies +/target [cc:stun] -- Scan for stunned enemies +``` + +--- + ## Conditionals Reference -All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff]`, `[nohelp]`). Some also have semantic opposites: `help`/`harm`, `isplayer`/`isnpc`, `alive`/`dead`, `inrange`/`outrange`. +All conditionals support negation with `no` prefix unless noted. Some have semantic opposites: `help`/`harm`, `alive`/`dead`, `isplayer`/`isnpc`, `inrange`/`outrange`. -### Modifiers & Player State -| Conditional | Example | Description | -|-------------|---------|-------------| -| `mod` | `[mod:alt/ctrl/shift]` | Modifier key pressed | -| `combat` | `[combat]` `[combat:target]` | In combat (player or unit) | -| `form/stance` | `[form:1]` `[stance:2]` | Shapeshift/stance index | -| `stealth` | `[stealth]` | In stealth (Rogue/Druid) | -| `group` | `[group]` `[group:party/raid]` | Player in group type | -| `resting` | `[resting]` | In rest area | -| `swimming` | `[swimming]` | Can use aquatic form | -| `moving` | `[moving]` `[moving:>100]` | Moving / speed % (MonkeySpeed) | -| `zone` | `[zone:"Ironforge"]` | Current zone name | +**Table columns:** +- **Description** — what the conditional checks +- **No** — supports `no` prefix negation +- **Multi** — supports multi-value OR (`/`), AND (`&`), or comparison operators (`>`, `<`, `>=`, `<=`, `=`, `~=`) +- **NP** — minimum Nampower version required +- **Addon** — addon required or recommended -### Resources -| Conditional | Example | Description | -|-------------|---------|-------------| -| `myhp` | `[myhp:<30]` | Player HP % | -| `myrawhp` | `[myrawhp:>1000]` | Player raw HP value | -| `myhplost` | `[myhplost:>500]` | Player HP lost (max - current) | -| `mypower` | `[mypower:>50]` | Player mana/rage/energy % | -| `myrawpower` | `[myrawpower:>500]` | Player raw power value | -| `mypowerlost` | `[mypowerlost:>200]` | Player power lost | -| `druidmana` | `[druidmana:>=500]` | Druid mana while shapeshifted | -| `combo` | `[combo:>=4]` | Combo points | -| `stat` | `[stat:agi>100]` `[stat:ap>1000]` | Player stats (see below) | +### Player Conditionals (@player) -**Stat types:** `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` +These always evaluate against the player. Cannot be redirected with `@unit`. -### Buffs & Debuffs -| Conditional | Example | Description | -|-------------|---------|-------------| -| `mybuff` | `[mybuff:"Name"<5]` | Player has buff (with time check) | -| `mydebuff` | `[mydebuff:"Name"]` | Player has debuff | -| `mybuffcount` | `[mybuffcount:>15]` `[nomybuffcount:>28]` | Player buff slot count (32 max) | -| `buff` | `[buff:"Name">#3]` | Target has buff (with stacks) | -| `debuff` | `[debuff:"Sunder">20]` | Target has debuff (with time) | -| `cursive` | `[cursive:Rake<3]` | Cursive addon tracking (GUID-based) | +| Conditional | Description | No | Multi | Example | NP | Addon | +|-------------|-------------|:--:|:-----:|---------|:--:|-------| +| ***State & Modifiers*** | | | | | | | +| `mod` | Modifier key pressed | ✅ | ✅ | `[mod:alt/ctrl]` | | | +| `combat` | In combat (player default, or unit) | ✅ | ✅ | `[combat]` `[combat:target]` | | | +| `form` / `stance` | Shapeshift form or stance index | ✅ | ✅ | `[form:1/3]` `[stance:2]` | | | +| `stealth` | In stealth (Rogue/Druid) | ✅ | — | `[stealth]` | | | +| `group` | Player is in group type | ✅ | ✅ | `[group:party/raid]` | | | +| `resting` | In rest area | ✅ | — | `[resting]` | | | +| `swimming` | Swimming | ✅ | — | `[swimming]` | | | +| `moving` | Moving / speed % | ✅ | ✅ | `[moving]` `[moving:>100&<200]` | | MonkeySpeed (speed %) | +| `zone` | Current zone name | ✅ | ✅ | `[zone:Ironforge/Stormwind]` | | | +| ***Resources*** | | | | | | | +| `myhp` | Player HP % | — | ✅ | `[myhp:<30]` `[myhp:>20&<50]` | | | +| `myrawhp` | Player raw HP value | — | ✅ | `[myrawhp:>1000]` | | | +| `myhplost` | Player HP deficit (max - current) | — | ✅ | `[myhplost:>500]` | | | +| `mypower` | Player mana/rage/energy % | — | ✅ | `[mypower:>50]` | | | +| `myrawpower` | Player raw power value | — | ✅ | `[myrawpower:>=40]` | | | +| `mypowerlost` | Player power deficit | — | ✅ | `[mypowerlost:>200]` | | | +| `druidmana` | Druid mana while shapeshifted | — | ✅ | `[druidmana:>=500]` | | | +| `combo` | Combo points | ✅ | ✅ | `[combo:>=4]` | | | +| `stat` | Player stat value | ✅ | ✅ | `[stat:agi>100]` `[stat:ap>1000]` | | | +| `mylevel` | Player level | — | ✅ | `[mylevel:=60]` | | | +| ***Player Auras*** | | | | | | | +| `mybuff` | Player has buff (with time/stacks) | ✅ | ✅ | `[mybuff:Thorns<5]` `[nomybuff:MotW/GotW]` | | | +| `mydebuff` | Player has debuff (with time/stacks) | ✅ | ✅ | `[mydebuff:Curse_of_Agony]` | | | +| `mybuffcount` | Player buff slot count (32 max) | ✅ | ✅ | `[mybuffcount:>15]` `[nomybuffcount:>28]` | | | +| `mybuffcapped` | Player buff bar full (32) | ✅ | — | `[nomybuffcapped]` | v2.20 | | +| `mydebuffcapped` | Player debuff bar full (16) | ✅ | — | `[nomydebuffcapped]` | v2.20 | | +| `mycc` | Player has CC effect | ✅ | ✅ | `[mycc:stun/silence]` `[nomycc]` | | | +| ***Spells & Cooldowns*** | | | | | | | +| `cooldown` | Spell/item CD remaining (ignores GCD) | ✅ | ✅ | `[cooldown:Sprint<5]` | | | +| `cdgcd` | Spell/item CD remaining (includes GCD) | ✅ | ✅ | `[cdgcd:Sprint>0]` | | | +| `gcd` | GCD is active / time remaining | ✅ | ✅ | `[gcd]` `[gcd:<1]` | | | +| `usable` | Spell/item is usable now | ✅ | ✅ | `[usable:Overpower]` | | | +| `reactive` | Reactive ability available (on bar) | ✅ | ✅ | `[reactive:Overpower]` | | | +| `known` | Spell/talent known (with rank) | ✅ | ✅ | `[known:Berserk]` `[known:Frostbolt>#2]` | | | +| `selfcasting` | Player is casting or channeling | ✅ | ✅ | `[selfcasting]` `[noselfcasting:Hearthstone]` | | | +| `checkcasting` | True if NOT casting (specific spell) | — | ✅ | `[checkcasting]` `[checkcasting:Frostbolt]` | | | +| `channeled` | Player is channeling | ✅ | ✅ | `[channeled]` `[channeled:Arcane_Missiles]` | | | +| `checkchanneled` | True if NOT channeling (specific spell) | — | ✅ | `[checkchanneled]` `[checkchanneled:Evocation]` | | | +| `spellcasttime` | Spell cast time from tooltip (real-time) | ✅ | ✅ | `[spellcasttime:>1.5]` `[spellcasttime:Frostbolt<2]` | | | +| `queuedspell` | Spell is queued | ✅ | ✅ | `[queuedspell]` `[queuedspell:Fireball]` | v2.12 | | +| `onswingpending` | On-swing spell pending | ✅ | — | `[onswingpending]` | v2.12 | | +| ***Equipment*** | | | | | | | +| `equipped` | Item or weapon type equipped | ✅ | ✅ | `[equipped:Daggers/Swords]` | | | +| `mhimbue` | Main hand has temporary imbue | ✅ | ✅ | `[mhimbue:Instant_Poison<300]` `[mhimbue:>#5]` | | | +| `ohimbue` | Off hand has temporary imbue | ✅ | ✅ | `[noohimbue]` `[ohimbue:Crippling_Poison]` | | | +| `pet` | Has active pet (with family) | ✅ | ✅ | `[pet]` `[pet:Cat/Wolf]` | | | +| ***Melee Tracking*** | | | | | | | +| `lastswing` | Player melee swing type/timing | ✅ | ✅ | `[lastswing:dodge]` `[lastswing:<2]` | v2.24 | | +| `incominghit` | Incoming attack type/timing | ✅ | ✅ | `[incominghit:crushing]` `[noincominghit:crit]` | v2.24 | | +| `swingtimer` | Swing timer % elapsed | ✅ | ✅ | `[swingtimer:>80]` | | SP_SwingTimer | +| `stimer` | Alias for swingtimer | ✅ | ✅ | `[stimer:<15]` | | SP_SwingTimer | +| `slamclip` / `noslamclip` | Slam now will/won't clip auto-attack | ✅ | — | `[noslamclip]` | | SP_SwingTimer | +| `nextslamclip` / `nonextslamclip` | Instant now will/won't cause next Slam to clip | ✅ | — | `[nonextslamclip]` | | SP_SwingTimer | +| ***Target Existence*** | | | | | | | +| `hastarget` | Player has a target | — | — | `[hastarget]` | | | +| `notarget` | Player has no target | — | — | `[notarget]` | | | -### Cooldowns & Casting -| Conditional | Example | Description | -|-------------|---------|-------------| -| `cooldown` | `[cooldown:"Spell"<5]` | CD remaining (ignores GCD) | -| `cdgcd` | `[cdgcd:"Spell">0]` | CD remaining (includes GCD) | -| `gcd` | `[gcd]` `[gcd:<1]` | GCD is active / remaining time | -| `usable` | `[usable:"Spell"]` | Spell/item is usable | -| `reactive` | `[reactive:Overpower]` | Reactive ability available | -| `known` | `[known:"Spell">#2]` | Spell/talent known (with rank) | -| `channeled` | `[channeled]` | Currently channeling | -| `channeltime` | `[channeltime:<0.5]` | Channel time remaining (seconds) | -| `selfcasting` | `[selfcasting]` | Player is casting/channeling | -| `casttime` | `[casttime:<0.5]` | Player cast time remaining | -| `checkcasting` | `[checkcasting]` `[checkcasting:Frostbolt]` | NOT casting (specific spell) | -| `checkchanneled` | `[checkchanneled]` | NOT channeling (specific spell) | -| `queuedspell` | `[queuedspell]` `[queuedspell:Fireball]` | Spell queued (Nampower) | -| `onswingpending` | `[onswingpending]` | On-swing spell pending | +### Target Conditionals (default @target) -### Target Checks -| Conditional | Example | Description | -|-------------|---------|-------------| -| `exists` | `[@mouseover,exists]` | Unit exists | -| `alive/dead` | `[alive]` `[dead]` | Alive or dead | -| `help/harm` | `[help]` `[harm]` | Friendly or hostile | -| `hp` | `[hp:<20]` `[hp:>30&<70]` | Target HP % | -| `rawhp` | `[rawhp:>5000]` | Target raw HP value | -| `hplost` | `[hplost:>1000]` | Target HP lost | -| `power` | `[power:<30]` | Target power % | -| `rawpower` | `[rawpower:>500]` | Target raw power value | -| `powerlost` | `[powerlost:>100]` | Target power lost | -| `powertype` | `[powertype:mana/rage/energy]` | Target's power type | -| `level` | `[level:>60]` `[mylevel:=60]` | Unit level (skull = 63) | -| `class` | `[class:Warrior/Priest]` | Target class (players only) | -| `type` | `[type:Undead/Beast]` | Creature type | -| `isplayer` | `[isplayer]` | Target is a player | -| `isnpc` | `[isnpc]` | Target is an NPC | -| `targeting` | `[targeting:player]` `[targeting:tank]` | Unit targeting you / any tank | -| `istank` | `[istank]` `[@focus,istank]` | Unit is marked as tank (pfUI) | -| `casting` | `[casting:"Spell"]` | Unit casting spell | -| `party` | `[party]` `[party:focus]` | Unit in your party | -| `raid` | `[raid]` `[raid:mouseover]` | Unit in your raid | -| `member` | `[member]` | Target in party OR raid | -| `hastarget` | `[hastarget]` | Player has a target | -| `notarget` | `[notarget]` | Player has no target | -| `pet` | `[pet]` `[pet:Cat/Wolf]` | Has pet (with family) | -| `name` | `[name:Onyxia]` | Exact name match (case-insensitive) | -| `tag` | `[tag]` | Target is tapped (by anyone) | -| `notag` | `[notag]` | Target is not tapped | -| `mytag` | `[mytag]` | Target is tapped by you | -| `nomytag` | `[nomytag]` | Target is not tapped by you | -| `othertag` | `[othertag]` | Target is tapped by someone else | -| `noothertag` | `[noothertag]` | Not tapped by others (yours or unclaimed) | +These default to checking the current target. Most can be redirected with `@unit` (e.g., `[@focus,hp:<30]`, `[@mouseover,exists]`). -### Range & Position -| Conditional | Example | Description | -|-------------|---------|-------------| -| `distance` | `[distance:<40]` | Distance in yards (UnitXP) | -| `inrange` | `[inrange:"Spell"]` `[inrange:Spell>N]` | In spell range / count in range | -| `outrange` | `[outrange:"Spell"]` `[outrange:Spell>N]` | Out of spell range / count out | -| `meleerange` | `[meleerange]` `[meleerange:>N]` | In melee range / count in melee | -| `behind` | `[behind]` `[behind:>N]` | Behind target / count behind | -| `insight` | `[insight]` `[insight:>N]` | In line of sight / count in LoS | +| Conditional | Description | No | Multi | Example | NP | Addon | +|-------------|-------------|:--:|:-----:|---------|:--:|-------| +| ***Existence & Disposition*** | | | | | | | +| `exists` | Unit exists | ✅ | — | `[@mouseover,exists]` | | | +| `alive` | Unit is alive | ✅ | — | `[alive]` | | | +| `dead` | Unit is dead | ✅ | — | `[dead]` | | | +| `help` | Unit is friendly | ✅ | — | `[help]` | | | +| `harm` | Unit is hostile | ✅ | — | `[harm]` | | | +| `isplayer` | Unit is a player | ✅ | — | `[isplayer]` | | | +| `isnpc` | Unit is an NPC | ✅ | — | `[isnpc]` | | | +| ***Resources*** | | | | | | | +| `hp` | Target HP % | — | ✅ | `[hp:<20]` `[hp:>30&<70]` | | | +| `rawhp` | Target raw HP value | — | ✅ | `[rawhp:>5000]` | | | +| `hplost` | Target HP deficit (max - current) | — | ✅ | `[hplost:>1000]` | | | +| `power` | Target mana/rage/energy % | — | ✅ | `[power:<30]` | | | +| `rawpower` | Target raw power value | — | ✅ | `[rawpower:>500]` | | | +| `powerlost` | Target power deficit | — | ✅ | `[powerlost:>100]` | | | +| `powertype` | Target power type | ✅ | ✅ | `[powertype:mana/rage]` | | | +| `level` | Target level (skull = 63) | — | ✅ | `[level:>60]` | | | +| ***Classification*** | | | | | | | +| `class` | Target class (players only) | ✅ | ✅ | `[class:Warrior/Priest]` | | | +| `type` | Creature type | ✅ | ✅ | `[type:Undead/Beast]` | | | +| `name` | Exact name match (case-insensitive) | ✅ | ✅ | `[name:Onyxia]` `[noname:Critter/Dummy]` | | | +| ***Target Auras*** | | | | | | | +| `buff` | Target has buff (with time/stacks) | ✅ | ✅ | `[buff:Shield>#3]` `[nobuff:Renew/Rejuv]` | | | +| `debuff` | Target has debuff (with time/stacks) | ✅ | ✅ | `[debuff:Moonfire<4]` `[nodebuff:Rake]` | | | +| `buffcapped` | Target buff bar full (32) | ✅ | — | `[nobuffcapped]` | v2.20 | | +| `debuffcapped` | Target debuff bar full | ✅ | — | `[nodebuffcapped]` | v2.20 | | +| `cc` | Target has CC effect | ✅ | ✅ | `[cc:stun/fear]` `[nocc:polymorph]` | | | +| `cursive` | GUID-based debuff tracking (time remaining) | ✅ | ✅ | `[cursive:Rake<3]` `[nocursive:Rip]` | | Cursive | +| ***Casting*** | | | | | | | +| `casting` | Target is casting (specific spell) | ✅ | ✅ | `[casting]` `[casting:Heal]` | | | +| `casttime` | Target cast time remaining (seconds) | ✅ | ✅ | `[casttime:<0.5]` | | | +| `channeltime` | Target channel time remaining (seconds) | ✅ | ✅ | `[channeltime:<0.5]` | | | +| ***Relationship*** | | | | | | | +| `party` | Unit is in your party | ✅ | ✅ | `[party]` `[party:focus]` | | | +| `raid` | Unit is in your raid | ✅ | ✅ | `[raid]` `[raid:mouseover]` | | | +| `member` | Target is in party or raid | ✅ | — | `[member]` | | | +| `targeting` | Unit is targeting you or a tank | ✅ | ✅ | `[targeting:player]` `[notargeting:tank]` | | pfUI (for tank) | +| `istank` | Unit is marked as tank | ✅ | — | `[istank]` `[@focus,istank]` | | pfUI | +| `tag` | Target is tapped by anyone | ✅ | — | `[tag]` `[notag]` | | | +| `mytag` | Target is tapped by you | ✅ | — | `[mytag]` `[nomytag]` | | | +| `othertag` | Target is tapped by someone else | ✅ | — | `[othertag]` `[noothertag]` | | | +| ***Range & Position*** | | | | | | | +| `distance` | Distance in yards | ✅ | ✅ | `[distance:<40]` | | UnitXP_SP3 | +| `meleerange` | In melee range / count in melee | ✅ | ✅ | `[meleerange]` `[meleerange:>1]` | | UnitXP_SP3 | +| `inrange` | In spell range / count in range | ✅ | ✅ | `[inrange:Charge]` `[inrange:Multi-Shot>1]` | | UnitXP_SP3 (count) | +| `outrange` | Out of spell range / count out | ✅ | ✅ | `[outrange:Charge]` `[outrange:Charge>0]` | | UnitXP_SP3 (count) | +| `insight` | In line of sight / count in LoS | ✅ | ✅ | `[insight]` `[insight:>0]` | | UnitXP_SP3 | +| `behind` | Behind target / count behind | ✅ | ✅ | `[behind]` `[behind:>=2]` | | UnitXP_SP3 | +| `multiscan` | Scan enemies by priority, soft-cast | — | ✅ | `[multiscan:nearest]` `[multiscan:skull]` | | UnitXP_SP3 | +| ***Immunity*** | | | | | | | +| `immune` | Target is immune (school or CC type) | ✅ | ✅ | `[noimmune:fire]` `[noimmune:stun]` | | | +| `resisted` | Last spell was resisted | ✅ | ✅ | `[resisted]` `[resisted:full/partial]` | | | +| ***Addon Integrations*** | | | | | | | +| `threat` | Threat % on target (100 = pull) | ✅ | ✅ | `[threat:>80]` | | TWThreat | +| `ttk` | Time to kill target (seconds) | ✅ | ✅ | `[ttk:<10]` | | TimeToKill | +| `tte` | Time to execute threshold (seconds) | ✅ | ✅ | `[tte:<5]` | | TimeToKill | -**Multi-Unit Count Mode:** Add operator + number to count enemies matching the condition (requires UnitXP). Operators: `>`, `<`, `>=`, `<=`, `=`, `~=` +### Multi-Unit Count Mode + +Range and position conditionals (`meleerange`, `behind`, `insight`, `inrange`, `outrange`) support counting enemies when given an operator + number. Requires UnitXP_SP3. Operators: `>`, `<`, `>=`, `<=`, `=`, `~=` ```lua /cast [meleerange:>1] Whirlwind -- AoE if 2+ enemies in melee @@ -168,126 +286,53 @@ All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff /cast [insight:>0] Arcane Explosion -- AoE if any enemy in LoS ``` -### Equipment -| Conditional | Example | Description | -|-------------|---------|-------------| -| `equipped` | `[equipped:Daggers]` | Item/type equipped | -| `mhimbue` | `[mhimbue]` `[mhimbue:Instant_Poison]` | Main-hand has temporary imbue | -| `ohimbue` | `[ohimbue]` `[ohimbue:Crippling_Poison]` | Off-hand has temporary imbue | +### Multiscan (Target Scanning) + +Scans enemies and soft-casts without changing your target. Requires UnitXP_SP3. Scanned targets must be in combat with player (exceptions: current target and `@unit` specified in macro). + +| Priority | Description | +|----------|-------------| +| `nearest` | Closest enemy | +| `farthest` | Farthest enemy | +| `highesthp` / `lowesthp` | Highest/lowest HP % | +| `highestrawhp` / `lowestrawhp` | Highest/lowest raw HP | +| `markorder` | First mark in kill order (skull→cross→square→moon→triangle→diamond→circle→star) | +| `skull` `cross` `square` `moon` `triangle` `diamond` `circle` `star` | Specific raid mark | -**Imbue Conditionals (Poisons, Oils, Sharpening Stones):** ```lua -[mhimbue] -- Has any temporary imbue -[mhimbue:Instant_Poison] -- Has specific imbue (tooltip match) -[mhimbue:<300] -- Imbue expires in < 300 seconds (5 min) -[mhimbue:Instant_Poison<300] -- Specific imbue with time check -[mhimbue:>#5] -- Has > 5 charges remaining -[nomhimbue] -- No temporary imbue (apply needed) +/cast [multiscan:nearest,nodebuff:Rake] Rake +/cast [multiscan:skull,harm] Eviscerate +/cast [multiscan:nearest,notargeting:tank,harm] Taunt ``` -**Note:** Temporary imbues are detected via tooltip time/charge markers, filtering out permanent enchants like Crusader. +### Reference Tables -### CC & Immunity -| Conditional | Example | Description | -|-------------|---------|-------------| -| `cc` | `[cc]` `[cc:stun/fear]` | Target has CC effect | -| `mycc` | `[mycc]` `[mycc:silence]` | Player has CC effect | -| `immune` | `[immune:fire]` `[immune:stun]` | Target IS immune (skip cast) | -| `noimmune` | `[noimmune]` `[noimmune:bleed]` | Target NOT immune (allow cast) | -| `resisted` | `[resisted]` `[resisted:full/partial]` | Last spell was resisted | +**CC Types** (for `[cc]`, `[mycc]`, `[immune]`): -**CC Types:** stun, fear, root, snare/slow, sleep, charm, polymorph, banish, horror, disorient, silence, disarm, daze, freeze, shackle +| Type | Examples | +|------|----------| +| `stun` | Cheap Shot, Kidney Shot, HoJ, Bash, Gouge, Sap | +| `fear` | Fear, Psychic Scream, Howl of Terror | +| `root` | Entangling Roots, Frost Nova | +| `snare` / `slow` | Hamstring, Wing Clip, Crippling Poison | +| `sleep` | Hibernate, Wyvern Sting | +| `charm` | Mind Control, Seduction | +| `polymorph` | Polymorph (all variants) | +| `banish` | Banish | +| `horror` | Death Coil, Intimidating Shout | +| `disorient` | Scatter Shot, Blind | +| `silence` | Silence, Counterspell | +| `disarm` | Disarm | +| `daze` | Daze effects | +| `freeze` | Freeze effects | +| `shackle` | Shackle Undead | **Loss-of-control** (checked by bare `[cc]`): stun, fear, sleep, charm, polymorph, banish, horror, freeze, disorient, shackle -**Damage Schools:** physical, fire, frost, nature, shadow, arcane, holy, bleed +**Damage Schools** (for `[immune]`): physical, fire, frost, nature, shadow, arcane, holy, bleed -### Addon Integrations -| Conditional | Addon | Example | Description | -|-------------|-------|---------|-------------| -| `swingtimer` | SP_SwingTimer | `[swingtimer:<15]` | Swing % elapsed | -| `stimer` | SP_SwingTimer | `[stimer:>80]` | Alias for swingtimer | -| `threat` | TWThreat | `[threat:>80]` | Threat % (100=pull) | -| `ttk` | TimeToKill | `[ttk:<10]` | Time to kill (seconds) | -| `tte` | TimeToKill | `[tte:<5]` | Time to execute (20% HP) | -| `cursive` | Cursive | `[cursive:Rake>3]` | GUID debuff tracking | -| `moving` | MonkeySpeed | `[moving:>100]` | Speed % (100=normal run) | -| `targeting:tank` | pfUI | `[targeting:tank]` | Target is attacking any tank | -| `istank` | pfUI | `[istank]` | Target is marked as tank | +**Swing/Hit Types** (for `[lastswing]`, `[incominghit]`): -### pfUI Tank Integration - -Identify loose mobs in trash pulls by checking if enemies are targeting designated tanks. Works with pfUI's tank marking systems. - -**Tank Conditionals:** -| Conditional | Description | -|-------------|-------------| -| `[targeting:tank]` | Target IS attacking any player marked as tank | -| `[notargeting:tank]` | Target is NOT attacking any tank (loose mob!) | -| `[istank]` | Target unit IS marked as tank | -| `[noistank]` | Target unit is NOT marked as tank | - -**Setting Up Tanks in pfUI:** - -1. **Nameplate Off-Tank Names** (recommended): `/pfui` → Nameplates → Off-Tank Names - - Add tank names separated by `#`: `#TankName1#TankName2#TankName3` - - These names will show different colored nameplates AND work with `[targeting:tank]` - -2. **Raid Frame Toggle**: Right-click a player in raid frames → "Toggle as Tank" - - Only works when raid frames are visible - -**Example Macros:** -```lua --- Pick up loose mobs (not targeting any tank) -/cast [multiscan:nearest,notargeting:tank,harm] Taunt - --- Only taunt if mob is targeting you (tank) -/cast [targeting:player] Sunder Armor - --- Assist tanks - attack what they're tanking -/cast [multiscan:nearest,targeting:tank,harm] Sunder Armor - --- Emergency taunt on loose mob hitting a healer -/cast [notargeting:tank,notargeting:player] Taunt -``` - -**Debug Command:** -``` -/cleveroid tankdebug -``` -Shows all marked tanks, current target info, and conditional results. - -### Warrior Slam Conditionals -For optimizing Slam rotations without clipping auto-attacks: - -| Conditional | Description | -|-------------|-------------| -| `noslamclip` | True if Slam NOW won't clip auto-attack | -| `slamclip` | True if Slam NOW WILL clip auto-attack | -| `nonextslamclip` | True if instant NOW won't cause NEXT Slam to clip | -| `nextslamclip` | True if instant NOW WILL cause NEXT Slam to clip | - -```lua -/cast [noslamclip] Slam -/cast [slamclip] Heroic Strike -- Use HS when past slam window -/cast [nonextslamclip] Bloodthirst -``` - -### Auto-Attack Conditionals (Nampower v2.24+) - -Track player melee swings and incoming attacks. Requires `NP_EnableAutoAttackEvents=1` CVar. - -| Conditional | Example | Description | -|-------------|---------|-------------| -| `lastswing` | `[lastswing]` | Any melee swing in last 5 seconds | -| `lastswing:type` | `[lastswing:crit]` | Last swing was specific type | -| `lastswing:48] Shred -/cast [myrawpower:>40] Claw - --- WITH /firstaction: Only Shred casts if energy >= 48, Claw is skipped -/firstaction -/cast [myrawpower:>48] Shred -/cast [myrawpower:>40] Claw - --- Mixed mode: priority section + always-queue section -/firstaction -/cast [myrawpower:>48] Shred -- Priority: only one of these -/cast [myrawpower:>40] Claw -/nofirstaction -/cast Tiger's Fury -- Always evaluates (can queue alongside) -/startattack - --- With conditionals -/firstaction [group] -- Priority mode only in groups -/cast [myrawpower:>48] Shred -/cast [myrawpower:>40] Claw -``` - -**Child Macro Behavior:** `/firstaction` and `/nofirstaction` in child macros (called via `{MacroName}`) affect subsequent lines in the parent macro. Think of the child's lines as being "inlined" at the call site. - -**Comparison:** -- `/stopmacro [cond]` - Stop if condition is true (regardless of cast success) -- `/skipmacro [cond]` - Stop current submacro only -- `/firstaction [cond]` - Stop on first **successful** cast/use (priority mode) -- `/nofirstaction [cond]` - Re-enable multi-queue behavior after `/firstaction` - -### Commands without Conditional Support - -| Command | Description | -|---------|-------------| -| `/retarget` | Clear invalid target, target nearest enemy | -| `/runmacro Name` | Execute macro by name (use `{MacroName}` in `/cast` for conditionals) | -| `/rl` | Reload UI | - -### UnitXP 3D Enemy Scanning - -`/target` with any conditionals automatically uses UnitXP 3D scanning to find enemies in line of sight, even without nameplates visible. The only exception is `[help]` without `[harm]` (friendly-only targeting). - -```lua -/target [name:Onyxia] -- Scans for exact name match -/target [nodead,harm] -- Scans for living enemies -/target [hp:<30] -- Scans for low HP enemies -/target [cc:stun] -- Scans for stunned enemies - --- Kara 40 Mage Incantagos example -- -/target [name:Red_Affinity] -/cast [name:Red_Affinity] Fireball -/target [name:Blue_Affinity] -/cast [name:Blue_Affinity] Frostbolt -/target [name:Mana_Affinity] -/cast [name:Mana_Affinity] Arcane Missiles -``` - -If no matching target is found, your original target is preserved. +**Speed Reference** (for `[moving:>N]`): 0 = still, 100 = run, 160 = mount, 200 = epic mount. --- @@ -473,6 +364,12 @@ Auto-learns debuff durations from your casts. 335+ spells pre-configured. /cast Wrath -- Filler ``` +**Notes:** +- Existence checks (`[nodebuff]`, `[debuff]`) detect ANY debuff on target +- Time-remaining checks (`[debuff:X<5]`) use internal tracking from your casts +- Shared debuffs (Sunder, Faerie Fire) are detected from any source +- [Cursive](https://github.com/pepopo978/Cursive) provides GUID-based tracking with better accuracy (handles target switching, pending casts, debuff cap) + ### Combo Point Tracking Tracks finisher durations (Rip, Rupture, Kidney Shot) accounting for combo points spent. @@ -487,9 +384,9 @@ Automatically adjusts tracked durations for talents (Imp. Gouge, Taste for Blood --- -## Immunity Tracking Guide +## Immunity Tracking -The addon auto-learns NPC immunities from combat. When a spell fails with "immune", the addon remembers it for that NPC. +Auto-learns NPC immunities from combat. When a spell fails with "immune", the addon remembers it for that NPC. ### Using `[noimmune]` @@ -500,172 +397,21 @@ The addon auto-learns NPC immunities from combat. When a spell fails with "immun | `[noimmune:bleed]` | Bleed immunity specifically | | `[noimmune:stun]` | Stun CC immunity | -### Common Examples +**Split damage spells** (Rake, Pounce, Garrote) have initial physical hit + bleed DoT. `[noimmune]` automatically checks **both** components. ```lua --- Fire Mage: Skip immune targets -/cast [noimmune] Fireball - --- Warlock: Check shadow immunity -/cast [noimmune:shadow] Corruption - --- Rogue: Skip stun-immune targets -/cast [noimmune:stun] Cheap Shot -/cast Sinister Strike - --- Druid: Check fear immunity for Hibernate -/cast [noimmune:sleep,type:Beast/Dragonkin] Hibernate +/cast [noimmune] Rake -- Checks physical AND bleed +/cast [noimmune:stun] Cheap Shot -- Checks stun immunity +/cast [noimmune:shadow] Corruption -- Checks shadow immunity ``` -### Split Damage Spells (Rake, Pounce, Garrote) - -These spells have an initial physical hit + a bleed DoT. The addon **automatically checks BOTH immunities**: - +### Manual Immunity Commands ```lua --- Checks both physical AND bleed immunity -/cast [noimmune] Rake - --- With time check using Cursive -/cast [noimmune, cursive:Rake<1.5] Rake - --- Multi-DoT rotation -/cast [noimmune, nocursive:Rake] Rake -/cast [noimmune, nocursive:Rip, combo:>=4] Rip -``` - -`[noimmune]` for these spells returns false (skip cast) if target is immune to **either** component. You can also check specific schools: -- `[noimmune:physical]` - only checks physical immunity -- `[noimmune:bleed]` - only checks bleed immunity - -### Pure Bleed Spells (Rip, Rupture, Rend) - -These are pure bleeds with no initial hit: - -```lua --- Rip (pure bleed) -/cast [noimmune, combo:>=4] Rip - --- Rupture (pure bleed) -/cast [noimmune, combo:>=4] Rupture -``` - -### Manually Adding Immunities - -If you know an NPC is immune before fighting: -```lua -/cleveroid addimmune "Boss Name" bleed -- Permanent bleed immunity -/cleveroid addimmune "Boss Name" fire "Shield" -- Fire immune only when buffed -/cleveroid addccimmune "Boss Name" stun -- Permanent stun immunity -``` - -### Viewing Learned Immunities - -```lua -/cleveroid listimmune -- All school immunities -/cleveroid listimmune bleed -- Only bleed immunities -/cleveroid listccimmune -- All CC immunities -/cleveroid listccimmune stun -- Only stun immunities -``` - ---- - -## Debuff Tracking Guide - -### Built-in Tracking (`[debuff]`) - -The addon checks debuffs on the target: - -```lua --- Basic: Only cast if target doesn't have Moonfire -/cast [nodebuff:Moonfire] Moonfire - --- Time check: Refresh when < 4 seconds left -/cast [debuff:Moonfire<4] Moonfire -``` - -**Notes:** -- Existence checks (`[nodebuff]`, `[debuff]`) detect ANY debuff on target -- Time-remaining checks (`[debuff:X<5]`) use internal tracking from your casts -- Shared debuffs (Sunder, Faerie Fire) are detected from any source - -### Cursive Integration (`[cursive]`) - -[Cursive](https://github.com/avitasia/Cursive) provides more accurate GUID-based tracking: - -```lua --- Check if target has Rake (any time remaining) -/cast [nocursive:Rake] Rake - --- Refresh when < 3 seconds remaining -/cast [cursive:Rake<3] Rake - --- Complex: Only if missing OR about to expire -/cast [nocursive:Rake>1.5] Rake -``` - -**Advantages of Cursive:** -- GUID-based (survives target switching) -- Tracks pending casts -- Works at debuff cap -- More accurate timing - -### MonkeySpeed Integration (`[moving]`) - -[MonkeySpeed](https://github.com/jrc13245/MonkeySpeed) provides accurate movement speed detection via SuperWoW's `UnitPosition` API: - -```lua --- Only cast if standing still -/cast [nomoving] Aimed Shot - --- Cast instant ability while moving -/cast [moving] Arcane Shot - --- Check speed percentage (100 = normal run speed) -/cast [moving:>100] Sprint -- Already faster than normal -/cast [moving:<50] Escape Artist -- Currently slowed - --- Combined with other conditionals -/cast [nomoving, harm] Aimed Shot -/cast [moving, harm] Arcane Shot -``` - -**Speed Reference:** -| Speed | Description | -|-------|-------------| -| `0` | Standing still | -| `100` | Normal run speed | -| `160` | Level 40 mount | -| `200` | Epic mount | - -**Notes:** -- Basic `[moving]`/`[nomoving]` works without MonkeySpeed (position fallback) -- Speed comparisons (`[moving:>100]`) require MonkeySpeed addon -- Speed values vary with buffs/debuffs affecting movement - -### Recommended DoT Macro Patterns - -**Druid Feral (with Cursive):** -```lua -#showtooltip -/cast [noimmune, nocursive:Rake] Rake -``` - -**Druid Feral (without Cursive):** -```lua -#showtooltip -/cast [noimmune, nodebuff] Rake -``` - -**Warlock Multi-DoT:** -```lua -#showtooltip -/cast [noimmune:shadow, nodebuff] Corruption -``` - -**Rogue Rupture:** -```lua -#showtooltip -/cast [noimmune, nodebuff, combo:>=4] Rupture +/cleveroid addimmune "Boss Name" bleed -- Permanent immunity +/cleveroid addimmune "Boss Name" fire "Shield" -- Conditional (while buffed) +/cleveroid addccimmune "Boss Name" stun -- CC immunity +/cleveroid listimmune [school] -- View school immunities +/cleveroid listccimmune [type] -- View CC immunities ``` --- @@ -673,25 +419,15 @@ The addon checks debuffs on the target: ## Settings ```lua -/cleveroid -- View settings -/cleveroid realtime 0|1 -- Instant updates (more CPU) -/cleveroid refresh 1-10 -- Update rate in Hz -/cleveroid debug 0|1 -- Debug messages -/cleveroid learn -- Manually set spell duration -/cleveroid forget -- Forget duration(s) -``` - -### Immunity Commands -```lua -/cleveroid listimmune [school] -/cleveroid addimmune "NPC" school [buff] -/cleveroid listccimmune [type] -/cleveroid addccimmune "NPC" type [buff] -``` - -### Combo Tracking -```lua -/combotrack show|clear|debug +/cleveroid -- View settings +/cleveroid realtime 0|1 -- Instant tooltip updates (more CPU) +/cleveroid refresh 1-10 -- Tooltip update rate in Hz +/cleveroid debug 0|1 -- Debug messages +/cleveroid learn -- Manually set spell duration +/cleveroid forget -- Forget learned duration(s) +/cleveroid debuffdebug [spell] -- Debug debuff tracking on target +/cleveroid tankdebug -- Debug pfUI tank conditionals +/combotrack show|clear|debug -- Combo point tracking ``` --- @@ -702,22 +438,14 @@ The addon checks debuffs on the target: **Action Bars:** Blizzard, [pfUI](https://github.com/jrc13245/pfUI), Bongos, Discord Action Bars -**Integrations:** SP_SwingTimer, TWThreat, TimeToKill, QuickHeal, Cursive (with mouseover support for DoT timer bars), ClassicFocus, SuperMacro, MonkeySpeed +**Integrations:** [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer), [TWThreat](https://github.com/MarcelineVQ/TWThreat), [TimeToKill](https://github.com/jrc13245/TimeToKill), [QuickHeal](https://github.com/jrc13245/QuickHeal), [Cursive](https://github.com/pepopo978/Cursive), [ClassicFocus](https://github.com/wtfcolt/Addons-for-Vanilla-1.12.1-CFM/tree/master/ClassicFocus), [SuperMacro](https://github.com/jrc13245/SuperMacro-turtle-SuperWoW), [MonkeySpeed](https://github.com/jrc13245/MonkeySpeed) > **Note:** For pfUI users, the [jrc13245/pfUI fork](https://github.com/jrc13245/pfUI) includes native SuperCleveRoidMacros integration for proper cooldown, icon, and tooltip display on conditional macros. --- -## Known Issues - -- Unique macro names required (no blanks, duplicates, or spell names) -- Reactive abilities must be on action bars for detection -- HealComm requires MarcelineVQ's [LunaUnitFrames](https://github.com/MarcelineVQ/LunaUnitFrames) for SuperWoW compatibility - ---- - ## Credits -Based on [CleverMacro](https://github.com/DanielAdolfsson/CleverMacro) and [Roid-Macros](https://github.com/DennisWG/Roid-Macros). Cursive integration by Avitasia. +Based on [CleverMacro](https://github.com/DanielAdolfsson/CleverMacro) and [Roid-Macros](https://github.com/DennisWG/Roid-Macros). MIT License diff --git a/Utility.lua b/Utility.lua index 0ac6afb..cbf83fa 100644 --- a/Utility.lua +++ b/Utility.lua @@ -1972,7 +1972,7 @@ function lib:ShouldApplyDebuffRank(targetGUID, newSpellID) end -- Also clean up pfUI's tracking to prevent it from showing old ranks - if pfUI and pfUI.api and pfUI.api.libdebuff and targetName then + if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff and targetName then local pflib = pfUI.api.libdebuff if pflib.objects and pflib.objects[targetName] then @@ -2084,10 +2084,9 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) lib.objects[guid][spellID] = rec - -- PFUI INTEGRATION: Inject all tracked debuffs into pfUI's libdebuff - -- This allows pfUI to show accurate timers for ALL tracked debuffs (player + other casters) - -- For shared debuffs like Sunder Armor, we want pfUI to show timers regardless of who cast it - if pfUI and pfUI.api and pfUI.api.libdebuff and unitName then + -- PFUI INTEGRATION: Inject all tracked debuffs into pfUI's libdebuff (pre-7.6 only) + -- pfUI 7.6+ handles all duration tracking internally via GetUnitField + if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and not CleveRoids.hasPfUI76 then local pflib = pfUI.api.libdebuff local spellName = SpellInfo(spellID) @@ -2351,8 +2350,8 @@ local function SeedUnit(unit) existing.start = GetTime() existing.duration = duration - -- PFUI INTEGRATION: Inject refreshed timer into pfUI - if pfUI and pfUI.api and pfUI.api.libdebuff and unitName then + -- PFUI INTEGRATION: Inject refreshed timer into pfUI (pre-7.6 only) + if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and not CleveRoids.hasPfUI76 then local pflib = pfUI.api.libdebuff local spellName = SpellInfo(spellID) if spellName and pflib.AddEffect then @@ -2440,8 +2439,8 @@ local function SeedUnit(unit) existing.start = GetTime() existing.duration = duration - -- PFUI INTEGRATION: Inject refreshed timer into pfUI - if pfUI and pfUI.api and pfUI.api.libdebuff and unitName then + -- PFUI INTEGRATION: Inject refreshed timer into pfUI (pre-7.6 only) + if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and not CleveRoids.hasPfUI76 then local pflib = pfUI.api.libdebuff local spellName = SpellInfo(spellID) if spellName and pflib.AddEffect then @@ -2714,7 +2713,7 @@ function lib.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID) -- DON'T call pfUI's AddEffect - just update the existing entry directly -- pfUI will pick up the new duration through our GetDuration/UnitDebuff hooks - if pfUI and pfUI.api and pfUI.api.libdebuff then + if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then local pflib = pfUI.api.libdebuff local ripSpellName = SpellInfo(ripSpellID) local baseName = ripSpellName and string.gsub(ripSpellName, "%s*%(Rank %d+%)", "") or "Rip" @@ -2827,7 +2826,7 @@ function lib.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID) -- DON'T call pfUI's AddEffect - just update the existing entry directly -- pfUI will pick up the new duration through our GetDuration/UnitDebuff hooks - if pfUI and pfUI.api and pfUI.api.libdebuff then + if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then local pflib = pfUI.api.libdebuff local rakeSpellName = SpellInfo(rakeSpellID) local baseName = rakeSpellName and string.gsub(rakeSpellName, "%s*%(Rank %d+%)", "") or "Rake" @@ -4010,9 +4009,9 @@ ev:SetScript("OnEvent", function() ) end - -- CRITICAL: Update pfUI's duration database directly - if pfUI and pfUI.api and pfUI.api.libdebuff and pfUI.api.libdebuff.debuffs then - -- pfUI stores durations by spell name in its debuffs table + -- Update pfUI's duration database directly (pre-7.6 only) + -- pfUI 7.6+ handles combo durations internally via GetStoredComboPoints() + if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff and pfUI.api.libdebuff.debuffs then pfUI.api.libdebuff.debuffs[baseName] = duration if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( @@ -5203,7 +5202,7 @@ evLearn:SetScript("OnEvent", function() -- Sync refresh to pfUI local targetName = lib.guidToName[targetGUID] - if pfUI and pfUI.api and pfUI.api.libdebuff and targetName then + if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff and targetName then local pflib = pfUI.api.libdebuff local spellName = SpellInfo(flameShockID) if spellName and pflib.AddEffect then @@ -5369,17 +5368,14 @@ evJudgement:SetScript("OnEvent", function() ) end - -- Also sync to pfUI if it's loaded - if pfUI and pfUI.api and pfUI.api.libdebuff then + -- Also sync to pfUI if it's loaded (pre-7.6 only) + if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then local targetName = lib.guidToName[targetGUID] or UnitName("target") local targetLevel = UnitLevel("target") or 0 local spellName = SpellInfo(spellID) if spellName and targetName then - -- Remove rank from spell name to match pfUI's format local effectName = string.gsub(spellName, "%s*%(Rank %d+%)", "") - - -- Refresh in pfUI's tracking pfUI.api.libdebuff:AddEffect(targetName, targetLevel, effectName, rec.duration, "player") if CleveRoids.debug then