fix target overflowframe and possible immunity system fix

This commit is contained in:
Jrc13245
2026-03-17 18:25:20 -04:00
parent 11e33fc0d3
commit 137e08a209
6 changed files with 303 additions and 163 deletions
+8 -8
View File
@@ -2922,7 +2922,7 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
local checked = {}
-- 1. Current target (use "target" token for best API compatibility)
if UnitExists("target") and UnitCanAttack("player", "target") and not UnitIsDeadOrGhost("target") then
if UnitExists("target") and UnitCanAttack("player", "target") and not CleveRoids.IsUnitDeadOrGhost("target") then
local guid = CleveRoids.GetGUID("target")
if guid then
checked[guid] = true
@@ -2937,7 +2937,7 @@ function CleveRoids.CountEnemiesMatching(checkFunc)
local function tryUnit(unit)
if not UnitExists(unit) then return end
if not UnitCanAttack("player", unit) then return end
if UnitIsDeadOrGhost(unit) then return end
if CleveRoids.IsUnitDeadOrGhost(unit) then return end
local guid = CleveRoids.GetGUID(unit)
if not guid or checked[guid] then return end
checked[guid] = true
@@ -5535,7 +5535,7 @@ end
--- @return boolean
function CleveRoids.IsUnitInMeleeRange(unit, cleaveRange)
if not UnitExists(unit) then return false end
if UnitIsDead(unit) then return false end
if CleveRoids.IsUnitDead(unit) then return false end
local threshold = cleaveRange and 5 or 2
@@ -5924,22 +5924,22 @@ CleveRoids.Keywords = {
dead = function(conditionals)
if not conditionals.target then return false end
return UnitIsDeadOrGhost(conditionals.target)
return CleveRoids.IsUnitDeadOrGhost(conditionals.target)
end,
alive = function(conditionals)
if not conditionals.target then return false end
return not UnitIsDeadOrGhost(conditionals.target)
return not CleveRoids.IsUnitDeadOrGhost(conditionals.target)
end,
noalive = function(conditionals)
if not conditionals.target then return false end
return UnitIsDeadOrGhost(conditionals.target)
return CleveRoids.IsUnitDeadOrGhost(conditionals.target)
end,
nodead = function(conditionals)
if not conditionals.target then return false end
return not UnitIsDeadOrGhost(conditionals.target)
return not CleveRoids.IsUnitDeadOrGhost(conditionals.target)
end,
reactive = function(conditionals)
@@ -8615,7 +8615,7 @@ CleveRoids.STATIC_CONDITIONALS = {
--- @return number|nil Score (lower = better) or nil if invalid candidate
function CleveRoids.GetMultiscanScore(unit, priority, currentTargetGuid, specifiedUnitGuid)
if not unit or not UnitExists(unit) then return nil end
if UnitIsDeadOrGhost(unit) then return nil end
if CleveRoids.IsUnitDeadOrGhost(unit) then return nil end
-- Must be attackable
if not UnitCanAttack("player", unit) then return nil end
+1 -1
View File
@@ -85,7 +85,7 @@ end
-- Define original implementations before hooking them.
-- This ensures we have a fallback for non-conditional use.
local StartAttack = function(msg)
if not UnitExists("target") or UnitIsDead("target") then TargetNearestEnemy() end
if not UnitExists("target") or CleveRoids.IsUnitDead("target") then TargetNearestEnemy() end
-- Check both event-based flag AND action bar state for reliable detection
local isAttacking = CleveRoids.CurrentSpell.autoAttack
if not isAttacking then
+2 -2
View File
@@ -2780,7 +2780,7 @@ function CleveRoids.DoTarget(msg)
end
local function IsGuidValid(unitTok, conds)
if not unitTok or not UnitExists(unitTok) or UnitIsDeadOrGhost(unitTok) then
if not unitTok or not UnitExists(unitTok) or CleveRoids.IsUnitDeadOrGhost(unitTok) then
return false
end
local orig = conds.target
@@ -3042,7 +3042,7 @@ end
-- PERFORMANCE: Module-level action to avoid closure allocation per call
local function _startAttackAction()
if not UnitExists("target") or UnitIsDead("target") then TargetNearestEnemy() end
if not UnitExists("target") or CleveRoids.IsUnitDead("target") then TargetNearestEnemy() end
-- Check both event-based flag AND action bar state for reliable detection
local isAttacking = CleveRoids.CurrentSpell.autoAttack
if not isAttacking then
+66 -10
View File
@@ -220,6 +220,25 @@ local function GetPlayerOverflowBuffs()
local overflowBuffs = CleveRoids.OverflowBuffs
if not overflowBuffs then return results end
-- If the player is no longer buff-capped (< 32 buff slots occupied),
-- overflow entries are stale — the server doesn't migrate overflow buffs
-- into freed slots, so they've expired. Wipe and return early.
if _G.GetPlayerAuraDuration then
local buffSlotCount = 0
for slot = 0, 31 do
local sid = _G.GetPlayerAuraDuration(slot)
if sid and sid > 0 then
buffSlotCount = buffSlotCount + 1
end
end
if buffSlotCount < 32 then
for k in pairs(overflowBuffs) do
overflowBuffs[k] = nil
end
return results
end
end
-- Build set of spell IDs in debuff slots (32-47) to filter out debuffs
-- that were incorrectly added due to AURA_CAST_ON_SELF race condition
-- (debuff slot not yet assigned when the event fired)
@@ -242,14 +261,23 @@ local function GetPlayerOverflowBuffs()
elseif debuffSpellIds and debuffSpellIds[spellId] then
-- Debuff in a visible debuff slot — not an overflow buff, clean up
overflowBuffs[spellId] = nil
elseif entry.timestamp and entry.durationSec then
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
elseif entry.timestamp then
local permanent = not entry.durationSec or entry.durationSec <= 0
if permanent then
table.insert(results, {
spellId = spellId,
remaining = remaining,
remaining = 0,
source = "player",
})
else
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
table.insert(results, {
spellId = spellId,
remaining = remaining,
source = "player",
})
end
end
end
end
@@ -270,6 +298,28 @@ local function GetTargetOverflowBuffs()
local trackingData = CleveRoids.AllCasterAuraTracking
if not trackingData or not trackingData[targetGuid] then return results end
-- Only show overflow for targets that are buff-capped (all 32 buff slots occupied).
-- GetUnitField "aura" returns spell IDs for all 48 slots including hidden auras,
-- so counting occupied buff slots 1-32 gives the true buff count in real-time.
-- Without this gate, AllCasterAuraTracking (which stores ALL aura casts, not
-- just overflow) would show false-positive overflow entries on uncapped targets.
if not testMode then
local buffSlotCount = 0
local auras = _G.GetUnitField and _G.GetUnitField("target", "aura")
if auras then
for slot = 1, 32 do
if auras[slot] and auras[slot] > 0 then
buffSlotCount = buffSlotCount + 1
end
end
if buffSlotCount < 32 then return results end
else
-- Fallback: auraCapStatus from last AURA_CAST_ON_OTHER event
local capEntry = CleveRoids.AuraCapStatus.targetCapStatus[targetGuid]
if not capEntry or not capEntry.buffCapped then return results end
end
end
-- Build set of visible aura spell IDs on target (buffs + debuffs)
-- Use spellId matching instead of texture matching for accuracy:
-- texture matching fails when icon isn't cached (false positive) or when
@@ -429,13 +479,19 @@ local function RefreshDurations()
local data = playerIcons[i]
if data.spellId then
local entry = CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[data.spellId]
if entry and entry.timestamp and entry.durationSec then
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
data.remaining = remaining
data.duration:SetText(FormatDuration(remaining))
if entry and entry.timestamp then
local permanent = not entry.durationSec or entry.durationSec <= 0
if permanent then
data.remaining = 0
data.duration:SetText("")
else
anyExpired = true
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
data.remaining = remaining
data.duration:SetText(FormatDuration(remaining))
else
anyExpired = true
end
end
else
anyExpired = true
+2 -1
View File
@@ -284,7 +284,8 @@ end
-- Immunity data version - increment this when changing immunity data format
-- This will cause all immunity data to be reset on addon update
-- v3: Fixed Master Strike false physical immunity recording (split CC spell handling)
CleveRoids.IMMUNITY_DATA_VERSION = 3
-- v4: Fixed false immunity recording when target dies with spells in-flight (dead = IMMUNE)
CleveRoids.IMMUNITY_DATA_VERSION = 4
-- Call on next frame to ensure everything is loaded
local initFrame = CreateFrame("Frame")
+224 -141
View File
@@ -82,6 +82,24 @@ function CleveRoids.GetGUID(unit)
return nil
end
-- Check if a unit is truly dead using Nampower health (sees through feign death)
-- Returns: true if unit HP is 0 (truly dead), false if alive or feigning
function CleveRoids.IsUnitDead(unit)
if CleveRoids.NampowerAPI then
local hp = CleveRoids.NampowerAPI.GetUnitHealth(unit)
if hp ~= nil then
return hp == 0
end
end
return UnitIsDead(unit)
end
-- Check if a unit is truly dead or a ghost using Nampower health
-- Returns: true if unit HP is 0 or unit is a ghost
function CleveRoids.IsUnitDeadOrGhost(unit)
return CleveRoids.IsUnitDead(unit) or UnitIsGhost(unit)
end
-- Hidden tooltip for scanning spell info
local SpellScanTooltip = nil
@@ -703,6 +721,8 @@ lib.slotOwnership = lib.slotOwnership or {} -- [targetGUID][auraSlot] = {casterG
lib.allAuraCasts = lib.allAuraCasts or {} -- [targetGUID][spellName][casterGuid] = {startTime, duration, rank}
lib.pendingCasts = lib.pendingCasts or {} -- [targetGUID][spellName] = {casterGuid, rank, time, comboPoints}
lib.recentMisses = lib.recentMisses or {} -- [targetGUID][spellName] = {time, spellId, targetName, reason} for miss/dodge/parry detection
lib.recentDeaths = lib.recentDeaths or {} -- [targetGUID] = GetTime() timestamp of UNIT_DIED (prevents false immunity on dead targets)
lib.recentCCHits = lib.recentCCHits or {} -- [targetGUID][ccType] = {count, lastHitTime} for DR tracking (prevents DR immunity → permanent)
lib.iconCache = lib.iconCache or {} -- [spellId] = texture (shared with pfUI 7.6 or standalone)
-- Buff tracking tables (parallel to debuff tables, standalone Nampower mode only)
@@ -1230,6 +1250,15 @@ function lib:CleanupStaleTrackingData()
end
end
-- Clean recentDeaths (dead target immunity guard)
if lib.recentDeaths then
for guid, deathTime in pairs(lib.recentDeaths) do
if (now - deathTime) > staleTime then
lib.recentDeaths[guid] = nil
end
end
end
-- Clean pendingCasts (including combo point data from SPELL_GO)
if lib.pendingCasts then
for guid, spells in pairs(lib.pendingCasts) do
@@ -2985,7 +3014,7 @@ end
local _GetTime = GetTime
local _UnitExists = UnitExists
local _UnitDebuff = UnitDebuff
local _UnitIsDead = UnitIsDead
local _IsUnitDead = CleveRoids.IsUnitDead
local _UnitName = UnitName
local _GetSpellRecField = GetSpellRecField
local _string_find = string.find
@@ -3018,6 +3047,23 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
end
_lastDelayedTrackingUpdate = currentTime
-- Process deferred guidToName cleanups
if lib._guidNameCleanupQueue then
local i = 1
while i <= table.getn(lib._guidNameCleanupQueue) do
local entry = lib._guidNameCleanupQueue[i]
if entry and currentTime >= entry.expiry then
-- Only remove if no active tracking data references this GUID
if not (lib.ownDebuffs[entry.guid] or lib.pendingCasts[entry.guid]) then
lib.guidToName[entry.guid] = nil
end
table.remove(lib._guidNameCleanupQueue, i)
else
i = i + 1
end
end
end
-- PERFORMANCE: Early exit if all queues are empty
-- NOTE: Use next() instead of [1] to handle array holes from table.remove()
-- Array holes can occur when combat log handlers remove entries mid-iteration
@@ -3067,14 +3113,18 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
-- Cache frequently accessed values
local hasSuperwow = CleveRoids.hasSuperwow
local hasExtendedTokens = CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.features
and CleveRoids.NampowerAPI.features.hasExtendedUnitTokens
local debug = CleveRoids.debug
-- Resolve a GUID to a queryable unit token
-- SuperWoW: returns GUID directly (SuperWoW extends WoW APIs to accept GUIDs)
-- Without SuperWoW: returns "target" if GUID matches current target, nil otherwise
-- Nampower v2.41+ extended tokens: GUID works as unit token if unit is in range
-- Fallback: returns "target" if GUID matches current target, nil otherwise
local function ResolveGUIDUnit(guid)
if not guid then return nil end
if hasSuperwow then return guid end
if hasExtendedTokens and _UnitExists(guid) then return guid end
local ctGUID = CleveRoids.GetGUID("target")
if ctGUID and ctGUID == guid then
return "target"
@@ -3196,7 +3246,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
if not isWhitelisted then
-- Check if target is dead - requires special handling
if _UnitIsDead(verifyUnit) then
if _IsUnitDead(verifyUnit) then
-- Target died - check if we saw "afflicted by" message before death
if pending.verifiedByAffliction then
-- We confirmed bleed landed via combat log before target died
@@ -3440,7 +3490,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
and pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil
if ccVerifyUnit then
-- Skip verification if target is dead (debuffs are removed on death)
if _UnitIsDead(ccVerifyUnit) then
if _IsUnitDead(ccVerifyUnit) then
ccVerified = true -- Assume CC landed, can't verify on dead target
if debug then
DEFAULT_CHAT_FRAME:AddMessage(
@@ -3503,7 +3553,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
end
-- If CC didn't land, check if it's immunity or debuff cap
if not ccVerified and ccVerifyUnit and not _UnitIsDead(ccVerifyUnit) then
if not ccVerified and ccVerifyUnit and not _IsUnitDead(ccVerifyUnit) then
-- totalDebuffs already counted above, reuse it
if totalDebuffs == 0 then
-- Count wasn't done (dead target check skipped counting), do it now
@@ -3530,12 +3580,14 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
lib.guidToName[pending.targetGUID] = resolvedTargetName
end
end
-- Try to resolve using SuperWoW GUID-based query
if not resolvedTargetName and hasSuperwow then
local name = _UnitName(pending.targetGUID)
if name and name ~= "Unknown" then
resolvedTargetName = name
lib.guidToName[pending.targetGUID] = resolvedTargetName
-- Try to resolve using SuperWoW or Nampower extended tokens GUID-based query
if not resolvedTargetName and (hasSuperwow or hasExtendedTokens) then
if _UnitExists(pending.targetGUID) then
local name = _UnitName(pending.targetGUID)
if name and name ~= "Unknown" then
resolvedTargetName = name
lib.guidToName[pending.targetGUID] = resolvedTargetName
end
end
end
end
@@ -3556,8 +3608,22 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
end
-- Skip immunity recording - damage landed, only CC was resisted
elseif totalDebuffs < DEBUFF_CAP_THRESHOLD then
-- Few debuffs = likely CC immunity
if resolvedTargetName and resolvedTargetName ~= "" and pending.ccType then
-- Check DR before recording as permanent CC immunity
local isDR = false
if pending.targetGUID and pending.ccType then
local drEntry = lib.recentCCHits[pending.targetGUID] and lib.recentCCHits[pending.targetGUID][pending.ccType]
if drEntry and (GetTime() - drEntry.lastHitTime) < 20 and drEntry.count >= 3 then
isDR = true
end
end
if isDR then
if debug then
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cff00aaff[CC DR Skip]|r %s on %s - likely DR immune (3+ recent %s hits), not recording permanent immunity",
pending.spellName or "Unknown", resolvedTargetName or "Unknown", pending.ccType or "Unknown")
)
end
elseif resolvedTargetName and resolvedTargetName ~= "" and pending.ccType then
CleveRoids.RecordCCImmunity(resolvedTargetName, pending.ccType, nil, pending.spellName)
if debug then
@@ -3590,6 +3656,15 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
if resolvedTargetName and pending.ccType then
CleveRoids.RemoveCCImmunity(resolvedTargetName, pending.ccType)
end
-- Track successful CC hit for DR detection
if pending.targetGUID and pending.ccType then
lib.recentCCHits[pending.targetGUID] = lib.recentCCHits[pending.targetGUID] or {}
local entry = lib.recentCCHits[pending.targetGUID][pending.ccType]
lib.recentCCHits[pending.targetGUID][pending.ccType] = {
count = (entry and entry.count or 0) + 1,
lastHitTime = GetTime(),
}
end
if debug then
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cff00ff00[CC Verified]|r %s landed on %s",
@@ -3669,7 +3744,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
local sharedVerifyUnit = not debuffVerified and not pending.spellGoHit and not pending.spellGoMissed
and pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil
if sharedVerifyUnit then
if _UnitIsDead(sharedVerifyUnit) then
if _IsUnitDead(sharedVerifyUnit) then
-- Target died - can't verify immunity, assume debuff landed
debuffVerified = true
if debug then
@@ -3864,6 +3939,18 @@ ev:SetScript("OnEvent", function()
CleveRoids.knownEnemyGuids[k] = nil
end
end
-- Wipe GUID-to-name cache — GUIDs can be reused across zones
if lib.guidToName then
for k in pairs(lib.guidToName) do
lib.guidToName[k] = nil
end
end
-- Wipe DR tracking — DR state is invalid in a new zone
if lib.recentCCHits then
for k in pairs(lib.recentCCHits) do
lib.recentCCHits[k] = nil
end
end
return
elseif event == "PLAYER_TARGET_CHANGED" then
@@ -5521,6 +5608,10 @@ ev:SetScript("OnEvent", function()
-- Get unit name before cleanup (for debug output)
local unitName = lib.guidToName[guid]
-- Track recent death to prevent false immunity recording
-- (dead targets report SPELL_MISS IMMUNE for in-flight spells)
lib.recentDeaths[guid] = GetTime()
-- Clean up all tracking data for this GUID immediately
if lib.ownDebuffs[guid] then
lib.ownDebuffs[guid] = nil
@@ -5543,6 +5634,9 @@ ev:SetScript("OnEvent", function()
if lib.recentMisses and lib.recentMisses[guid] then
lib.recentMisses[guid] = nil
end
if lib.recentCCHits and lib.recentCCHits[guid] then
lib.recentCCHits[guid] = nil
end
-- Clean up buff tracking tables
if lib.ownBuffCasts[guid] then
@@ -5583,9 +5677,9 @@ ev:SetScript("OnEvent", function()
CleveRoids.knownEnemyGuids[guid] = nil
end
-- Clean up GUID to name mapping (after 5 seconds to allow final lookups)
-- Actually, keep it for a bit in case we need it for immunity detection
-- lib.guidToName[guid] = nil
-- Deferred GUID-to-name cleanup (5s delay to allow immunity detection lookups)
lib._guidNameCleanupQueue = lib._guidNameCleanupQueue or {}
table.insert(lib._guidNameCleanupQueue, { guid = guid, expiry = GetTime() + 5 })
if CleveRoids.debug and unitName then
DEFAULT_CHAT_FRAME:AddMessage(
@@ -6007,7 +6101,7 @@ evCleanup:SetScript("OnEvent", function()
local isCurrentTarget = (targetGUID == guid)
-- Check if current target is dead
if isCurrentTarget and UnitIsDead("target") then
if isCurrentTarget and CleveRoids.IsUnitDead("target") then
lib.objects[guid] = nil
lib.guidToName[guid] = nil -- MEMORY: Clean up name mapping
else
@@ -7834,40 +7928,40 @@ local function ParseImmunityCombatLog()
end
end
-- Skip if target is dead (dead targets report "immune" for in-flight spells)
if targetName and UnitExists("target") and UnitName("target") == targetName and CleveRoids.IsUnitDead("target") then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[CombatLog Dead Skip]|r " .. targetName .. " is dead - ignoring immunity from combat log")
end
CancelPendingVerification(targetName, spellName)
return
end
-- If we have a school but no spell, use the school directly
if school and targetName and not spellName then
-- Record immunity by school
if not CleveRoids_ImmunityData[school] then
CleveRoids_ImmunityData[school] = {}
end
-- Check for conditional buff
local buffs = nil
if UnitExists("target") and UnitName("target") == targetName then
buffs = GetUnitBuffs("target")
end
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
CleveRoids_ImmunityData[school][targetName] = { buff = singleBuff }
-- Check if target is queryable for buff check
local canQuery = UnitExists("target") and UnitName("target") == targetName
if canQuery then
local immunityBuff = HasImmunityGrantingBuff("target")
if immunityBuff then
if CleveRoids.debug then
CleveRoids.Print("|cffff6600Immunity:|r " .. targetName .. " is immune to " .. school .. " when buffed with: " .. singleBuff)
CleveRoids.Print("|cff00aaff[CombatLog Temp Skip]|r " .. targetName .. " has " .. immunityBuff)
end
-- Cancel pending verification - combat log confirmed immunity
CancelPendingVerification(targetName, nil)
return
end
else
-- Cannot query target — inconclusive, don't record
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[CombatLog Inconclusive]|r Cannot query " .. targetName .. " for buffs - not recording " .. school .. " immunity")
end
CancelPendingVerification(targetName, nil)
return
end
-- Record immunity by school
if not CleveRoids_ImmunityData[school] then
CleveRoids_ImmunityData[school] = {}
end
-- Permanent immunity
@@ -7892,75 +7986,9 @@ local function ParseImmunityCombatLog()
)
end
-- Check if target has any buffs (for conditional immunity)
local buffs = nil
if UnitExists("target") and UnitName("target") == targetName then
buffs = GetUnitBuffs("target")
end
-- If target has exactly one buff, assume it's causing the immunity
if buffs and next(buffs) then
local buffCount = 0
local singleBuff = nil
for buff, _ in pairs(buffs) do
buffCount = buffCount + 1
singleBuff = buff
if buffCount > 1 then
singleBuff = nil
break
end
end
if singleBuff then
-- Try to get spell ID for more accurate school detection
local spellID = CleveRoids.GetSpellIdForName and CleveRoids.GetSpellIdForName(spellName)
-- 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
-- Check both spell ID and spell name (all weapon variants share the same display name)
local baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
if (spellID and SPLIT_CC_SPELLS[spellID]) or SPLIT_CC_SPELL_NAMES[baseName] then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[Split CC Skip]|r " .. spellName .. " CC immune on " .. targetName .. " - skipping immunity recording (physical damage landed)")
end
CancelPendingVerification(targetName, spellName)
return
end
-- Check if this is a CC spell - if so, record CC immunity instead
local ccType = spellID and GetSpellCCType(spellID)
if ccType then
RecordCCImmunity(targetName, ccType, singleBuff, spellName)
else
RecordImmunity(targetName, spellName, singleBuff, spellID)
end
-- Cancel pending verification - combat log confirmed immunity
CancelPendingVerification(targetName, spellName)
return
end
end
-- Check for immunity-granting buffs before recording permanent immunity
-- If target has a known immunity buff (Divine Shield, Ice Block, etc.), skip recording
local immunityBuff = nil
if UnitExists("target") and UnitName("target") == targetName then
immunityBuff = HasImmunityGrantingBuff("target")
end
if immunityBuff then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[Temporary Immunity Skip]|r " .. targetName .. " has " .. immunityBuff .. " active - skipping permanent immunity recording for " .. spellName)
end
CancelPendingVerification(targetName, spellName)
return
end
-- No immunity buff detected, record as permanent immunity
local spellID = CleveRoids.GetSpellIdForName and CleveRoids.GetSpellIdForName(spellName)
-- 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
-- Check both spell ID and spell name (all weapon variants share the same display name)
local baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
if (spellID and SPLIT_CC_SPELLS[spellID]) or SPLIT_CC_SPELL_NAMES[baseName] then
if CleveRoids.debug then
@@ -7970,14 +7998,33 @@ local function ParseImmunityCombatLog()
return
end
-- Check if this is a CC spell - if so, record CC immunity instead
-- Check if target is queryable for buff check
local canQuery = UnitExists("target") and UnitName("target") == targetName
if canQuery then
local immunityBuff = HasImmunityGrantingBuff("target")
if immunityBuff then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[CombatLog Temp Skip]|r " .. targetName .. " has " .. immunityBuff .. " active - skipping permanent immunity recording for " .. spellName)
end
CancelPendingVerification(targetName, spellName)
return
end
else
-- Cannot query target — inconclusive, don't record
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[CombatLog Inconclusive]|r Cannot query " .. targetName .. " for buffs - not recording immunity for " .. spellName)
end
CancelPendingVerification(targetName, spellName)
return
end
-- Record as permanent immunity
local ccType = spellID and GetSpellCCType(spellID)
if ccType then
RecordCCImmunity(targetName, ccType, nil, spellName)
else
RecordImmunity(targetName, spellName, nil, spellID)
end
-- Cancel pending verification - combat log confirmed immunity
CancelPendingVerification(targetName, spellName)
end
end
@@ -8934,7 +8981,9 @@ CleveRoids.usingSpellMissEvents = false
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
-- Resolve target name from GUID cache, current target, or extended tokens
local hasExtTokens = CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.features
and CleveRoids.NampowerAPI.features.hasExtendedUnitTokens
local targetName = lib.guidToName[targetGuid]
if not targetName then
local currentTargetGUID = CleveRoids.GetGUID("target")
@@ -8945,6 +8994,21 @@ local function ProcessSpellMissSelf(spellId, targetGuid, missInfo)
end
end
end
-- Nampower extended tokens fallback: resolve name from GUID directly
if not targetName and hasExtTokens and UnitExists(targetGuid) then
targetName = UnitName(targetGuid)
if targetName then
lib.guidToName[targetGuid] = targetName
end
end
-- Cross-validate cached name against live data when extended tokens available
if targetName and hasExtTokens and UnitExists(targetGuid) then
local liveName = UnitName(targetGuid)
if liveName and liveName ~= "Unknown" and liveName ~= targetName then
lib.guidToName[targetGuid] = liveName
targetName = liveName
end
end
-- Resolve spell name
local spellName = GetSpellRecField and type(spellId) == "number" and spellId > 0 and GetSpellRecField(spellId, "name")
@@ -8973,45 +9037,64 @@ local function ProcessSpellMissSelf(spellId, targetGuid, missInfo)
return
end
-- Skip if target has temporary immunity buff (Divine Shield, etc.)
-- Skip if target recently died (dead targets report IMMUNE for in-flight spells)
if lib.recentDeaths[targetGuid] and (GetTime() - lib.recentDeaths[targetGuid]) < 3 then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[SPELL_MISS Dead Skip]|r " .. targetName .. " recently died - ignoring immunity")
end
return
end
-- Also check if target is truly dead via Nampower health
if UnitExists("target") and UnitName("target") == targetName and CleveRoids.IsUnitDead("target") then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[SPELL_MISS Dead Skip]|r " .. targetName .. " is dead - ignoring immunity")
end
-- Record death GUID for future spells in same frame
lib.recentDeaths[targetGuid] = GetTime()
return
end
-- Resolve a queryable unit for buff checks
-- Try current target first, then GUID via extended tokens
local queryUnit = nil
if UnitExists("target") and UnitName("target") == targetName then
local immunityBuff = HasImmunityGrantingBuff("target")
queryUnit = "target"
elseif hasExtTokens and UnitExists(targetGuid) then
queryUnit = targetGuid
end
-- Skip if target has temporary immunity buff (Divine Shield, etc.)
if queryUnit then
local immunityBuff = HasImmunityGrantingBuff(queryUnit)
if immunityBuff then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[SPELL_MISS Temp Skip]|r " .. targetName .. " has " .. immunityBuff)
end
return
end
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
-- Check DR before recording as permanent CC immunity
local ccType = GetSpellCCType(spellId)
if ccType and targetGuid then
local drEntry = lib.recentCCHits[targetGuid] and lib.recentCCHits[targetGuid][ccType]
if drEntry and (GetTime() - drEntry.lastHitTime) < 20 and drEntry.count >= 3 then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[SPELL_MISS DR Skip]|r " .. targetName .. " - likely DR immune to " .. ccType .. ", not recording")
end
return
end
end
-- No buff detected - record as permanent immunity
local ccType = GetSpellCCType(spellId)
-- Cannot query target (despawned/phased/switched) - inconclusive, don't record
if not queryUnit then
if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[SPELL_MISS Inconclusive]|r Cannot query " .. targetName .. " for buffs - not recording immunity")
end
return
end
-- Record as permanent immunity
if ccType then
RecordCCImmunity(targetName, ccType, nil, spellName)
else