mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
Compare commits
10
Commits
87ea8bdd41
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37ca7cef40 | ||
|
|
4474a607f7 | ||
|
|
ca6c55a5e7 | ||
|
|
c1a7bdb51b | ||
|
|
5d5f987555 | ||
|
|
eddc49fc60 | ||
|
|
54e3c3b3f4 | ||
|
|
4fa254b4e6 | ||
|
|
b82d06d061 | ||
|
|
891467cd7e |
+14
-14
@@ -3,7 +3,8 @@
|
||||
|
||||
ClassicAPI is a client mod (sibling to Nampower/SuperWoW) that backports the
|
||||
modern C_* API into the 1.12.1 Lua environment. It is a HARD REQUIREMENT of
|
||||
this addon (ClassicAPI v1.15.0+, which added frame:RegisterUnitEvent), so the
|
||||
this addon (ClassicAPI v1.15.8+, which scoped GetMouseButtonClicked to the
|
||||
click dispatch; v1.15.0 added frame:RegisterUnitEvent), so the
|
||||
wrappers below call the API directly — no fallbacks. The load-time requirement
|
||||
check (Core.lua) uses IsAvailable() to warn when the DLL is missing and
|
||||
HasMinimumVersion() when it's too old; users who don't want ClassicAPI should
|
||||
@@ -247,8 +248,8 @@ end
|
||||
-- Pummel / Earth Shock lockout) on the player, or 0 when not kicked. Read from
|
||||
-- C_LossOfControl, which synthesizes the lockout from the server's own
|
||||
-- SMSG_SPELL_COOLDOWN packet -- a state no debuff scan can see. Also returns the
|
||||
-- seconds remaining (nil if ClassicAPI didn't observe the applying cast). Returns
|
||||
-- 0 for a client without C_LossOfControl. Player-only (vanilla LoC is local-only).
|
||||
-- seconds remaining (nil if ClassicAPI didn't observe the applying cast).
|
||||
-- Player-only (vanilla LoC is local-only).
|
||||
function API.GetSchoolLockout()
|
||||
local n = C_LossOfControl.GetActiveLossOfControlDataCount() or 0
|
||||
for i = 1, n do
|
||||
@@ -312,10 +313,9 @@ end
|
||||
-- Unit Health
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Health deficit (max - current) for `unit` in one call. Falls back to
|
||||
-- UnitHealthMax - UnitHealth without ClassicAPI.
|
||||
API.UnitHealthMissing = UnitHealthMissing or function(unit)
|
||||
return (UnitHealthMax(unit) or 0) - (UnitHealth(unit) or 0)
|
||||
-- Health deficit (max - current) for `unit` in one call.
|
||||
function API.UnitHealthMissing(unit)
|
||||
return UnitHealthMissing(unit)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -324,18 +324,18 @@ end
|
||||
|
||||
-- Current power for a specific Enum.PowerType (0=Mana, 1=Rage, 2=Focus,
|
||||
-- 3=Energy, 4=Happiness), or the unit's primary power when powerType is omitted.
|
||||
-- Display-divided (rage reads 0..100). Falls back to UnitMana without ClassicAPI.
|
||||
API.UnitPower = UnitPower or function(unit, powerType)
|
||||
return UnitMana(unit)
|
||||
-- Display-divided (rage reads 0..100).
|
||||
function API.UnitPower(unit, powerType)
|
||||
return UnitPower(unit, powerType)
|
||||
end
|
||||
|
||||
API.UnitPowerMax = UnitPowerMax or function(unit, powerType)
|
||||
return UnitManaMax(unit)
|
||||
function API.UnitPowerMax(unit, powerType)
|
||||
return UnitPowerMax(unit, powerType)
|
||||
end
|
||||
|
||||
-- Power deficit (max - current) for the type / primary power, in one call.
|
||||
API.UnitPowerMissing = UnitPowerMissing or function(unit, powerType)
|
||||
return (UnitManaMax(unit) or 0) - (UnitMana(unit) or 0)
|
||||
function API.UnitPowerMissing(unit, powerType)
|
||||
return UnitPowerMissing(unit, powerType)
|
||||
end
|
||||
|
||||
-- Unit's primary power type as an integer (0=Mana .. 4=Happiness).
|
||||
|
||||
+146
-36
@@ -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
|
||||
|
||||
@@ -5241,32 +5228,154 @@ local function ResolvePetHappinessState(value)
|
||||
return PET_HAPPINESS_STATES[GetLowercaseString(value)]
|
||||
end
|
||||
|
||||
-- ClassicAPI's totem bar is TBC-ordered: SUMMON_TOTEM_SLOT1..4 in Spell.dbc.
|
||||
local TOTEM_SLOTS = {
|
||||
fire = 1,
|
||||
earth = 2,
|
||||
water = 3,
|
||||
air = 4,
|
||||
["1"] = 1,
|
||||
["2"] = 2,
|
||||
["3"] = 3,
|
||||
["4"] = 4,
|
||||
}
|
||||
|
||||
-- Resolve a [totem:X] argument to a slot. An element name or slot number names
|
||||
-- the slot directly; anything else is matched against the name of the totem
|
||||
-- standing in each slot, so [totem:Searing_Totem] asks for that totem rather
|
||||
-- than "whatever is in the fire slot". nil when neither matches -- which
|
||||
-- includes naming a totem that is not currently out.
|
||||
local function ResolveTotemSlot(name)
|
||||
if type(name) ~= "string" or name == "" then return nil end
|
||||
|
||||
local searchName = GetLowercaseString(
|
||||
CleveRoids.Trim(string.gsub(CleveRoids.StripRank(name), "_", " ")))
|
||||
|
||||
local slot = TOTEM_SLOTS[searchName]
|
||||
if slot then return slot end
|
||||
|
||||
for i = 1, 4 do
|
||||
-- Second return, not the first: GetTotemInfo's haveTotem reports whether
|
||||
-- the player carries the slot's TOOL item, not whether a totem is out.
|
||||
local _, totemName = GetTotemInfo(i)
|
||||
if totemName and totemName ~= "" and GetLowercaseString(totemName) == searchName then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- True while any totem slot has a timer running. Drives the OnUpdate re-test
|
||||
-- that keeps [totem:X<N] icons honest -- see the caller for why polling is the
|
||||
-- only option. GetTotemTimeLeft, not GetTotemInfo: this asks whether a countdown
|
||||
-- is in progress (a totem with no timer has nothing to go stale), and it reads
|
||||
-- the slot alone, where GetTotemInfo walks the bags for the tool item.
|
||||
function CleveRoids.AnyTotemTimerRunning()
|
||||
for i = 1, 4 do
|
||||
local timeLeft = GetTotemTimeLeft(i)
|
||||
if timeLeft and timeLeft > 0 then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- [totem:X] / [nototem:X], shaped like the aura validators: X is a plain name or
|
||||
-- a parsed comparison entry, comparisons read seconds left, and `#N` stack
|
||||
-- comparisons read 1 for a standing totem -- a totem is either up or it isn't.
|
||||
--
|
||||
-- An empty slot reads -1 on both axes, the same "missing counts as least" the
|
||||
-- aura path uses, so [totem:Searing_Totem<5] passes while the totem is expiring
|
||||
-- AND while it is absent: one clause for the whole recast macro.
|
||||
local function ValidateTotem(args)
|
||||
if not args then return false end
|
||||
|
||||
if type(args) ~= "table" then
|
||||
args = { name = args }
|
||||
end
|
||||
|
||||
local remaining, stacks = -1, -1
|
||||
local slot = ResolveTotemSlot(args.name)
|
||||
if slot then
|
||||
-- Occupancy comes from the name, not from the time left: a totem whose
|
||||
-- summon spell carries no SpellDuration row reads 0 seconds, and that is
|
||||
-- "up with no timer", not "absent".
|
||||
local _, totemName = GetTotemInfo(slot)
|
||||
if totemName and totemName ~= "" then
|
||||
stacks = 1
|
||||
remaining = GetTotemTimeLeft(slot)
|
||||
end
|
||||
end
|
||||
|
||||
local ops = CleveRoids.operators
|
||||
local cmp = CleveRoids.comparators
|
||||
|
||||
-- Multi-comparison (e.g. >2&<8) - ALL must pass
|
||||
if args.comparisons and type(args.comparisons) == "table" then
|
||||
for _, comp in ipairs(args.comparisons) do
|
||||
if not ops[comp.operator] then return false end
|
||||
local value = comp.checkStacks and stacks or remaining
|
||||
if not cmp[comp.operator](value, comp.amount) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if not args.amount and not args.operator and not args.checkStacks then
|
||||
return stacks == 1
|
||||
elseif args.amount and ops[args.operator] then
|
||||
return cmp[args.operator](args.checkStacks and stacks or remaining, args.amount)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- A list of Conditionals and their functions to validate them
|
||||
CleveRoids.Keywords = {
|
||||
-- [button:N] — true while mouse button N is held (1=Left, 2=Right, 3=Middle,
|
||||
-- 4/5=extra). Routed through Multi so OR/AND lists and repeated groups behave
|
||||
-- like every other argument conditional ([button:1/2] = left or right).
|
||||
-- [button] with no argument — true if any mapped mouse button is held.
|
||||
-- [button:N] — N is the button that INVOKED this action (1=Left, 2=Right,
|
||||
-- 3=Middle, 4/5=extra), not one held down as a modifier. That is retail's
|
||||
-- meaning, and it is why a keybind press counts as button 1: retail activates
|
||||
-- through the left-click path, so [button:1] passes for a keybind and a
|
||||
-- left-click alike, while [button:2] passes only for an actual right-click.
|
||||
-- Routed through Multi so OR/AND lists behave like every other argument
|
||||
-- conditional ([button:1/2] = invoked by left or right).
|
||||
--
|
||||
-- Bare [button] / [nobutton] ask whether a click drove this at all -- the one
|
||||
-- thing the underlying data says that retail's conditional cannot express,
|
||||
-- and the practical "activated by a keybind, not a click" test.
|
||||
button = function(conditionals)
|
||||
if type(conditionals.button) ~= "table" then
|
||||
return CleveRoids.AnyMouseButtonDown()
|
||||
return CleveRoids.WasClickActivated()
|
||||
end
|
||||
local invoking = CleveRoids.GetActivatingButton()
|
||||
return Multi(conditionals.button, function(button)
|
||||
local name = CleveRoids.buttons[button]
|
||||
return name and IsMouseButtonDown(name) or false
|
||||
return CleveRoids.buttons[button] == invoking
|
||||
end, conditionals, "button")
|
||||
end,
|
||||
|
||||
nobutton = function(conditionals)
|
||||
if type(conditionals.nobutton) ~= "table" then
|
||||
return not CleveRoids.AnyMouseButtonDown()
|
||||
return not CleveRoids.WasClickActivated()
|
||||
end
|
||||
local invoking = CleveRoids.GetActivatingButton()
|
||||
return NegatedMulti(conditionals.nobutton, function(button)
|
||||
local name = CleveRoids.buttons[button]
|
||||
return not (name and IsMouseButtonDown(name))
|
||||
return CleveRoids.buttons[button] ~= invoking
|
||||
end, conditionals, "nobutton")
|
||||
end,
|
||||
|
||||
-- [totem:X] — X names a totem-bar slot (fire/earth/water/air, or 1-4) or the
|
||||
-- totem itself ([totem:Searing_Totem]). Bare [totem] takes the action as its
|
||||
-- argument, as [mybuff] does, so /cast [nototem] Searing Totem is the whole
|
||||
-- recast macro. Time and stack comparisons follow the aura precedent; see
|
||||
-- ValidateTotem.
|
||||
totem = function(conditionals)
|
||||
return Multi(conditionals.totem, function(v)
|
||||
return ValidateTotem(v)
|
||||
end, conditionals, "totem")
|
||||
end,
|
||||
|
||||
nototem = function(conditionals)
|
||||
return NegatedMulti(conditionals.nototem, function(v)
|
||||
return not ValidateTotem(v)
|
||||
end, conditionals, "nototem")
|
||||
end,
|
||||
|
||||
exists = function(conditionals)
|
||||
return UnitExists(conditionals.target)
|
||||
end,
|
||||
@@ -9200,6 +9309,7 @@ CleveRoids.STATIC_CONDITIONALS = {
|
||||
mod = true, nomod = true,
|
||||
keydown = true, nokeydown = true,
|
||||
button = true, nobutton = true,
|
||||
totem = true, nototem = true,
|
||||
swimming = true, noswimming = true, swim = true, noswim = true,
|
||||
indoors = true, noindoors = true, outdoors = true, nooutdoors = true,
|
||||
rooted = true, norooted = true,
|
||||
|
||||
@@ -201,9 +201,13 @@ requirementCheckFrame:SetScript("OnEvent", function()
|
||||
-- v1.15.0 added frame:RegisterUnitEvent, which Utility.lua calls at file scope.
|
||||
-- On an older build that call raises, aborting the rest of the chunk -- so this
|
||||
-- is not a degraded-features warning, it's "the addon did not finish loading".
|
||||
local hasClassicAPI1150 = hasClassicAPI and CleveRoids.ClassicAPI.HasMinimumVersion(1, 15, 0)
|
||||
-- v1.15.8 scoped GetMouseButtonClicked to the click dispatch. Below it the
|
||||
-- function exists but a held button keeps its value, so [button:N] reads the
|
||||
-- button you are turning the camera with -- wrong quietly, which is the worse
|
||||
-- failure and why the floor moved rather than the conditional being gated.
|
||||
local hasClassicAPI1158 = hasClassicAPI and CleveRoids.ClassicAPI.HasMinimumVersion(1, 15, 8)
|
||||
|
||||
if not hasNampower30 or not hasUnitXP or not hasClassicAPI or not hasClassicAPI1150 then
|
||||
if not hasNampower30 or not hasUnitXP or not hasClassicAPI or not hasClassicAPI1158 then
|
||||
-- Show warnings (don't disable — tearing down a partially-initialized addon causes hangs)
|
||||
if not hasNampower then
|
||||
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFAvitasia's Nampower v3.0.0+|r is required:")
|
||||
@@ -223,9 +227,9 @@ requirementCheckFrame:SetScript("OnEvent", function()
|
||||
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI|r is required:")
|
||||
CleveRoids.Print("https://github.com/brues-code/ClassicAPI")
|
||||
CleveRoids.Print("Dispel-type and movement conditionals will be unavailable without it.")
|
||||
elseif not hasClassicAPI1150 then
|
||||
elseif not hasClassicAPI1158 then
|
||||
local major, minor, patch = CleveRoids.ClassicAPI.GetVersion()
|
||||
CleveRoids.Print(format("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI v1.15.0+|r is required (you have v%d.%d.%d):", major, minor, patch))
|
||||
CleveRoids.Print(format("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI v1.15.8+|r is required (you have v%d.%d.%d):", major, minor, patch))
|
||||
CleveRoids.Print("https://github.com/brues-code/ClassicAPI")
|
||||
CleveRoids.Print("The addon cannot finish loading on this version -- update ClassicAPI.")
|
||||
end
|
||||
@@ -4190,6 +4194,21 @@ function CleveRoids.OnUpdate(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- A totem's timer runs down with nothing behind it: PLAYER_TOTEM_UPDATE fires
|
||||
-- when a slot changes hands, never as one counts toward zero, so a
|
||||
-- [totem:X<N] icon would hold the answer from its last refresh until some
|
||||
-- unrelated event queued another. Unlike an aura, there is no ambient event
|
||||
-- stream to ride -- UNIT_AURA is what quietly keeps [mybuff:X<N] honest -- so
|
||||
-- a re-test once a second while any totem timer runs is the whole mechanism.
|
||||
-- Only the icon is at stake: the click path evaluates conditionals live.
|
||||
-- Realtime mode re-tests everything each tick and needs none of this.
|
||||
if CRM.realtime == 0 and time >= (CR.nextTotemPoll or 0) then
|
||||
CR.nextTotemPoll = time + 1
|
||||
if CR.AnyTotemTimerRunning() then
|
||||
CR.isActionUpdateQueued = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Check the saved variable to decide which update mode to use.
|
||||
if CRM.realtime == 1 then
|
||||
-- Realtime Mode: Force an update on every throttled tick for maximum responsiveness.
|
||||
@@ -4447,12 +4466,8 @@ CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
|
||||
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
|
||||
CleveRoids.Frame:RegisterEvent("PLAYER_STARTED_MOVING")
|
||||
-- ClassicAPI loss-of-control (school-interrupt lockout) for [locked]/[nolocked].
|
||||
-- Gated on the namespace so an older ClassicAPI without it doesn't error on an
|
||||
-- unknown event.
|
||||
if type(C_LossOfControl) == "table" then
|
||||
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_ADDED")
|
||||
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
|
||||
end
|
||||
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_ADDED")
|
||||
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
|
||||
-- The unit state streams that drive icon refresh: GUID events when Nampower
|
||||
-- provides them (v2.39+, one event per unit change rather than one per token),
|
||||
-- else the stock per-token events. These cannot become RegisterUnitEvent calls
|
||||
@@ -4516,6 +4531,11 @@ if CleveRoids.NampowerAPI.features.hasKeyEvents then
|
||||
CleveRoids.Frame:RegisterEvent("KEY_UP")
|
||||
end
|
||||
|
||||
-- ClassicAPI totem-bar tracking, for [totem:X] icon refresh. Ungated, unlike the
|
||||
-- Nampower features above: it long predates the ClassicAPI floor the addon
|
||||
-- already refuses to run below.
|
||||
CleveRoids.Frame:RegisterEvent("PLAYER_TOTEM_UPDATE")
|
||||
|
||||
-- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua
|
||||
-- which has the complete implementation including the INTERCEPT path for all commands
|
||||
|
||||
@@ -5567,6 +5587,14 @@ function CleveRoids.Frame:KEY_UP()
|
||||
CleveRoids.isActionUpdateQueued = true
|
||||
end
|
||||
|
||||
-- ClassicAPI PLAYER_TOTEM_UPDATE: arg1 = the slot (1 Fire .. 4 Air) whose totem
|
||||
-- was dropped, expired, killed or recalled. The slot goes unread and every macro
|
||||
-- is refreshed: a [totem:X] macro may name any slot, and [nototem:X] flips on any
|
||||
-- of them.
|
||||
function CleveRoids.Frame:PLAYER_TOTEM_UPDATE()
|
||||
CleveRoids.QueueActionUpdate()
|
||||
end
|
||||
|
||||
-- Base SendChatMessage captured at first hook; used to break a hook cycle.
|
||||
local baseSendChatMessage = SendChatMessage
|
||||
local sendingChatMessage = false
|
||||
|
||||
@@ -45,8 +45,9 @@ CleveRoids.supported = CleveRoids.hasTurtle
|
||||
-- "may we call C_Macro.SetMacroDisplay" guard, so the two can never disagree.
|
||||
--
|
||||
-- Feature-detect rather than version-check: SetMacroDisplay ships in ClassicAPI
|
||||
-- v1.15.0, this addon's minimum, so a nil here means the client mod is missing
|
||||
-- outright -- the case Core.lua's requirement check warns about but keeps running.
|
||||
-- v1.15.0, below this addon's minimum, so a nil here means the client mod is
|
||||
-- missing outright -- the case Core.lua's requirement check warns about but
|
||||
-- keeps running.
|
||||
CleveRoids.ClassicAPIMacroDisplay =
|
||||
(type(C_Macro) == "table" and C_Macro.SetMacroDisplay ~= nil) and true or false
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
|
||||
|-----|:--------:|---------|
|
||||
| [Nampower](https://github.com/brues-code/nampower/releases) (v3.0.0+) | ✅ | Spell queueing, DBC data, auto-attack events |
|
||||
| [UnitXP_SP3](https://codeberg.org/konaka/UnitXP_SP3/releases) | ✅ | Distance checks, `[multiscan]` enemy scanning |
|
||||
| [ClassicAPI](https://github.com/brues-code/ClassicAPI/releases) (v1.15.0+) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed, unit-filtered events |
|
||||
| [ClassicAPI](https://github.com/brues-code/ClassicAPI/releases) (v1.15.8+) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed, unit-filtered events |
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
+247
-52
@@ -734,14 +734,24 @@ CleveRoids.buttons = {
|
||||
['5'] = 'Button5',
|
||||
}
|
||||
|
||||
-- True while any mapped mouse button is held. Backs the argument-less [button] /
|
||||
-- [nobutton], mirroring how a bare [mod] means "any modifier". Bare [nobutton] is
|
||||
-- the practical "activated by a keybind, not a click" test.
|
||||
function CleveRoids.AnyMouseButtonDown()
|
||||
for _, name in pairs(CleveRoids.buttons) do
|
||||
if IsMouseButtonDown(name) then return true end
|
||||
end
|
||||
return false
|
||||
-- The button that invoked the action now running, as retail's [button:N] means
|
||||
-- it. ClassicAPI v1.15.8+ scopes GetMouseButtonClicked to the click dispatch, so
|
||||
-- it reads the button of the handler this macro is running under -- through the
|
||||
-- helpers the handler calls, not just the handler itself -- and nil at any other
|
||||
-- time. A held button no longer keeps it set, which is what made mouse-turning
|
||||
-- poison every keybind press before v1.15.8.
|
||||
--
|
||||
-- nil outside a click dispatch, so the tooltip resolves as button 1 and sits
|
||||
-- still rather than tracking the mouse. Button:Click() with no argument reports
|
||||
-- LeftButton, which is how a keybind comes out as button 1 on retail.
|
||||
function CleveRoids.GetActivatingButton()
|
||||
return GetMouseButtonClicked() or "LeftButton"
|
||||
end
|
||||
|
||||
-- True while a click dispatch is what is running this action. Backs bare
|
||||
-- [button] / [nobutton], which retail has no equivalent for.
|
||||
function CleveRoids.WasClickActivated()
|
||||
return GetMouseButtonClicked() ~= nil
|
||||
end
|
||||
|
||||
CleveRoids.kmods = {
|
||||
@@ -837,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)
|
||||
@@ -2317,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
|
||||
@@ -3103,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
|
||||
@@ -3147,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
|
||||
@@ -3606,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
|
||||
@@ -3644,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
|
||||
})
|
||||
|
||||
@@ -4360,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
|
||||
@@ -4380,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
|
||||
})
|
||||
@@ -5995,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
|
||||
@@ -6003,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 = {
|
||||
@@ -6019,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)
|
||||
@@ -6573,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
|
||||
@@ -6584,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
|
||||
@@ -6643,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
|
||||
@@ -6667,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
|
||||
@@ -7360,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.)
|
||||
@@ -7615,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
|
||||
|
||||
@@ -7666,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
|
||||
|
||||
@@ -7693,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
|
||||
|
||||
@@ -7725,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
|
||||
@@ -8192,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
|
||||
@@ -8213,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
|
||||
@@ -8517,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