5 Commits

Author SHA1 Message Date
Brues 37ca7cef40 Document the CC, immunity and DR design
Records why the three identities are separate, the DBC mechanic table each
[cc:type] now follows, and which DR pools the immunity safeguard may act on.

Every vMaNGOS and nampower claim behind the preceding four commits is cited
to the source line that establishes it, so the next person to touch this can
check the premises instead of re-deriving them: the DRTYPE_ALL set is three
stun pools and nothing else, Charge and Intercept are the only triggered
stuns forced into the controlled pool, and SPELL_MISS puts the spell ID in
arg3.

Also notes the mouseover learning gap as designed behavior rather than a bug,
since the failure on the other side is permanent bad SavedVariables data.
2026-09-15 08:10:12 -05:00
Brues 4474a607f7 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.
2026-09-15 08:10:12 -05:00
Brues ca6c55a5e7 Give every [cc:type] its own exact DBC mechanic
stun matched mechanics 12, 14 and 30 at once through CCMechanicGroups, so
[cc:stun] fired on a gouged or sapped target and there was no way to ask
about any of the three on its own.

Name each mechanic instead: knockout (14) and sap (30) join the table, with
fumble (6), turn (23) and interrupt (26) filling the remaining gaps, and
grip stays as an alias of fumble so existing macros keep working. The group
indirection had no entries left afterwards, so it and its lookup branch are
gone; aliases in CCMechanics already cover every name that shares a mechanic.

CCTypesLossOfControl is untouched, so bare [cc] and [cc:any] still span the
whole loss-of-control set.
2026-09-15 08:10:12 -05:00
Brues c1a7bdb51b Fix the Nampower SPELL_MISS argument order
TriggerSpellMissEvent signals casterGuid, targetGuid, spellId, missInfo for
both SPELL_MISS_SELF and SPELL_MISS_OTHER. Both handlers read arg1 as the
spell ID, so every miss dispatched a GUID string where an ID was expected
and no miss ever reached the immunity recorder through the Nampower path.

Name mechanic 14 knockout to match the 1.12 DBC, and normalise CC type input
through one place so incap and incapacitate stay accepted as aliases for it.
2026-09-15 08:10:12 -05:00
Brues 5d5f987555 Learn NPC CC immunity per exact DBC mechanic
Learned immunity shared one coarse bucket with the [cc:*] conditionals, so
mechanic 13 (freeze), 14 (knockout) and 30 (sap) all collapsed into "stun".
An NPC immune to Gouge was then recorded as stun-immune, and every later
Kidney Shot was suppressed against a target that had never resisted one.

Split the two identities. MECHANIC_TO_IMMUNITY_TYPE keeps the mechanic
one-to-one for learning, and GetSpellImmunityType resolves it from the DBC
spell/effect mechanic fields, falling back to the aura type only where a
spell leaves those empty.

The DR safeguard needs the real Vanilla DR pool rather than the mechanic:
GetSpellImmunityDRType separates controlled stun, triggered stun and Kidney
Shot, since vMaNGOS marks only those three DRTYPE_ALL against creatures.
The remaining staged groups are player-only and must not hold back learning.
2026-09-15 08:03:43 -05:00
3 changed files with 430 additions and 70 deletions
+13 -26
View File
@@ -4847,7 +4847,8 @@ end
-- ============================================================================
-- Maps CC type names to mechanic constants (matches DBC mechanic IDs)
-- Note: Some types map to multiple mechanics via CCMechanicGroups below
-- One name, one mechanic: [cc:type] follows the exact DBC mechanic. Several
-- names may share a mechanic as aliases, but no name spans two mechanics.
CleveRoids.CCMechanics = {
-- Movement/control impairment
charm = 1, -- Mind Control, Seduction
@@ -4856,31 +4857,30 @@ CleveRoids.CCMechanics = {
disarm = 3, -- Disarm, Riposte disarm
distract = 4, -- Distract (Rogue ability)
fear = 5, -- Fear, Psychic Scream, Howl of Terror
grip = 6, -- Grip effects
fumble = 6, -- DBC Fumble mechanic
grip = 6, -- Legacy alias for fumble
root = 7, -- Entangling Roots, Frost Nova, Improved Hamstring
pacify = 8, -- Pacify effects
silence = 9, -- Silence, Kick, Counterspell (lockout)
sleep = 10, -- Hibernate, Wyvern Sting sleep
snare = 11, -- Hamstring, Wing Clip, Crippling Poison
slow = 11, -- Alias for snare
stun = 12, -- Consolidated: Stun(12) + Knockout(14) + Sap(30)
freeze = 13, -- Freeze effects (Frost Nova freeze)
stun = 12, -- Stun (Cheap Shot, Kidney Shot, Hammer of Justice)
freeze = 13, -- Freeze effects
knockout = 14, -- Knockout (Gouge, Repentance)
bleed = 15, -- Rend, Garrote, Deep Wounds
polymorph = 17, -- Polymorph (all variants)
banish = 18, -- Banish (Warlock)
shackle = 20, -- Shackle Undead
turn = 23, -- Turn effects
horror = 24, -- Death Coil (Warlock), Intimidating Shout (horror)
interrupt = 26, -- Interrupt mechanic
daze = 27, -- Dazed effects
}
-- Mechanic groups: CC types that check multiple DBC mechanics
-- Used when a single conditional should match several related effects
CleveRoids.CCMechanicGroups = {
stun = {12, 14, 30}, -- Stun(12), Knockout/Gouge(14), Sap(30)
sap = 30, -- Sap/Sapped mechanic
}
-- CC types that count as "crowd controlled" (loss of control)
-- Note: Mechanics 12, 14, 30 are all consolidated under "stun" for conditionals
-- Individual DBC mechanics remain distinct; this table only powers [cc]/[cc:any].
CleveRoids.CCTypesLossOfControl = {
[1] = true, -- charm
[2] = true, -- disoriented
@@ -4889,12 +4889,12 @@ CleveRoids.CCTypesLossOfControl = {
[10] = true, -- sleep
[12] = true, -- stun (Cheap Shot, Kidney Shot, etc.)
[13] = true, -- freeze
[14] = true, -- knockout/gouge (now part of stun group)
[14] = true, -- knockout/gouge
[17] = true, -- polymorph
[18] = true, -- banish
[20] = true, -- shackle
[24] = true, -- horror
[30] = true, -- sap (now part of stun group)
[30] = true, -- sap
}
-- Check if BuffLib is available with full mechanic support
@@ -4961,19 +4961,6 @@ function CleveRoids.ValidateUnitCC(unit, ccType)
return CleveRoids.ValidateUnitAnyCrowdControl(unit)
end
-- Check if this CC type maps to a group of mechanics
local mechanicGroup = CleveRoids.CCMechanicGroups[ccTypeLower]
if mechanicGroup then
-- Check all mechanics in the group (e.g., stun checks 12, 14, 30)
for _, mechanic in ipairs(mechanicGroup) do
if CleveRoids.ValidateUnitCCSingleMechanic(unit, mechanic) then
return true
end
end
return false
end
-- Single mechanic lookup
local mechanic = CleveRoids.CCMechanics[ccTypeLower]
if not mechanic then return false end
+229 -44
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.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)
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)
-- 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
-- Stores CC spells to verify they landed (for immunity detection)
-- Format: { [index] = { timestamp = GetTime(), targetGUID = guid, targetName = name, spellID = id, ccType = "stun" } }
-- Format: { [index] = { timestamp = GetTime(), targetGUID = guid, targetName = name, spellID = id, ccType = "stun", immunityType = "stun", drType = "stun_control" } }
lib.pendingCCDebuffs = lib.pendingCCDebuffs or {}
-- Shared debuff pending tracking system
@@ -3113,26 +3114,27 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
elseif totalDebuffs < DEBUFF_CAP_THRESHOLD 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
if pending.targetGUID and pending.drType then
local drEntry = lib.recentCCHits[pending.targetGUID] and lib.recentCCHits[pending.targetGUID][pending.drType]
if drEntry and (GetTime() - drEntry.lastHitTime) < DR_RESET_WINDOW 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")
_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.drType or "Unknown")
)
end
elseif resolvedTargetName and resolvedTargetName ~= "" and pending.ccType then
CleveRoids.RecordCCImmunity(resolvedTargetName, pending.ccType, nil, pending.spellName)
elseif resolvedTargetName and resolvedTargetName ~= "" and (pending.immunityType or pending.ccType) then
local learnedImmunityType = pending.immunityType or pending.ccType
CleveRoids.RecordCCImmunity(resolvedTargetName, learnedImmunityType, nil, pending.spellName)
if debug then
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cff00ff00[CC Immunity]|r %s is immune to %s (%s) - verified: debuff missing, only %d debuffs on target",
resolvedTargetName, pending.ccType, pending.spellName or "Unknown", totalDebuffs)
resolvedTargetName, learnedImmunityType, pending.spellName or "Unknown", totalDebuffs)
)
end
elseif debug then
@@ -3157,16 +3159,22 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
if (not resolvedTargetName or resolvedTargetName == "") and pending.targetGUID then
resolvedTargetName = lib.guidToName[pending.targetGUID]
end
if resolvedTargetName and pending.ccType then
CleveRoids.RemoveCCImmunity(resolvedTargetName, pending.ccType)
local learnedImmunityType = pending.immunityType or pending.ccType
if resolvedTargetName and learnedImmunityType then
CleveRoids.RemoveCCImmunity(resolvedTargetName, learnedImmunityType)
end
-- Track successful CC hit for DR detection
if pending.targetGUID and pending.ccType then
-- Track only NPC-applicable DR groups for the permanent-immunity safeguard.
if pending.targetGUID and pending.drType 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(),
local now = GetTime()
local entry = lib.recentCCHits[pending.targetGUID][pending.drType]
local count = 1
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
if debug then
@@ -3616,6 +3624,8 @@ ev:SetScript("OnEvent", function()
-- CC IMMUNITY TRACKING: Check if this spell is a CC spell and track for immunity verification
-- Uses the original spellID (not trackingSpellID) to detect CC type
local ccType = CleveRoids.GetSpellCCType and CleveRoids.GetSpellCCType(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
if CleveRoids.debug then
@@ -3654,6 +3664,8 @@ ev:SetScript("OnEvent", function()
spellID = spellID,
spellName = spellName,
ccType = ccType,
immunityType = immunityType or ccType,
drType = drType,
isHiddenCC = isHiddenCC, -- Flag for hidden CC spells
})
@@ -4370,6 +4382,8 @@ ev:SetScript("OnEvent", function()
-- CC IMMUNITY TRACKING: Check if this spell is a CC spell
local ccType = CleveRoids.GetSpellCCType and CleveRoids.GetSpellCCType(spellId)
local immunityType = CleveRoids.GetSpellImmunityType and CleveRoids.GetSpellImmunityType(spellId)
local drType = CleveRoids.GetSpellImmunityDRType and CleveRoids.GetSpellImmunityDRType(spellId)
if ccType then
local isHiddenCC = lib.hiddenCCSpells and lib.hiddenCCSpells[spellId]
if not isHiddenCC and _G.IsAuraHidden then
@@ -4390,6 +4404,8 @@ ev:SetScript("OnEvent", function()
spellID = spellId,
spellName = spellName,
ccType = ccType,
immunityType = immunityType or ccType,
drType = drType,
isHiddenCC = isHiddenCC,
spellGoHit = true, -- We already know it hit
})
@@ -6005,7 +6021,10 @@ local IMMUNITY_SCHOOLS = {
-- CC (Crowd Control) immunity types
-- These are stored with "cc_" prefix in CleveRoids_ImmunityData to avoid collision with damage schools
local CC_IMMUNITY_TYPES = {
stun = true, -- Cheap Shot, Kidney Shot, Bash, Gouge, Sap
stun = true, -- Mechanic 12: Cheap Shot, Kidney Shot, Hammer of Justice
freeze = true, -- Mechanic 13: frozen effects
knockout = true, -- Mechanic 14: Gouge, Repentance, Blast Wave
sap = true, -- Mechanic 30: Sap
fear = true, -- Fear, Psychic Scream, Howl of Terror
root = true, -- Entangling Roots, Frost Nova
silence = true, -- Silence, Counterspell
@@ -6013,11 +6032,25 @@ local CC_IMMUNITY_TYPES = {
charm = true, -- Mind Control, Seduction
polymorph = true, -- Polymorph (all variants)
banish = true, -- Banish
shackle = true, -- Shackle Undead
horror = true, -- Death Coil
disorient = true, -- Scatter Shot, Blind
daze = true, -- Daze mechanic
snare = true, -- Hamstring, Wing Clip
}
local CC_IMMUNITY_ALIASES = {
incap = "knockout",
incapacitate = "knockout",
incapacitated = "knockout",
}
local function NormalizeCCImmunityType(ccType)
if not ccType then return nil end
local normalized = string.lower(ccType)
return CC_IMMUNITY_ALIASES[normalized] or normalized
end
-- Maps DBC mechanic IDs back to CC type names for immunity recording
-- Inverse of CleveRoids.CCMechanics (defined in Conditionals.lua)
local MECHANIC_TO_CC_TYPE = {
@@ -6029,14 +6062,36 @@ local MECHANIC_TO_CC_TYPE = {
[10] = "sleep",
[11] = "snare",
[12] = "stun",
[13] = "stun", -- freeze → stun (similar effect)
[14] = "stun", -- knockout/gouge → stun
[13] = "freeze",
[14] = "knockout",
[17] = "polymorph",
[18] = "banish",
[20] = "shackle",
[24] = "horror",
[27] = "disorient", -- daze → disorient
[30] = "stun", -- sap → stun
[27] = "daze",
[30] = "sap",
}
-- Mechanic-precise mapping used only for learned NPC immunity. Keep it
-- one-to-one: Vanilla can distinguish Stun (12), Freeze (13), Knockout (14)
-- and Sap (30) immunities independently, so never fold them together here.
local MECHANIC_TO_IMMUNITY_TYPE = {
[1] = "charm",
[2] = "disorient",
[5] = "fear",
[7] = "root",
[9] = "silence",
[10] = "sleep",
[11] = "snare",
[12] = "stun",
[13] = "freeze",
[14] = "knockout",
[17] = "polymorph",
[18] = "banish",
[20] = "shackle",
[24] = "horror",
[27] = "daze",
[30] = "sap",
}
-- Spells with split damage types (initial hit vs DoT/debuff)
@@ -6583,6 +6638,113 @@ end
-- Expose publicly for use by other modules
CleveRoids.GetSpellCCType = GetSpellCCType
-- Resolve the mechanic bucket used for learned NPC immunity. DBC spell/effect
-- mechanics are authoritative; aura type is only a fallback for spells whose
-- mechanic fields are empty.
local function GetSpellImmunityType(spellID)
if not spellID or spellID <= 0 then return nil end
if GetSpellRecField then
local mechanic = GetSpellRecField(spellID, "mechanic")
if mechanic and mechanic > 0 and MECHANIC_TO_IMMUNITY_TYPE[mechanic] then
return MECHANIC_TO_IMMUNITY_TYPE[mechanic]
end
local effectMechanics = GetSpellRecField(spellID, "effectMechanic")
if effectMechanics then
for i = 1, 3 do
local em = effectMechanics[i]
if em and em > 0 and MECHANIC_TO_IMMUNITY_TYPE[em] then
return MECHANIC_TO_IMMUNITY_TYPE[em]
end
end
end
local auraNames = GetSpellRecField(spellID, "effectApplyAuraName")
if auraNames then
for i = 1, 3 do
local an = auraNames[i]
if an and an > 0 and AURA_NAME_TO_CC_TYPE[an] then
return AURA_NAME_TO_CC_TYPE[an]
end
end
end
end
local mechanic = CleveRoids.ClassicAPI.GetSpellMechanicByID(spellID)
if mechanic and mechanic > 0 and MECHANIC_TO_IMMUNITY_TYPE[mechanic] then
return MECHANIC_TO_IMMUNITY_TYPE[mechanic]
end
return nil
end
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)
-- Parameters:
-- npcName: Name of the NPC that is immune
@@ -6594,6 +6756,8 @@ local function RecordCCImmunity(npcName, ccType, conditionalBuff, spellName)
return
end
ccType = NormalizeCCImmunityType(ccType)
-- Validate CC type
if not CC_IMMUNITY_TYPES[ccType] then
if CleveRoids.debug then
@@ -6653,6 +6817,8 @@ local function RemoveCCImmunity(npcName, ccType)
return
end
ccType = NormalizeCCImmunityType(ccType)
local key = "cc_" .. ccType
if CleveRoids_ImmunityData[key] and CleveRoids_ImmunityData[key][npcName] then
@@ -6677,6 +6843,8 @@ local function CheckCCImmunity(unitId, ccType)
return false
end
ccType = NormalizeCCImmunityType(ccType)
-- CC immunity only tracked for NPCs
if UnitIsPlayer(unitId) then
return false
@@ -7370,8 +7538,23 @@ function CleveRoids.CheckImmunity(unitId, spellOrSchool)
-- Check if input is a CC type (stun, fear, root, etc.)
local inputLower = string.lower(spellOrSchool)
if CC_IMMUNITY_TYPES[inputLower] then
return CheckCCImmunity(unitId, inputLower)
local ccInput = NormalizeCCImmunityType(inputLower)
if CC_IMMUNITY_TYPES[ccInput] then
return CheckCCImmunity(unitId, ccInput)
end
-- Spell-name queries should honour mechanic immunity as well as school
-- immunity. This makes bare [immune]/[noimmune] accurate for CC spells.
local ccSpellName = CleveRoids.StripRank(spellOrSchool)
if ccSpellName then
ccSpellName = string.gsub(ccSpellName, "_", " ")
end
local ccSpellID = GetSpellIdForName and GetSpellIdForName(ccSpellName)
if ccSpellID then
local immunityType = GetSpellImmunityType(ccSpellID)
if immunityType and CheckCCImmunity(unitId, immunityType) then
return true
end
end
-- Universal debuff-based immunities (Banish, etc.)
@@ -7625,11 +7808,11 @@ end
-- List CC immunities
function CleveRoids.ListCCImmunities(ccType)
if ccType then
ccType = string.lower(ccType)
ccType = NormalizeCCImmunityType(ccType)
local key = "cc_" .. ccType
if not CC_IMMUNITY_TYPES[ccType] then
CleveRoids.Print("Invalid CC type. Use: stun, fear, root, silence, sleep, charm, polymorph, banish, horror, disorient, snare")
CleveRoids.Print("Invalid CC type. Use: stun, freeze, knockout, sap, fear, root, silence, sleep, charm, polymorph, banish, shackle, horror, disorient, daze, snare")
return
end
@@ -7676,11 +7859,11 @@ end
-- Clear CC immunities
function CleveRoids.ClearCCImmunities(ccType)
if ccType then
ccType = string.lower(ccType)
ccType = NormalizeCCImmunityType(ccType)
local key = "cc_" .. ccType
if not CC_IMMUNITY_TYPES[ccType] then
CleveRoids.Print("Invalid CC type. Use: stun, fear, root, silence, sleep, charm, polymorph, banish, horror, disorient, snare")
CleveRoids.Print("Invalid CC type. Use: stun, freeze, knockout, sap, fear, root, silence, sleep, charm, polymorph, banish, shackle, horror, disorient, daze, snare")
return
end
@@ -7703,13 +7886,13 @@ end
function CleveRoids.AddCCImmunity(npcName, ccType, buffName)
if not npcName or not ccType then
CleveRoids.Print("Usage: /cleveroid addccimmune <npc name> <cctype> [buff name]")
CleveRoids.Print("CC Types: stun, fear, root, silence, sleep, charm, polymorph, banish, horror, disorient, snare")
CleveRoids.Print("CC Types: stun, freeze, knockout, sap, fear, root, silence, sleep, charm, polymorph, banish, shackle, horror, disorient, daze, snare")
return
end
ccType = string.lower(ccType)
ccType = NormalizeCCImmunityType(ccType)
if not CC_IMMUNITY_TYPES[ccType] then
CleveRoids.Print("Invalid CC type. Use: stun, fear, root, silence, sleep, charm, polymorph, banish, horror, disorient, snare")
CleveRoids.Print("Invalid CC type. Use: stun, freeze, knockout, sap, fear, root, silence, sleep, charm, polymorph, banish, shackle, horror, disorient, daze, snare")
return
end
@@ -7735,7 +7918,7 @@ function CleveRoids.RemoveCCImmunityCommand(npcName, ccType)
return
end
ccType = string.lower(ccType)
ccType = NormalizeCCImmunityType(ccType)
local key = "cc_" .. ccType
if CleveRoids_ImmunityData[key] and CleveRoids_ImmunityData[key][npcName] then
CleveRoids_ImmunityData[key][npcName] = nil
@@ -8202,13 +8385,15 @@ local function ProcessSpellMissSelf(spellId, targetGuid, missInfo)
end
end
-- Check DR before recording as permanent CC immunity
-- Check NPC-applicable DR before recording 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
local immunityType = GetSpellImmunityType(spellId) or ccType
local drType = GetSpellImmunityDRType(spellId)
if drType and targetGuid 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
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
return
end
@@ -8223,8 +8408,8 @@ local function ProcessSpellMissSelf(spellId, targetGuid, missInfo)
end
-- Record as permanent immunity
if ccType then
RecordCCImmunity(targetName, ccType, nil, spellName)
if immunityType then
RecordCCImmunity(targetName, immunityType, nil, spellName)
else
RecordImmunity(targetName, spellName, nil, spellId)
end
@@ -8527,14 +8712,14 @@ reactiveFrame:SetScript("OnEvent", function()
-- NAMPOWER v2.31+ SPELL_MISS EVENTS (preferred when available)
-- ========================================================================
if event == "SPELL_MISS_SELF" then
-- Player's spell missed: arg1=spellId, arg2=targetGuid, arg3=missInfo
ProcessSpellMissSelf(arg1, arg2, arg3)
-- Nampower: casterGuid, targetGuid, spellId, missInfo
ProcessSpellMissSelf(arg3, arg2, arg4)
return
end
if event == "SPELL_MISS_OTHER" then
-- Other unit's spell missed: arg1=spellId, arg2=casterGuid, arg3=targetGuid, arg4=missInfo
ProcessSpellMissOther(arg1, arg2, arg3, arg4)
-- Nampower: casterGuid, targetGuid, spellId, missInfo
ProcessSpellMissOther(arg3, arg1, arg2, arg4)
return
end
+188
View File
@@ -0,0 +1,188 @@
# CC, Learned Immunity and Diminishing Returns
How SuperCleveRoidMacros classifies crowd control, what it learns about NPC
immunity, and why diminishing returns only ever acts as a brake on that
learning. Source citations are into `C:\Git\vmangos` and `C:\Git\nampower`.
## Three identities, never conflated
```text
ccType Exact DBC mechanic. Backs [cc:*] and CC landing verification.
Conditionals.lua, CleveRoids.CCMechanics
immunityType Exact DBC mechanic, recorded against an NPC name when a CC
spell comes back IMMUNE.
Utility.lua, MECHANIC_TO_IMMUNITY_TYPE / GetSpellImmunityType
DR type The real Vanilla diminishing-return pool. Internal only.
Utility.lua, GetSpellImmunityDRType
```
The first two are the same taxonomy used for different purposes; the third is
a separate axis and is never exposed to macros. Folding any pair together is
what produced the bugs this design replaces: a target that resisted Gouge got
written down as stun-immune, and every later Kidney Shot was suppressed
against a target that had never resisted one.
## `[cc:*]` follows the DBC mechanic exactly
One conditional, one mechanic. Several names may alias a single mechanic, but
no name spans two:
```text
[cc:stun] -> mechanic 12 only
[cc:freeze] -> mechanic 13 only
[cc:knockout] -> mechanic 14 only
[cc:sap] -> mechanic 30 only
[cc:daze] -> mechanic 27 only
```
Bare `[cc]` and `[cc:any]` still mean "any loss-of-control effect" and are
driven by `CleveRoids.CCTypesLossOfControl`, which is deliberately independent
of the per-mechanic table. An aggregate does not require redefining the parts.
Current aliases:
```text
slow -> snare
disoriented -> disorient
grip -> fumble (mechanic 6 is DBC Fumble)
incap / incapacitate / incapacitated -> knockout (immunity input only)
```
Aliases exist so published macros keep working. They resolve to the canonical
mechanic name before anything is stored, so SavedVariables only ever hold
canonical keys.
## Vanilla DBC mechanic taxonomy
The identities SCRM names. Where a mechanic is named at all, it is named as
itself:
| DBC mechanic | ID | Canonical `ccType` |
|---|---:|---|
| Charm | 1 | `charm` |
| Disoriented | 2 | `disorient` |
| Disarm | 3 | `disarm` |
| Distract | 4 | `distract` |
| Fear | 5 | `fear` |
| Fumble | 6 | `fumble` |
| Root | 7 | `root` |
| Pacify | 8 | `pacify` |
| Silence | 9 | `silence` |
| Sleep | 10 | `sleep` |
| Snare | 11 | `snare` |
| Stun | 12 | `stun` |
| Freeze | 13 | `freeze` |
| Knockout | 14 | `knockout` |
| Bleed | 15 | `bleed` |
| Polymorph | 17 | `polymorph` |
| Banish | 18 | `banish` |
| Shackle | 20 | `shackle` |
| Turn | 23 | `turn` |
| Horror | 24 | `horror` |
| Interrupt | 26 | `interrupt` |
| Daze | 27 | `daze` |
| Sapped | 30 | `sap` |
Mechanics 16, 19, 21, 22, 25, 28 and 29 (Bandage, Shield, Mount, Persuade,
Invulnerability, Discovery, Immune Shield) are real DBC values but are not
crowd control, so they get no conditional.
## Diminishing returns
DR matters here for exactly one reason: a target at DR level 4 returns
`IMMUNE`, and that must not be recorded as permanent immunity. Nothing else in
the addon consumes DR state, and there is no player-facing DR feature.
### Only three pools can diminish a creature
`GetDiminishingReturnsGroupType` in
`vmangos/src/game/Spells/SpellEntry.h:52-79` returns `DRTYPE_ALL` for exactly
three groups:
```text
DIMINISHING_CONTROL_STUN
DIMINISHING_TRIGGER_STUN
DIMINISHING_KIDNEYSHOT
```
Every other staged group — sleep, both roots, fear, Warlock fear, charm,
polymorph, silence, disarm, Death Coil, freeze, banish, knockout — is
`DRTYPE_PLAYER`, and `Unit.cpp:7881` applies those only when the victim is a
player. So on an NPC target they can never generate a DR immunity, and they
must never hold back immunity learning.
This is why `GetSpellImmunityDRType` returns nil for everything that is not
mechanic 12. It is not an approximation; it is the complete `DRTYPE_ALL` set.
### Classifying a stun into its pool
`SpellEntry::GetDiminishingReturnsGroup(bool triggered)`
(`vmangos/src/game/Spells/SpellEntry.cpp:281-432`) resolves the pool, and the
`triggered` argument means the runtime fact of whether an aura triggered the
cast — this is not a pure DBC property. SCRM reconstructs it from nampower
`SPELL_CAST_EVENT` correlation, which only fires for client-initiated casts:
a spell ID with a recent `CleveRoids.pendingCasts` entry was cast deliberately,
one without reached `SPELL_GO`/`SPELL_MISS` as a proc.
```text
Kidney Shot -> stun_kidneyshot
Charge / Intercept stun -> stun_control
other mechanic-12, client cast -> stun_control
other mechanic-12, proc -> stun_trigger
```
Kidney Shot is matched on rogue family bit 21 (`0x00200000`,
`SpellClassMask.h:228`) rather than a bare ID list, so custom ranks that keep
their DBC family data still classify correctly. Charge (7922) and Intercept
(20253, 20614, 20615) are internally triggered but explicitly returned as
controlled stun by `SpellEntry.cpp:389-399`; they are the only such exceptions
in 1.12.
### Pools that exist but do not apply
Recorded so nobody re-adds them to the safeguard. Knockout and Sap are
distinct mechanics that share `DIMINISHING_KNOCKOUT`
(`SpellEntry.cpp:424-425`), and Horror maps to `DIMINISHING_DEATHCOIL`
(`SpellEntry.cpp:428-429`). Both are `DRTYPE_PLAYER`. `DIMINISHING_LIMITONLY`
is a PvP duration cap rather than a staged pool, and `DIMINISHING_NONE` is not
a pool at all.
### The safeguard itself
`recentCCHits[targetGUID][drType]` counts landed CC per DR pool, so the three
stun pools keep separate histories. A hit more than `DR_RESET_WINDOW` (20s)
after the previous one in that pool restarts the count at 1, matching the DR
decay the skip check uses. Without that reset the counter only ever climbed,
so three stuns on a target permanently disqualified it from ever teaching the
addon anything again.
An `IMMUNE` result is treated as DR, and discarded, only while the pool holds
three or more hits inside the window. Otherwise it is learned.
## nampower `SPELL_MISS` arguments
`TriggerSpellMissEvent` (`nampower/spellevents.cpp:888-917`) signals both
`SPELL_MISS_SELF` and `SPELL_MISS_OTHER` with the same layout:
```text
arg1 = casterGuid arg2 = targetGuid arg3 = spellId arg4 = missInfo
```
`SPELL_MISS_SELF` carries `casterGuid` too, even though it is by definition
the player — the event is chosen by comparing the caster to the active player
GUID, not by changing the payload. Reading `arg1` as the spell ID hands a GUID
string to the immunity recorder and silently loses every miss.
## Known limitation: mouseover casts
`ProcessSpellMissSelf` refuses to learn permanent immunity when it cannot
resolve a queryable unit, because the temporary-immunity-buff check needs one
— an NPC under Divine Shield must not be recorded as permanently immune. A CC
cast at a mouseover that is not the current target can therefore return
`IMMUNE` without being learned.
This is the safeguard working as designed, not a known bug. Do not loosen it
without a reproduction showing a real failure, since the failure mode on the
other side is permanent bad data in `CleveRoids_ImmunityData`.