mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37ca7cef40 | |||
| 4474a607f7 | |||
| ca6c55a5e7 | |||
| c1a7bdb51b | |||
| 5d5f987555 |
+13
-26
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user