Stop DR suppressing NPC immunity learning past its window

The DR safeguard keyed recentCCHits by ccType and never reset the counter,
so three stuns on a target left it at count >= 3 forever. Every later
IMMUNE on that target was read as diminishing returns and no permanent
immunity was ever learned from it again.

Count within a window instead: a hit more than DR_RESET_WINDOW seconds
after the last one starts over at 1, matching the 20s DR decay the skip
check already used. Both the hit path and the skip check now key on the DR
pool from GetSpellImmunityDRType rather than the mechanic, so only the three
groups that actually diminish against creatures can hold learning back.

Learning itself records immunityType, the exact mechanic, so a target that
resists Gouge is no longer written down as stun-immune.

CheckImmunity also resolves a spell name through GetSpellImmunityType now,
so bare [immune]/[noimmune] answers for mechanic immunity and not just
schools.
This commit is contained in:
Brues
2026-09-15 08:06:22 -05:00
parent ca6c55a5e7
commit 4474a607f7
+93 -18
View File
@@ -847,7 +847,8 @@ lib.allAuraCasts = lib.allAuraCasts or {} -- [targetGUID][spellName][casterGui
lib.pendingCasts = lib.pendingCasts or {} -- [targetGUID][spellName] = {casterGuid, rank, time, comboPoints} 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.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.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) local DR_RESET_WINDOW = 20
lib.recentCCHits = lib.recentCCHits or {} -- [targetGUID][drType] = {count, lastHitTime} for NPC DR safeguard
lib.iconCache = lib.iconCache or {} -- [spellId] = texture (shared with pfUI 7.6 or standalone) 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) -- Buff tracking tables (parallel to debuff tables, standalone Nampower mode only)
@@ -2327,7 +2328,7 @@ lib.pendingPersonalDebuffs = lib.pendingPersonalDebuffs or {}
-- CC (Crowd Control) pending tracking system -- CC (Crowd Control) pending tracking system
-- Stores CC spells to verify they landed (for immunity detection) -- Stores CC spells to verify they landed (for immunity detection)
-- Format: { [index] = { timestamp = GetTime(), targetGUID = guid, targetName = name, spellID = id, ccType = "stun", immunityType = "stun" } } -- Format: { [index] = { timestamp = GetTime(), targetGUID = guid, targetName = name, spellID = id, ccType = "stun", immunityType = "stun", drType = "stun_control" } }
lib.pendingCCDebuffs = lib.pendingCCDebuffs or {} lib.pendingCCDebuffs = lib.pendingCCDebuffs or {}
-- Shared debuff pending tracking system -- Shared debuff pending tracking system
@@ -3113,17 +3114,17 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
elseif totalDebuffs < DEBUFF_CAP_THRESHOLD then elseif totalDebuffs < DEBUFF_CAP_THRESHOLD then
-- Check DR before recording as permanent CC immunity -- Check DR before recording as permanent CC immunity
local isDR = false local isDR = false
if pending.targetGUID and pending.ccType then if pending.targetGUID and pending.drType then
local drEntry = lib.recentCCHits[pending.targetGUID] and lib.recentCCHits[pending.targetGUID][pending.ccType] local drEntry = lib.recentCCHits[pending.targetGUID] and lib.recentCCHits[pending.targetGUID][pending.drType]
if drEntry and (GetTime() - drEntry.lastHitTime) < 20 and drEntry.count >= 3 then if drEntry and (GetTime() - drEntry.lastHitTime) < DR_RESET_WINDOW and drEntry.count >= 3 then
isDR = true isDR = true
end end
end end
if isDR then if isDR then
if debug then if debug then
DEFAULT_CHAT_FRAME:AddMessage( 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", _string_format("|cff00aaff[CC DR Skip]|r %s on %s - likely %s DR immunity (3+ recent hits), not recording permanent immunity",
pending.spellName or "Unknown", resolvedTargetName or "Unknown", pending.ccType or "Unknown") pending.spellName or "Unknown", resolvedTargetName or "Unknown", pending.drType or "Unknown")
) )
end end
elseif resolvedTargetName and resolvedTargetName ~= "" and (pending.immunityType or pending.ccType) then elseif resolvedTargetName and resolvedTargetName ~= "" and (pending.immunityType or pending.ccType) then
@@ -3162,13 +3163,18 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
if resolvedTargetName and learnedImmunityType then if resolvedTargetName and learnedImmunityType then
CleveRoids.RemoveCCImmunity(resolvedTargetName, learnedImmunityType) CleveRoids.RemoveCCImmunity(resolvedTargetName, learnedImmunityType)
end end
-- Track successful CC hit for DR detection -- Track only NPC-applicable DR groups for the permanent-immunity safeguard.
if pending.targetGUID and pending.ccType then if pending.targetGUID and pending.drType then
lib.recentCCHits[pending.targetGUID] = lib.recentCCHits[pending.targetGUID] or {} lib.recentCCHits[pending.targetGUID] = lib.recentCCHits[pending.targetGUID] or {}
local entry = lib.recentCCHits[pending.targetGUID][pending.ccType] local now = GetTime()
lib.recentCCHits[pending.targetGUID][pending.ccType] = { local entry = lib.recentCCHits[pending.targetGUID][pending.drType]
count = (entry and entry.count or 0) + 1, local count = 1
lastHitTime = GetTime(), if entry and (now - entry.lastHitTime) < DR_RESET_WINDOW then
count = (entry.count or 0) + 1
end
lib.recentCCHits[pending.targetGUID][pending.drType] = {
count = count,
lastHitTime = now,
} }
end end
if debug then if debug then
@@ -3619,6 +3625,7 @@ ev:SetScript("OnEvent", function()
-- Uses the original spellID (not trackingSpellID) to detect CC type -- Uses the original spellID (not trackingSpellID) to detect CC type
local ccType = CleveRoids.GetSpellCCType and CleveRoids.GetSpellCCType(spellID) local ccType = CleveRoids.GetSpellCCType and CleveRoids.GetSpellCCType(spellID)
local immunityType = CleveRoids.GetSpellImmunityType and CleveRoids.GetSpellImmunityType(spellID) local immunityType = CleveRoids.GetSpellImmunityType and CleveRoids.GetSpellImmunityType(spellID)
local drType = CleveRoids.GetSpellImmunityDRType and CleveRoids.GetSpellImmunityDRType(spellID)
-- Debug: Show what GetSpellCCType returns for this spell -- Debug: Show what GetSpellCCType returns for this spell
if CleveRoids.debug then if CleveRoids.debug then
@@ -3658,6 +3665,7 @@ ev:SetScript("OnEvent", function()
spellName = spellName, spellName = spellName,
ccType = ccType, ccType = ccType,
immunityType = immunityType or ccType, immunityType = immunityType or ccType,
drType = drType,
isHiddenCC = isHiddenCC, -- Flag for hidden CC spells isHiddenCC = isHiddenCC, -- Flag for hidden CC spells
}) })
@@ -4375,6 +4383,7 @@ ev:SetScript("OnEvent", function()
-- CC IMMUNITY TRACKING: Check if this spell is a CC spell -- CC IMMUNITY TRACKING: Check if this spell is a CC spell
local ccType = CleveRoids.GetSpellCCType and CleveRoids.GetSpellCCType(spellId) local ccType = CleveRoids.GetSpellCCType and CleveRoids.GetSpellCCType(spellId)
local immunityType = CleveRoids.GetSpellImmunityType and CleveRoids.GetSpellImmunityType(spellId) local immunityType = CleveRoids.GetSpellImmunityType and CleveRoids.GetSpellImmunityType(spellId)
local drType = CleveRoids.GetSpellImmunityDRType and CleveRoids.GetSpellImmunityDRType(spellId)
if ccType then if ccType then
local isHiddenCC = lib.hiddenCCSpells and lib.hiddenCCSpells[spellId] local isHiddenCC = lib.hiddenCCSpells and lib.hiddenCCSpells[spellId]
if not isHiddenCC and _G.IsAuraHidden then if not isHiddenCC and _G.IsAuraHidden then
@@ -4396,6 +4405,7 @@ ev:SetScript("OnEvent", function()
spellName = spellName, spellName = spellName,
ccType = ccType, ccType = ccType,
immunityType = immunityType or ccType, immunityType = immunityType or ccType,
drType = drType,
isHiddenCC = isHiddenCC, isHiddenCC = isHiddenCC,
spellGoHit = true, -- We already know it hit spellGoHit = true, -- We already know it hit
}) })
@@ -6671,6 +6681,70 @@ end
CleveRoids.GetSpellImmunityType = GetSpellImmunityType CleveRoids.GetSpellImmunityType = GetSpellImmunityType
-- NPC immunity learning only needs DR groups that can actually diminish creatures.
-- vMaNGOS 1.12 marks controlled stun, triggered stun, and Kidney Shot as DRTYPE_ALL;
-- the other staged DR groups are player-only and must not suppress NPC immunity learning.
local STUN_CONTROL_DR_OVERRIDES = {
[7922] = true, -- Charge Stun
[20253] = true, -- Intercept Stun Rank 1
[20614] = true, -- Intercept Stun Rank 2
[20615] = true, -- Intercept Stun Rank 3
}
local KIDNEY_SHOT_IDS = {
[408] = true,
[8643] = true,
}
local function IsKidneyShotSpell(spellID)
if KIDNEY_SHOT_IDS[spellID] then return true end
-- Vanilla Rogue family bit 21 (0x00200000) is Kidney Shot. Prefer the
-- family mask so custom ranks that preserve DBC family data also classify correctly.
if GetSpellRecField then
local familyName = GetSpellRecField(spellID, "spellFamilyName")
local familyFlags = GetSpellRecField(spellID, "spellFamilyFlags")
if familyName == 8 and familyFlags then -- SPELLFAMILY_ROGUE
local kidneyBit = 2097152 -- 0x00200000
if math.mod(math.floor(familyFlags / kidneyBit), 2) == 1 then
return true
end
end
end
return false
end
local function WasClientInitiatedSpell(spellID)
-- Nampower SPELL_CAST_EVENT fires only for spells initiated by the client.
-- Proc/trigger spell IDs reach server SPELL_GO/MISS without their own entry.
local cast = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellID]
if not cast or not cast.timestamp then return false end
local age = GetTime() - cast.timestamp
return age >= 0 and age <= 5
end
local function GetSpellImmunityDRType(spellID)
if GetSpellCCType(spellID) ~= "stun" then return nil end
if IsKidneyShotSpell(spellID) then
return "stun_kidneyshot"
end
-- These stun subspells are internally triggered but explicitly belong to
-- controlled-stun DR in Vanilla.
if STUN_CONTROL_DR_OVERRIDES[spellID] then
return "stun_control"
end
if WasClientInitiatedSpell(spellID) then
return "stun_control"
end
return "stun_trigger"
end
CleveRoids.GetSpellImmunityDRType = GetSpellImmunityDRType
-- Record a CC immunity (permanent or buff-based) -- Record a CC immunity (permanent or buff-based)
-- Parameters: -- Parameters:
-- npcName: Name of the NPC that is immune -- npcName: Name of the NPC that is immune
@@ -8311,14 +8385,15 @@ local function ProcessSpellMissSelf(spellId, targetGuid, missInfo)
end end
end end
-- Check DR before recording as permanent CC immunity -- Check NPC-applicable DR before recording permanent CC immunity.
local ccType = GetSpellCCType(spellId) local ccType = GetSpellCCType(spellId)
local immunityType = GetSpellImmunityType(spellId) or ccType local immunityType = GetSpellImmunityType(spellId) or ccType
if ccType and targetGuid then local drType = GetSpellImmunityDRType(spellId)
local drEntry = lib.recentCCHits[targetGuid] and lib.recentCCHits[targetGuid][ccType] if drType and targetGuid then
if drEntry and (GetTime() - drEntry.lastHitTime) < 20 and drEntry.count >= 3 then local drEntry = lib.recentCCHits[targetGuid] and lib.recentCCHits[targetGuid][drType]
if drEntry and (GetTime() - drEntry.lastHitTime) < DR_RESET_WINDOW and drEntry.count >= 3 then
if CleveRoids.debug then if CleveRoids.debug then
CleveRoids.Print("|cff00aaff[SPELL_MISS DR Skip]|r " .. targetName .. " - likely DR immune to " .. ccType .. ", not recording") CleveRoids.Print("|cff00aaff[SPELL_MISS DR Skip]|r " .. targetName .. " - likely " .. drType .. " DR immunity, not recording")
end end
return return
end end