diff --git a/Conditionals.lua b/Conditionals.lua index a345127..b44d172 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -1806,10 +1806,20 @@ local function IsPendingDebuffCast(spellName, targetUnit) local remaining = CleveRoids.castDuration - (GetTime() - CleveRoids.castStartTime) if remaining > 0.1 then -- Cast is in-flight, debuff is pending + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cffff00ff[PendingDebuff]|r %s pending via CurrentSpell (%.1fs remaining)", + spellName, remaining)) + end return true end else -- No timing info but spell type is "cast" - assume pending + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cffff00ff[PendingDebuff]|r %s pending via CurrentSpell (no timing info)", + spellName)) + end return true end end @@ -1823,22 +1833,66 @@ local function IsPendingDebuffCast(spellName, targetUnit) local normalizedQueued = NormalizeSpellNameForComparison(queuedName) if normalizedQueued == normalizedCheck then -- Spell is queued, debuff is pending + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cffff00ff[PendingDebuff]|r %s pending via queuedSpell", + spellName)) + end return true end end - -- Check pfUI's pending debuff tracking (uses SPELL_GO events for accuracy) - -- pfUI.libdebuff_pending[guid][spellName] = true when cast is in-flight - -- Cleared on SPELL_GO miss/resist, so this is very accurate - if pfUI and pfUI.libdebuff_pending and targetGuid then - local pendingForTarget = pfUI.libdebuff_pending[targetGuid] - if pendingForTarget then - -- pfUI stores by spell name (may or may not have rank) - -- Check both the exact name and stripped name - for pendingSpell, _ in pairs(pendingForTarget) do - local normalizedPending = NormalizeSpellNameForComparison(pendingSpell) - if normalizedPending == normalizedCheck then - return true + -- Check in-flight debuffs: use pfUI's pending table when available (shared data, + -- avoids duplicate tracking), otherwise fall back to our own pending arrays + local lib = CleveRoids.libdebuff + if lib and targetGuid then + if pfUI and pfUI.libdebuff_pending then + -- pfUI.libdebuff_pending[guid][spellName] = {casterGuid, rank, time} + -- Tracks all casters; filter to player's own casts only + local pendingForTarget = pfUI.libdebuff_pending[targetGuid] + if pendingForTarget then + local playerGuid = CleveRoids.GetGUID("player") + for pendingSpell, pendingData in pairs(pendingForTarget) do + local normalizedPending = NormalizeSpellNameForComparison(pendingSpell) + if normalizedPending == normalizedCheck then + local casterGuid = type(pendingData) == "table" and pendingData.casterGuid or nil + if casterGuid and casterGuid == playerGuid then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cffff00ff[PendingDebuff]|r %s pending via pfUI.libdebuff_pending (ours)", + spellName)) + end + return true + elseif CleveRoids.debug and casterGuid then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cffff00ff[PendingDebuff]|r %s in pfUI pending but caster=%s (not ours) - skipping", + spellName, string.sub(tostring(casterGuid), 1, 16))) + end + end + end + end + else + -- Standalone mode (no pfUI): check our own pending arrays + -- These are player-only, populated from UNIT_CASTEVENT / SPELL_GO_SELF + local pendingArrays = { lib.pendingPersonalDebuffs, lib.pendingSharedDebuffs, lib.pendingCCDebuffs } + for _, arr in ipairs(pendingArrays) do + if arr then + for _, pending in pairs(arr) do + if pending and pending.targetGUID == targetGuid and pending.spellID then + local pendingName = GetSpellRecField and GetSpellRecField(pending.spellID, "name") + if pendingName then + local normalizedPending = NormalizeSpellNameForComparison(pendingName) + if normalizedPending == normalizedCheck then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cffff00ff[PendingDebuff]|r %s pending via libdebuff pending array (ID:%d)", + spellName, pending.spellID)) + end + return true + end + end + end + end end end end @@ -4370,14 +4424,17 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if gufResult ~= nil then usedFastPath = true if gufResult then - -- Step 1 (tracking) already ran and didn't find this debuff. - -- Accept any GetUnitField match as fallback — covers shared debuffs, - -- untracked personal debuffs, and unknown/custom debuffs. - found = true - stacks = gufStacks or 0 - spellID = gufSpellId - foundSpellId = gufSpellId - remaining = nil + -- Step 1 (tracking) already ran and didn't find this debuff as ours. + -- Only accept shared debuffs (Sunder, Faerie Fire, etc.) from any caster. + -- Personal debuffs not in our tracking table = another player's cast → skip. + local isShared = IsSharedDebuffByIdOrName(lib, gufSpellId, args.name) + if isShared then + found = true + stacks = gufStacks or 0 + spellID = gufSpellId + foundSpellId = gufSpellId + remaining = nil + end elseif lib.objects[guid] then -- gufResult == false: aura definitively not on target -- Clean any stale tracking entries @@ -4435,13 +4492,18 @@ function CleveRoids.ValidateUnitDebuff(unit, args) matched = baseName and (baseName == args.name or fullName == args.name) end if matched then - found = true - texture = tex - stacks = debuffStacks or 0 - spellID = debuffSpellID - foundSpellId = debuffSpellID - remaining = nil - break + -- Only accept shared debuffs here — personal debuffs not in + -- our tracking table (Step 1) are another player's cast + local isShared = IsSharedDebuffByIdOrName(lib, debuffSpellID, args.name) + if isShared then + found = true + texture = tex + stacks = debuffStacks or 0 + spellID = debuffSpellID + foundSpellId = debuffSpellID + remaining = nil + break + end end end end @@ -4461,13 +4523,16 @@ function CleveRoids.ValidateUnitDebuff(unit, args) matched = baseName and (baseName == args.name or fullName == args.name) end if matched then - found = true - texture = tex - stacks = buffStacks or 0 - spellID = buffSpellID - foundSpellId = buffSpellID - remaining = nil - break + local isShared = IsSharedDebuffByIdOrName(lib, buffSpellID, args.name) + if isShared then + found = true + texture = tex + stacks = buffStacks or 0 + spellID = buffSpellID + foundSpellId = buffSpellID + remaining = nil + break + end end end end diff --git a/Utility.lua b/Utility.lua index 827b2de..7ff861b 100644 --- a/Utility.lua +++ b/Utility.lua @@ -3345,13 +3345,26 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Add debuff after 0.2 second delay (enough time for server sync) if elapsed >= 0.2 then - -- BLEED IMMUNITY VERIFICATION: Check if bleed debuff actually appeared + -- DEBUFF IMMUNITY VERIFICATION: Check if debuff actually appeared -- This happens AFTER the delay, giving the server time to sync the debuff local isBleedSpell = CleveRoids.BleedSpellIDs and CleveRoids.BleedSpellIDs[pending.spellID] - local bleedVerified = true + local debuffVerified = true + + -- SPELL_GO confirmed miss → debuff definitely didn't land (any spell type) + -- Immunity is recorded by SPELL_MISS_SELF → ProcessSpellMissSelf → RecordImmunity + if pending.spellGoMissed then + debuffVerified = false + if debug then + local spellNameDbg = _GetSpellRecField(pending.spellID, "name") or "Unknown" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffff6600[Personal Miss]|r %s (ID:%d) missed on %s - SPELL_GO confirmed miss", + spellNameDbg, pending.spellID, pending.targetName or "Unknown") + ) + end + end -- DEBUG: Log verification attempt for bleed spells - if debug and isBleedSpell then + if debug and isBleedSpell and not pending.spellGoMissed then local spellNameDbg = _GetSpellRecField(pending.spellID, "name") or "Unknown" DEFAULT_CHAT_FRAME:AddMessage( _string_format("|cff00aaff[Bleed Verify Start]|r %s (ID:%d) on %s - hasSuperwow:%s, targetGUID:%s", @@ -3360,7 +3373,8 @@ delayedTrackingFrame:SetScript("OnUpdate", function() ) end - local verifyUnit = isBleedSpell and pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil + local verifyUnit = isBleedSpell and not pending.spellGoMissed + and pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil if isBleedSpell and verifyUnit then -- Check if mob is in bleed whitelist (skip verification for known bleeders) local isWhitelisted = CleveRoids.MobsThatBleed and CleveRoids.MobsThatBleed[pending.targetGUID] @@ -3388,8 +3402,8 @@ delayedTrackingFrame:SetScript("OnUpdate", function() if timeSinceCast < ONE_SHOT_THRESHOLD then -- One-shot kill - can't determine immunity, skip recording - -- Set bleedVerified = false so we don't add to tracking or remove existing immunity - bleedVerified = false + -- Set debuffVerified=false so we don't add to tracking or remove existing immunity + debuffVerified = false if debug then local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Bleed" DEFAULT_CHAT_FRAME:AddMessage( @@ -3397,13 +3411,13 @@ delayedTrackingFrame:SetScript("OnUpdate", function() spellNameDebug, pending.targetName or "Unknown", timeSinceCast) ) end - -- Note: We set bleedVerified = false but DON'T record immunity + -- Note: We set debuffVerified = false but DON'T record immunity -- This is intentional - one-shots are inconclusive else -- Target died without "afflicted by" — inconclusive, don't record immunity. -- "afflicted by" combat log messages can be delayed/throttled, so absence -- is not proof of immunity. SPELL_MISS_SELF handles true immunity detection. - bleedVerified = false + debuffVerified = false if debug then local spellNameForImmunity = _GetSpellRecField(pending.spellID, "name") or "Bleed" DEFAULT_CHAT_FRAME:AddMessage( @@ -3416,7 +3430,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function() elseif not UnitExists(verifyUnit) then -- Target despawned or GUID is invalid - can't verify, skip without recording immunity -- This is similar to one-shot kills: inconclusive result, don't record immunity - bleedVerified = false + debuffVerified = false if debug then local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Bleed" DEFAULT_CHAT_FRAME:AddMessage( @@ -3424,11 +3438,11 @@ delayedTrackingFrame:SetScript("OnUpdate", function() spellNameDebug, pending.targetName or "Unknown") ) end - -- Note: We set bleedVerified = false but DON'T record immunity + -- Note: We set debuffVerified = false but DON'T record immunity -- This is intentional - despawned targets are inconclusive else -- Target is alive - check debuffs for bleed spell - bleedVerified = false + debuffVerified = false local totalDebuffs = 0 for slot = 1, 48 do @@ -3438,14 +3452,14 @@ delayedTrackingFrame:SetScript("OnUpdate", function() else totalDebuffs = totalDebuffs + 1 if debuffSpellID == pending.spellID then - bleedVerified = true + debuffVerified = true -- Don't break - continue counting total debuffs for immunity vs cap detection end end end -- If bleed is missing and target has few debuffs, it's likely immunity (not debuff cap) - if not bleedVerified then + if not debuffVerified then local DEBUFF_CAP_THRESHOLD = 47 -- Max is 48 (16 visible + 32 overflow) -- Guard: don't record immunity for player targets (PvP resists, trinkets, etc.) local isPlayer = UnitIsPlayer(verifyUnit) @@ -3500,8 +3514,98 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end end + -- NON-BLEED VERIFICATION: Scan debuffs for non-bleed personal debuffs without SPELL_GO data + -- This catches "soft immunity" where SPELL_GO reports hit but debuff doesn't actually apply + -- (e.g., some Turtle WoW boss mechanics, or server-side immunity without SPELL_MISS event) + if not isBleedSpell and debuffVerified and not pending.spellGoHit and not pending.spellGoMissed then + local nonBleedVerifyUnit = pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil + if nonBleedVerifyUnit then + if _IsUnitDead(nonBleedVerifyUnit) then + -- Target died - can't verify, assume landed + if debug then + local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Debuff" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffaaaaaa[NonBleed Skip]|r %s on %s - target dead, assuming landed", + spellNameDebug, pending.targetName or "Unknown") + ) + end + elseif not UnitExists(nonBleedVerifyUnit) then + -- Target despawned - inconclusive, don't add to tracking + debuffVerified = false + if debug then + local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Debuff" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffaaaaaa[NonBleed Skip]|r %s on %s - target despawned, can't verify", + spellNameDebug, pending.targetName or "Unknown") + ) + end + else + -- Target is alive - check if debuff exists + local nonBleedFound = false + local totalDebuffs = 0 + + for slot = 1, 48 do + local _, _, _, debuffSpellID = _UnitDebuff(nonBleedVerifyUnit, slot) + if not debuffSpellID then + if slot <= 16 then break end -- Regular debuffs are dense, overflow continues on nil + else + totalDebuffs = totalDebuffs + 1 + if debuffSpellID == pending.spellID then + nonBleedFound = true + -- Don't break - continue counting for immunity vs cap detection + end + end + end + + if not nonBleedFound then + debuffVerified = false + local DEBUFF_CAP_THRESHOLD = 47 + local isPlayer = UnitIsPlayer(nonBleedVerifyUnit) + local immunityBuff = not isPlayer and HasImmunityGrantingBuff(nonBleedVerifyUnit) + if isPlayer then + if debug then + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffaaaaaa[NonBleed Skip]|r Player target %s - not recording immunity", + pending.targetName or "Unknown") + ) + end + elseif immunityBuff then + if debug then + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffaaaaaa[NonBleed Skip]|r %s has %s - not recording immunity", + pending.targetName or "Unknown", immunityBuff) + ) + end + elseif totalDebuffs < DEBUFF_CAP_THRESHOLD then + -- Few debuffs = likely school immunity, not debuff cap + -- Use RecordImmunity for proper DBC school lookup (with bleed override) + if pending.targetName and pending.targetName ~= "" then + CleveRoids.RecordImmunity(pending.targetName, nil, nil, pending.spellID) + if debug then + local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Debuff" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffff6600[NonBleed Immunity]|r %s is immune to %s - only %d debuffs on target", + pending.targetName, spellNameDebug, totalDebuffs) + ) + end + end + else + if debug then + local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Debuff" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffff6600[NonBleed Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs)", + spellNameDebug, pending.targetName or "Unknown", totalDebuffs) + ) + end + end + end + end + end + -- If no verifyUnit (can't resolve GUID), debuffVerified stays true (can't verify) + end + -- Only apply the debuff to tracking if it was verified (or not a bleed/was whitelisted) - if bleedVerified then + if debuffVerified then -- Debug: Show what we're about to add to tracking if debug then local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Unknown" @@ -3890,6 +3994,18 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end end + -- SPELL_GO confirmed miss - debuff definitely didn't land + if pending.spellGoMissed then + -- Leave debuffVerified=false → falls through to immunity recording + if debug then + local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Shared Debuff" + DEFAULT_CHAT_FRAME:AddMessage( + _string_format("|cffff6600[Shared Miss]|r %s missed on %s - SPELL_GO confirmed miss", + spellNameDebug, pending.targetName or "Unknown") + ) + end + end + -- Skip verification if target is dead (debuffs are removed on death) -- Guard: skip debuff scanning if SPELL_GO already determined outcome local sharedVerifyUnit = not debuffVerified and not pending.spellGoHit and not pending.spellGoMissed @@ -3920,7 +4036,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end end end - else + elseif not pending.spellGoMissed then -- No SuperWoW or no GUID - assume debuff landed (can't verify) debuffVerified = true end @@ -7879,6 +7995,16 @@ local function RecordImmunity(npcName, spellName, conditionalBuff, spellID) if dbcSchool and SCHOOL_NAMES[dbcSchool] then school = SCHOOL_NAMES[dbcSchool] end + -- Bleed spells have DBC school=0 (physical) but immunity should be "bleed", not "physical" + -- Split damage spells (Rake/Pounce/Garrote) are already handled above — their cast spell + -- triggers IMMUNE for the initial physical hit, so "physical" is correct for those. + -- Pure bleed spells (Rip, etc.) that reach this path need the override. + if school == "physical" and CleveRoids.BleedSpellIDs and CleveRoids.BleedSpellIDs[spellID] then + school = "bleed" + if CleveRoids.debug then + CleveRoids.Print("|cff00aaff[Bleed Override]|r " .. (spellName or tostring(spellID)) .. " immunity recorded as 'bleed' (DBC school=physical, but spell is a bleed)") + end + end end -- Fallback: try learned mapping from damage events