mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bbb8df010 | |||
| 96dbe9aca8 | |||
| 1de455f96e | |||
| d93d97a469 | |||
| 2a562e0550 | |||
| daaab2fd11 | |||
| 43f1ba378a | |||
| 828acc0c4b | |||
| f296425c94 | |||
| 4dd5326172 | |||
| c5ddc6c313 |
+19
-32
@@ -3,17 +3,11 @@
|
||||
|
||||
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.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
|
||||
run the upstream addon.
|
||||
|
||||
The minimum is not advisory: Utility.lua calls frame:RegisterUnitEvent at file
|
||||
scope, so an older ClassicAPI aborts that chunk and leaves most of the addon
|
||||
undefined. Raise the Core.lua minimum in step with any new API adopted at file
|
||||
scope.
|
||||
this addon (ClassicAPI v1.12.1+, which added the positional
|
||||
C_UnitAuras.UnitAura), 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 run the upstream addon.
|
||||
|
||||
Detection: the global CLASSIC_API_VERSION is defined once the client has
|
||||
booted, encoded as X*10000 + Y*100 + Z for a vX.Y.Z tag (untagged dev builds
|
||||
@@ -43,14 +37,6 @@ function API.GetVersionNumber()
|
||||
return CLASSIC_API_VERSION or 0
|
||||
end
|
||||
|
||||
-- Returns the loaded version as major, minor, patch (0, 0, 0 if absent).
|
||||
function API.GetVersion()
|
||||
local v = CLASSIC_API_VERSION or 0
|
||||
local major = math.floor(v / 10000)
|
||||
local minor = math.floor(v / 100) - major * 100
|
||||
return major, minor, v - math.floor(v / 100) * 100
|
||||
end
|
||||
|
||||
-- True if the ClassicAPI client mod is loaded at all.
|
||||
function API.IsAvailable()
|
||||
return CLASSIC_API_VERSION ~= nil
|
||||
@@ -70,7 +56,7 @@ end
|
||||
|
||||
-- Scan one aura range of `unit` (filter = "HELPFUL" or "HARMFUL") for an aura
|
||||
-- matching the dispel type. Uses the positional C_UnitAuras.UnitAura (added in
|
||||
-- ClassicAPI v1.12.1, below this addon's minimum) -- no table allocated per slot, with
|
||||
-- ClassicAPI v1.12.1, this addon's minimum) -- no table allocated per slot, with
|
||||
-- dispelName as the 4th return. The filtered index self-terminates at the end of
|
||||
-- the range (nil name); 48 is a backstop over vanilla's 32 helpful / 16 harmful slots.
|
||||
local function scanDispel(unit, filter, dispelType, wantAny)
|
||||
@@ -248,8 +234,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).
|
||||
-- Player-only (vanilla LoC is local-only).
|
||||
-- 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).
|
||||
function API.GetSchoolLockout()
|
||||
local n = C_LossOfControl.GetActiveLossOfControlDataCount() or 0
|
||||
for i = 1, n do
|
||||
@@ -313,9 +299,10 @@ end
|
||||
-- Unit Health
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Health deficit (max - current) for `unit` in one call.
|
||||
function API.UnitHealthMissing(unit)
|
||||
return UnitHealthMissing(unit)
|
||||
-- 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)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -324,18 +311,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).
|
||||
function API.UnitPower(unit, powerType)
|
||||
return UnitPower(unit, powerType)
|
||||
-- Display-divided (rage reads 0..100). Falls back to UnitMana without ClassicAPI.
|
||||
API.UnitPower = UnitPower or function(unit, powerType)
|
||||
return UnitMana(unit)
|
||||
end
|
||||
|
||||
function API.UnitPowerMax(unit, powerType)
|
||||
return UnitPowerMax(unit, powerType)
|
||||
API.UnitPowerMax = UnitPowerMax or function(unit, powerType)
|
||||
return UnitManaMax(unit)
|
||||
end
|
||||
|
||||
-- Power deficit (max - current) for the type / primary power, in one call.
|
||||
function API.UnitPowerMissing(unit, powerType)
|
||||
return UnitPowerMissing(unit, powerType)
|
||||
API.UnitPowerMissing = UnitPowerMissing or function(unit, powerType)
|
||||
return (UnitManaMax(unit) or 0) - (UnitMana(unit) or 0)
|
||||
end
|
||||
|
||||
-- Unit's primary power type as an integer (0=Mana .. 4=Happiness).
|
||||
|
||||
+86
-6
@@ -15,6 +15,23 @@ CleveRoids.ComboPointTracking = CleveRoids.ComboPointTracking or {}
|
||||
-- Structure: CleveRoids_ComboDurations[spellID][comboPoints] = duration
|
||||
CleveRoids_ComboDurations = CleveRoids_ComboDurations or {}
|
||||
|
||||
-- Storage for last Rip cast (for Carnage talent mechanic)
|
||||
-- Carnage talent: When Ferocious Bite procs Carnage, it refreshes Rip and Rake to their original duration
|
||||
-- Detection: When combo points don't drop to 0 after FB (they stay at 1 = Carnage proc)
|
||||
-- Talent Position: Tab 2 (Feral Combat), Talent 17
|
||||
-- Rank 1: 10% per CP, Rank 2: 20% per CP
|
||||
CleveRoids.lastRipCast = CleveRoids.lastRipCast or {
|
||||
duration = nil,
|
||||
targetGUID = nil,
|
||||
timestamp = 0
|
||||
}
|
||||
|
||||
CleveRoids.lastRakeCast = CleveRoids.lastRakeCast or {
|
||||
duration = nil,
|
||||
targetGUID = nil,
|
||||
timestamp = 0
|
||||
}
|
||||
|
||||
-- Family membership without hardcoded rank lists. C_Spell.GetSpellName resolves
|
||||
-- ANY spellID from the client's Spell.dbc -- every rank (so no enumeration or
|
||||
-- spellbook scan) and TWoW's custom spells alike -- so "is this spellID a Rip?"
|
||||
@@ -69,8 +86,10 @@ CleveRoids.ComboScalingSpellsByID = {
|
||||
[9896] = { base = 10, increment = 2, name = "Rip" }, -- Rank 6
|
||||
}
|
||||
|
||||
-- Ferocious Bite spell IDs. A combo-point finisher, so the cast-time combo
|
||||
-- snapshot has to know it.
|
||||
-- Ferocious Bite spell IDs (for Carnage talent mechanic)
|
||||
-- Carnage talent: When FB procs Carnage, refreshes Rip and Rake back to their original duration
|
||||
-- Proc detection: combo points stay at 1 after FB instead of dropping to 0
|
||||
-- Talent Position: Tab 2 (Feral Combat), Talent 17
|
||||
CleveRoids.FerociousBiteSpellIDs = {
|
||||
[22557] = true, -- Rank 1
|
||||
[22568] = true, -- Rank 2
|
||||
@@ -80,6 +99,10 @@ CleveRoids.FerociousBiteSpellIDs = {
|
||||
[31018] = true, -- Rank 6
|
||||
}
|
||||
|
||||
-- Rip / Rake families (for Carnage talent). Seeded by Rank 1; matches every rank.
|
||||
CleveRoids.RipSpellIDs = RankSet(1079)
|
||||
CleveRoids.RakeSpellIDs = RankSet(1822)
|
||||
|
||||
-- Combined table for all bleed spells that need immunity detection
|
||||
-- Used when checking if a cast bleed failed to apply (indicates bleed immunity)
|
||||
-- NOTE: These are the DEBUFF spell IDs (what appears on target), not cast spell IDs
|
||||
@@ -145,6 +168,14 @@ CleveRoids.ImmolateSpellIDs = {
|
||||
[25309] = true, -- Rank 8
|
||||
}
|
||||
|
||||
-- =============================================================================
|
||||
-- WARLOCK: Dark Harvest Duration Acceleration (TWoW Custom)
|
||||
-- Channeled spell that accelerates DoT tick rate by 30% while channeling
|
||||
-- Complex tracking: debuff expires 30% faster while Dark Harvest is active
|
||||
-- =============================================================================
|
||||
-- TWoW custom (see MoltenBlast note).
|
||||
CleveRoids.DarkHarvestSpellIDs = RankSet(52550)
|
||||
|
||||
-- =============================================================================
|
||||
-- DRUID: Rake Debuff Cap Boss Whitelist
|
||||
-- These bosses are likely to hit the 48 debuff cap, causing Rake to get pushed off
|
||||
@@ -714,7 +745,7 @@ function Extension.OnLoad()
|
||||
Extension.RegisterEvent("SPELLCAST_FAILED", "OnSpellcastFailed")
|
||||
Extension.RegisterEvent("SPELLCAST_INTERRUPTED", "OnSpellcastInterrupted")
|
||||
Extension.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged")
|
||||
Extension.RegisterUnitEvent("UNIT_AURA", "OnUnitAura", "target", "player")
|
||||
Extension.RegisterEvent("UNIT_AURA", "OnUnitAura")
|
||||
Extension.RegisterEvent("PLAYER_COMBO_POINTS", "OnComboPointsChanged")
|
||||
|
||||
-- PERFORMANCE OPTIMIZATION: Removed OnUpdate polling for combo points
|
||||
@@ -734,14 +765,63 @@ function Extension.OnTargetChanged()
|
||||
CleveRoids.UpdateComboPoints()
|
||||
end
|
||||
|
||||
-- Registered as a unit event for target and player, so arg1 is always one of
|
||||
-- those two -- no token check needed.
|
||||
function Extension.OnUnitAura()
|
||||
CleveRoids.UpdateComboPoints()
|
||||
if arg1 == "target" or arg1 == "player" then
|
||||
CleveRoids.UpdateComboPoints()
|
||||
end
|
||||
end
|
||||
|
||||
function Extension.OnComboPointsChanged()
|
||||
CleveRoids.UpdateComboPoints()
|
||||
|
||||
-- CARNAGE PROC DETECTION (Cursive-style)
|
||||
-- When Ferocious Bite is used, combo points should drop to 0
|
||||
-- If Carnage procs, combo points will be 1 instead (the Carnage-granted combo point)
|
||||
-- Check: After Ferocious Bite (within 0.5s), if combo points > 0, Carnage procced
|
||||
if CleveRoids.lastFerociousBiteTime and CleveRoids.lastFerociousBiteTargetGUID then
|
||||
local timeSinceBite = GetTime() - CleveRoids.lastFerociousBiteTime
|
||||
if timeSinceBite < 0.5 then
|
||||
local currentCP = CleveRoids.GetComboPoints()
|
||||
if currentCP > 0 then
|
||||
-- Carnage procced! Combo points didn't drop to 0 (or rose back to 1)
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cffff00ff[Carnage]|r PROC DETECTED! CP=%d after Ferocious Bite (%.2fs ago)",
|
||||
currentCP, timeSinceBite)
|
||||
)
|
||||
end
|
||||
|
||||
-- Apply the Carnage refresh to Rip and Rake
|
||||
local targetGUID = CleveRoids.lastFerociousBiteTargetGUID
|
||||
local targetName = CleveRoids.lastFerociousBiteTargetName or "Unknown"
|
||||
local biteSpellID = CleveRoids.lastFerociousBiteSpellID
|
||||
|
||||
-- Call the Carnage refresh function in Utility.lua
|
||||
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyCarnageRefresh then
|
||||
CleveRoids.libdebuff.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID)
|
||||
end
|
||||
|
||||
-- Clear the tracking to prevent multiple refreshes
|
||||
CleveRoids.lastFerociousBiteTime = nil
|
||||
CleveRoids.lastFerociousBiteTargetGUID = nil
|
||||
CleveRoids.lastFerociousBiteTargetName = nil
|
||||
CleveRoids.lastFerociousBiteSpellID = nil
|
||||
end
|
||||
else
|
||||
-- Time window expired, clear tracking
|
||||
if CleveRoids.lastFerociousBiteTime then
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cffff00ff[Carnage]|r No proc - time window expired (%.2fs)", timeSinceBite)
|
||||
)
|
||||
end
|
||||
CleveRoids.lastFerociousBiteTime = nil
|
||||
CleveRoids.lastFerociousBiteTargetGUID = nil
|
||||
CleveRoids.lastFerociousBiteTargetName = nil
|
||||
CleveRoids.lastFerociousBiteSpellID = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Event handlers
|
||||
|
||||
+73
-146
@@ -897,6 +897,43 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo,
|
||||
CleveRoids.LastSwing.resistAmount = totalResist or 0
|
||||
CleveRoids.LastSwing.targetGuid = targetGuid
|
||||
|
||||
-- Paladin: refresh active Judgements on melee hit (Nampower fallback for UNIT_CASTEVENT)
|
||||
if CleveRoids.playerClass == "PALADIN" and targetGuid then
|
||||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||||
if lib and lib.objects then
|
||||
local normalizedTarget = CleveRoids.NormalizeGUID(targetGuid)
|
||||
if normalizedTarget and lib.objects[normalizedTarget] then
|
||||
for spellID, rec in pairs(lib.objects[normalizedTarget]) do
|
||||
if lib.judgementSpells and lib.judgementSpells[spellID] and rec.start and rec.duration then
|
||||
local remaining = rec.duration + rec.start - GetTime()
|
||||
if remaining > 0 and rec.caster == "player" then
|
||||
rec.start = GetTime()
|
||||
|
||||
if CleveRoids.debug then
|
||||
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
|
||||
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on melee hit - new duration: %ds",
|
||||
baseName, spellID, rec.duration)
|
||||
)
|
||||
end
|
||||
|
||||
-- Sync to pfUI if loaded (pre-7.6 only)
|
||||
if pfUI and pfUI.api and pfUI.api.libdebuff then
|
||||
local spellName = C_Spell.GetSpellName(spellID) or nil
|
||||
local baseName = CleveRoids.StripRank(spellName)
|
||||
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
|
||||
local targetLevel = UnitLevel("target") or 0
|
||||
if targetName and baseName then
|
||||
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Process AUTO_ATTACK_SELF event (player being attacked)
|
||||
@@ -4847,8 +4884,7 @@ end
|
||||
-- ============================================================================
|
||||
|
||||
-- Maps CC type names to mechanic constants (matches DBC mechanic IDs)
|
||||
-- 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.
|
||||
-- Note: Some types map to multiple mechanics via CCMechanicGroups below
|
||||
CleveRoids.CCMechanics = {
|
||||
-- Movement/control impairment
|
||||
charm = 1, -- Mind Control, Seduction
|
||||
@@ -4857,30 +4893,31 @@ CleveRoids.CCMechanics = {
|
||||
disarm = 3, -- Disarm, Riposte disarm
|
||||
distract = 4, -- Distract (Rogue ability)
|
||||
fear = 5, -- Fear, Psychic Scream, Howl of Terror
|
||||
fumble = 6, -- DBC Fumble mechanic
|
||||
grip = 6, -- Legacy alias for fumble
|
||||
grip = 6, -- Grip effects
|
||||
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, -- Stun (Cheap Shot, Kidney Shot, Hammer of Justice)
|
||||
freeze = 13, -- Freeze effects
|
||||
knockout = 14, -- Knockout (Gouge, Repentance)
|
||||
stun = 12, -- Consolidated: Stun(12) + Knockout(14) + Sap(30)
|
||||
freeze = 13, -- Freeze effects (Frost Nova freeze)
|
||||
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
|
||||
sap = 30, -- Sap/Sapped mechanic
|
||||
}
|
||||
|
||||
-- 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)
|
||||
}
|
||||
|
||||
-- CC types that count as "crowd controlled" (loss of control)
|
||||
-- Individual DBC mechanics remain distinct; this table only powers [cc]/[cc:any].
|
||||
-- Note: Mechanics 12, 14, 30 are all consolidated under "stun" for conditionals
|
||||
CleveRoids.CCTypesLossOfControl = {
|
||||
[1] = true, -- charm
|
||||
[2] = true, -- disoriented
|
||||
@@ -4889,12 +4926,12 @@ CleveRoids.CCTypesLossOfControl = {
|
||||
[10] = true, -- sleep
|
||||
[12] = true, -- stun (Cheap Shot, Kidney Shot, etc.)
|
||||
[13] = true, -- freeze
|
||||
[14] = true, -- knockout/gouge
|
||||
[14] = true, -- knockout/gouge (now part of stun group)
|
||||
[17] = true, -- polymorph
|
||||
[18] = true, -- banish
|
||||
[20] = true, -- shackle
|
||||
[24] = true, -- horror
|
||||
[30] = true, -- sap
|
||||
[30] = true, -- sap (now part of stun group)
|
||||
}
|
||||
|
||||
-- Check if BuffLib is available with full mechanic support
|
||||
@@ -4961,6 +4998,19 @@ 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
|
||||
|
||||
@@ -5228,154 +5278,32 @@ 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] — 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: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 = function(conditionals)
|
||||
if type(conditionals.button) ~= "table" then
|
||||
return CleveRoids.WasClickActivated()
|
||||
return CleveRoids.AnyMouseButtonDown()
|
||||
end
|
||||
local invoking = CleveRoids.GetActivatingButton()
|
||||
return Multi(conditionals.button, function(button)
|
||||
return CleveRoids.buttons[button] == invoking
|
||||
local name = CleveRoids.buttons[button]
|
||||
return name and IsMouseButtonDown(name) or false
|
||||
end, conditionals, "button")
|
||||
end,
|
||||
|
||||
nobutton = function(conditionals)
|
||||
if type(conditionals.nobutton) ~= "table" then
|
||||
return not CleveRoids.WasClickActivated()
|
||||
return not CleveRoids.AnyMouseButtonDown()
|
||||
end
|
||||
local invoking = CleveRoids.GetActivatingButton()
|
||||
return NegatedMulti(conditionals.nobutton, function(button)
|
||||
return CleveRoids.buttons[button] ~= invoking
|
||||
local name = CleveRoids.buttons[button]
|
||||
return not (name and IsMouseButtonDown(name))
|
||||
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,
|
||||
@@ -9309,7 +9237,6 @@ 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,
|
||||
|
||||
@@ -198,16 +198,10 @@ requirementCheckFrame:SetScript("OnEvent", function()
|
||||
local hasNampower30 = hasNampower and CleveRoids.NampowerAPI
|
||||
and CleveRoids.NampowerAPI.HasMinimumVersion(3, 0, 0)
|
||||
local hasClassicAPI = CleveRoids.ClassicAPI and CleveRoids.ClassicAPI.IsAvailable()
|
||||
-- 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".
|
||||
-- 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)
|
||||
-- v1.12.1 added the positional C_UnitAuras.UnitAura the dispel conditionals use.
|
||||
local hasClassicAPI1121 = hasClassicAPI and CleveRoids.ClassicAPI.HasMinimumVersion(1, 12, 1)
|
||||
|
||||
if not hasNampower30 or not hasUnitXP or not hasClassicAPI or not hasClassicAPI1158 then
|
||||
if not hasNampower30 or not hasUnitXP or not hasClassicAPI or not hasClassicAPI1121 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:")
|
||||
@@ -227,11 +221,10 @@ 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 hasClassicAPI1158 then
|
||||
local major, minor, patch = CleveRoids.ClassicAPI.GetVersion()
|
||||
CleveRoids.Print(format("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI v1.15.8+|r is required (you have v%d.%d.%d):", major, minor, patch))
|
||||
elseif not hasClassicAPI1121 then
|
||||
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI v1.12.1+|r is required:")
|
||||
CleveRoids.Print("https://github.com/brues-code/ClassicAPI")
|
||||
CleveRoids.Print("The addon cannot finish loading on this version -- update ClassicAPI.")
|
||||
CleveRoids.Print("Dispel-type conditionals will be unavailable with this older version.")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -598,10 +591,6 @@ frame:SetScript("OnEvent", function()
|
||||
if type(CleveRoidMacros.macrocheck) ~= "number" then
|
||||
CleveRoidMacros.macrocheck = 1 -- enabled by default
|
||||
end
|
||||
|
||||
-- The saved realtime setting is only readable now; the unit streams were
|
||||
-- registered at load assuming event-driven mode.
|
||||
CleveRoids.ApplyUnitStreamEvents()
|
||||
end)
|
||||
|
||||
-- Queues a full update of all action bars.
|
||||
@@ -1146,7 +1135,7 @@ local publishedDisplay = {}
|
||||
-- nothing matched" and shows the question mark. Skipping the call instead would hand
|
||||
-- the macro back to ClassicAPI's own #showtooltip parser.
|
||||
function CleveRoids.PublishDisplay(actions)
|
||||
if not CleveRoids.ClassicAPIMacroDisplay then return end
|
||||
if not CleveRoids.useClassicAPIDisplay then return end
|
||||
local macroID = actions and actions.macroID
|
||||
if not macroID then return end -- SuperMacro macros have no Blizzard index
|
||||
|
||||
@@ -1161,7 +1150,7 @@ end
|
||||
-- ClassicAPI re-evaluates nothing for us. Covers macros that aren't on a bar too,
|
||||
-- which is what keeps the macro window grid's icons correct.
|
||||
function CleveRoids.PublishAllDisplays()
|
||||
if not CleveRoids.ClassicAPIMacroDisplay then return end
|
||||
if not CleveRoids.useClassicAPIDisplay then return end
|
||||
-- Forget what we published so every macro republishes once. Callers reach here
|
||||
-- after login and after a re-parse, where a cached value could otherwise
|
||||
-- suppress the publish a freshly rebuilt macro still needs.
|
||||
@@ -1176,16 +1165,13 @@ end
|
||||
|
||||
-- Hand every macro back to ClassicAPI's own parser and stop claiming ownership.
|
||||
function CleveRoids.ReleaseDisplays()
|
||||
if not CleveRoids.ClassicAPIMacroDisplay then return end
|
||||
-- Sweep the whole index space rather than the macros we know we published:
|
||||
-- PublishAllDisplays clears that record on every re-parse, so a macro claimed
|
||||
-- before one and gone after it would keep our last value forever. Releasing a
|
||||
-- slot we never claimed costs nothing, and this runs once.
|
||||
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
|
||||
if not CleveRoids.useClassicAPIDisplay then return end
|
||||
for i = 1, 36 do
|
||||
C_Macro.SetMacroDisplay(i, nil)
|
||||
end
|
||||
publishedDisplay = {}
|
||||
CleveRoids.ClassicAPIMacroDisplay = false
|
||||
CleveRoids.useClassicAPIDisplay = false
|
||||
end
|
||||
|
||||
-- PERFORMANCE: Static buffer references for hot path
|
||||
@@ -1231,10 +1217,17 @@ function CleveRoids.TestForAllActiveActions()
|
||||
local slots = actionsToSlots[actions]
|
||||
local stateChanged = CleveRoids.TestForActiveAction(actions)
|
||||
if stateChanged then
|
||||
-- Publishing repaints every slot holding this macro through the client's
|
||||
-- own notifier, so there is no per-slot fan-out to do here. It no-ops once
|
||||
-- ReleaseDisplays has handed the macros back.
|
||||
CleveRoids.PublishDisplay(actions)
|
||||
if CleveRoids.useClassicAPIDisplay then
|
||||
-- Publishing repaints every slot holding this macro through the
|
||||
-- client's own notifier, so the per-slot fan-out below is redundant.
|
||||
CleveRoids.PublishDisplay(actions)
|
||||
else
|
||||
-- Send event to ALL slots that use this macro
|
||||
local count = slots._count
|
||||
for j = 1, count do
|
||||
CleveRoids.SendEventForAction(slots[j], "ACTIONBAR_SLOT_CHANGED", slots[j])
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Clear for reuse (reset count and clear buffer reference)
|
||||
for j = 1, slots._count do
|
||||
@@ -4194,21 +4187,6 @@ 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.
|
||||
@@ -4279,6 +4257,20 @@ function CleveRoids.OnUpdate(self)
|
||||
if (time - CR.lastCleanupTime) >= CR.CLEANUP_INTERVAL then
|
||||
CR.lastCleanupTime = time
|
||||
|
||||
-- MEMORY: Clean up carnageDurationOverrides older than 30 seconds
|
||||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||||
local carnageOverrides = CR.carnageDurationOverrides
|
||||
if carnageOverrides then
|
||||
local spellID, data = next(carnageOverrides)
|
||||
while spellID do
|
||||
local nextID = next(carnageOverrides, spellID)
|
||||
if data.timestamp and (time - data.timestamp) > 30 then
|
||||
carnageOverrides[spellID] = nil
|
||||
end
|
||||
spellID, data = nextID, nextID and carnageOverrides[nextID]
|
||||
end
|
||||
end
|
||||
|
||||
-- MEMORY: Clean up old ComboPointTracking entries (older than 60 seconds)
|
||||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||||
local comboTracking = CR.ComboPointTracking
|
||||
@@ -4466,38 +4458,24 @@ 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].
|
||||
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
|
||||
-- -- the handlers ignore the unit and refresh every macro, because a conditional
|
||||
-- may name any unit ([@party3,hp:<50], @focus, @mouseover), so narrowing the
|
||||
-- token set would leave those icons stale.
|
||||
local unitStreamEvents
|
||||
-- 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
|
||||
-- Use GUID events when available (v2.39+), fall back to standard per-token events
|
||||
if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then
|
||||
unitStreamEvents = { "UNIT_AURA_GUID", "UNIT_HEALTH_GUID", "UNIT_MANA_GUID", "UNIT_RAGE_GUID", "UNIT_ENERGY_GUID" }
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_AURA_GUID")
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH_GUID")
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_MANA_GUID")
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_RAGE_GUID")
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_ENERGY_GUID")
|
||||
else
|
||||
unitStreamEvents = { "UNIT_AURA", "UNIT_HEALTH", "UNIT_POWER" }
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_AURA")
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH")
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_POWER")
|
||||
end
|
||||
|
||||
-- They do fire continuously for every unit in range, and in realtime mode their
|
||||
-- handlers do nothing at all: the OnUpdate refreshes on every throttled tick and
|
||||
-- QueueActionUpdate no-ops. Rather than pay a Lua dispatch per event to return
|
||||
-- early, drop the registrations entirely while realtime is on. Re-applied at
|
||||
-- VARIABLES_LOADED (when the saved value is first known) and whenever
|
||||
-- `/cleveroid realtime` flips it.
|
||||
function CleveRoids.ApplyUnitStreamEvents()
|
||||
local eventDriven = not CleveRoidMacros or CleveRoidMacros.realtime == 0
|
||||
for i = 1, table.getn(unitStreamEvents) do
|
||||
if eventDriven then
|
||||
CleveRoids.Frame:RegisterEvent(unitStreamEvents[i])
|
||||
else
|
||||
CleveRoids.Frame:UnregisterEvent(unitStreamEvents[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
CleveRoids.ApplyUnitStreamEvents()
|
||||
if CleveRoids.hasSuperwow then
|
||||
CleveRoids.Frame:RegisterEvent("UNIT_CASTEVENT")
|
||||
end
|
||||
@@ -4531,11 +4509,6 @@ 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
|
||||
|
||||
@@ -4636,11 +4609,58 @@ function CleveRoids.Frame:ADDON_LOADED(addon)
|
||||
end
|
||||
|
||||
function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time)
|
||||
-- Melee swings carry no spell to resolve. ClassicAPI mirrors the server's
|
||||
-- paladin judgement-refresh rule off SMSG_ATTACKERSTATEUPDATE in the DLL
|
||||
-- (src/aura/JudgementRefresh.cpp), so there is nothing to do here.
|
||||
-- Handle melee swings for judgement refresh
|
||||
if action == "MAINHAND" or action == "OFFHAND" then
|
||||
return
|
||||
-- Only process if this is the player's melee swing
|
||||
if caster == CleveRoids.playerGuid and CleveRoids.playerClass == "PALADIN" then
|
||||
-- Refresh judgements on the target
|
||||
-- Defensive: verify libdebuff is a table before accessing properties
|
||||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||||
if target and lib and lib.objects then
|
||||
local normalizedTarget = CleveRoids.NormalizeGUID(target)
|
||||
if normalizedTarget and lib.objects[normalizedTarget] then
|
||||
-- Refresh all active Judgements on the target
|
||||
for spellID, rec in pairs(lib.objects[normalizedTarget]) do
|
||||
-- Check if this is a judgement by spell ID
|
||||
if lib.judgementSpells and lib.judgementSpells[spellID] and rec.start and rec.duration then
|
||||
-- Only refresh if the Judgement is still active and was cast by player
|
||||
local remaining = rec.duration + rec.start - GetTime()
|
||||
if remaining > 0 and rec.caster == "player" then
|
||||
-- Refresh the Judgement by updating the start time
|
||||
rec.start = GetTime()
|
||||
|
||||
local spellName = C_Spell.GetSpellName(spellID)
|
||||
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
|
||||
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on %s hit - new duration: %ds",
|
||||
baseName, spellID, action, rec.duration)
|
||||
)
|
||||
end
|
||||
|
||||
-- Also sync to pfUI if it's loaded (pre-7.6 only)
|
||||
if pfUI and pfUI.api and pfUI.api.libdebuff then
|
||||
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
|
||||
local targetLevel = UnitLevel("target") or 0
|
||||
|
||||
if targetName then
|
||||
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player")
|
||||
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00ffaa[pfUI Judgement Refresh]|r Synced %s refresh to pfUI", baseName)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return -- Still return early after processing melee
|
||||
end
|
||||
|
||||
-- Debug channel tracking
|
||||
@@ -5034,6 +5054,26 @@ function CleveRoids.Frame:SPELLCAST_CHANNEL_STOP()
|
||||
CleveRoids.CurrentSpell.spellName = ""
|
||||
CleveRoids.CurrentSpell.castingSpellId = nil
|
||||
|
||||
-- WARLOCK DARK HARVEST: Mark channeling as ended
|
||||
-- Credits: Avitasia / Cursive addon
|
||||
if CleveRoids.darkHarvestData and CleveRoids.darkHarvestData.isActive then
|
||||
CleveRoids.darkHarvestData.isActive = false
|
||||
CleveRoids.darkHarvestData.endTime = GetTime()
|
||||
|
||||
-- Apply Dark Harvest end to all DoTs on target (finalizes reduction)
|
||||
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyDarkHarvestEnd then
|
||||
CleveRoids.libdebuff.ApplyDarkHarvestEnd(CleveRoids.darkHarvestData.targetGUID)
|
||||
end
|
||||
|
||||
if CleveRoids.debug then
|
||||
local activeTime = CleveRoids.darkHarvestData.endTime - CleveRoids.darkHarvestData.startTime
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff9482c9[Dark Harvest]|r Channel ended after %.1fs (DoT acceleration stopped)",
|
||||
activeTime)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- Force immediate action update
|
||||
CleveRoids.TestForAllActiveActions()
|
||||
end
|
||||
@@ -5587,14 +5627,6 @@ 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
|
||||
@@ -5776,8 +5808,6 @@ SlashCmdList["CLEVEROID"] = function(msg)
|
||||
local num = tonumber(val)
|
||||
if num == 0 or num == 1 then
|
||||
CleveRoidMacros.realtime = num
|
||||
-- The unit streams are only worth receiving in event-driven mode.
|
||||
CleveRoids.ApplyUnitStreamEvents()
|
||||
CleveRoids.Print("realtime set to " .. num)
|
||||
else
|
||||
CleveRoids.Print("Usage: /cleveroid realtime 0 or 1 - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load.)")
|
||||
|
||||
@@ -1180,7 +1180,7 @@ local function ReportAllMacroErrors()
|
||||
|
||||
-- Collect body (syntax) errors per macro. Macro names are no longer
|
||||
-- restricted (slot/index-based identification), so no name checks here.
|
||||
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
|
||||
for i = 1, 36 do
|
||||
local nameOk, name = pcall(GetMacroInfo, i)
|
||||
if nameOk and name and name ~= "" then
|
||||
local errors = {}
|
||||
|
||||
@@ -66,10 +66,6 @@ function CleveRoids.RegisterExtension(name)
|
||||
CleveRoids.RegisterEvent(name, eventName, callbackName)
|
||||
end
|
||||
|
||||
extension.RegisterUnitEvent = function(eventName, callbackName, ...)
|
||||
CleveRoids.RegisterUnitEvent(name, eventName, callbackName, unpack(arg))
|
||||
end
|
||||
|
||||
extension.Hook = function(functionName, callbackName, dontCallOriginal)
|
||||
CleveRoids.RegisterHook(name, functionName, callbackName, dontCallOriginal)
|
||||
end
|
||||
@@ -150,20 +146,6 @@ function CleveRoids.RegisterEvent(extensionName, eventName, callbackName)
|
||||
extension.internal.frame:RegisterEvent(eventName)
|
||||
end
|
||||
|
||||
-- Registers a callback for a UNIT_* event, filtered to the given unit tokens.
|
||||
-- The callback then only runs for those units: the client drops every other
|
||||
-- unit's copy, instead of all of them reaching Lua to be compared away. Use
|
||||
-- this over RegisterEvent whenever the handler starts by testing arg1.
|
||||
-- extensionName: The name of the extension trying to register the callback
|
||||
-- eventName: The UNIT_* event to register
|
||||
-- callbackName: The name of the callback that gets called when the event fires
|
||||
-- ...: the unit tokens to accept (e.g. "player", "target")
|
||||
function CleveRoids.RegisterUnitEvent(extensionName, eventName, callbackName, ...)
|
||||
local extension = CleveRoids.Extensions[extensionName]
|
||||
extension.internal.eventHandlers[eventName] = callbackName
|
||||
extension.internal.frame:RegisterUnitEvent(eventName, unpack(arg))
|
||||
end
|
||||
|
||||
-- Hooks the given function by it's name
|
||||
-- extensionName: The name of the extension trying to register the callback
|
||||
-- functionName: The name of the function that'll be hooked
|
||||
|
||||
@@ -19,15 +19,6 @@ CleveRoids.mouseOverResolvers = {}
|
||||
CleveRoids.mouseoverUnit = CleveRoids.mouseoverUnit or nil
|
||||
CleveRoids.mouseOverUnit = nil
|
||||
|
||||
-- Every macro slot the client can hold: 18 account-wide (1-18) followed by 18
|
||||
-- character-specific (19-36). This is the index space GetMacroInfo and
|
||||
-- C_Macro.SetMacroDisplay address, and it is fixed -- GetNumMacros() returns how
|
||||
-- many of each tab are *used*, which cannot be summed into a range, because the
|
||||
-- character block starts at 19 no matter how few account macros exist. Blizzard's
|
||||
-- own MAX_MACROS is no help either: it lives in the load-on-demand Blizzard_MacroUI
|
||||
-- and is nil until the player opens the macro window.
|
||||
CleveRoids.MAX_MACRO_SLOTS = 36
|
||||
|
||||
-- Environment flags
|
||||
CleveRoids.hasSuperwow = SetAutoloot and true or false
|
||||
CleveRoids.hasTurtle = (type(_G.TURTLE_WOW_VERSION) ~= "nil")
|
||||
@@ -38,18 +29,14 @@ CleveRoids.supported = CleveRoids.hasTurtle
|
||||
-- tooltip, cooldown sweep, range and usable state all come from the client -- including
|
||||
-- the drag cursor and the macro window grid, which Lua cannot reach.
|
||||
--
|
||||
-- ClassicAPI stands down from macro display entirely when it sees this addon loaded;
|
||||
-- ClassicAPIMacroDisplay is what tells it we drive it instead, and ReleaseDisplays
|
||||
-- clears it to hand every macro back. A fork that leaves the flag unset keeps the old
|
||||
-- behavior -- both must never drive the same buttons. It doubles as the internal
|
||||
-- "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, 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 =
|
||||
-- Feature-detect rather than version-check: the API is unreleased, so
|
||||
-- CLASSIC_API_VERSION reports the dev sentinel. ClassicAPI stands down from macro
|
||||
-- display entirely when it sees this addon loaded; ClassicAPIMacroDisplay is what
|
||||
-- tells it we drive it instead. A fork that leaves the flag unset keeps the old
|
||||
-- behavior -- both must never drive the same buttons.
|
||||
CleveRoids.useClassicAPIDisplay =
|
||||
(type(C_Macro) == "table" and C_Macro.SetMacroDisplay ~= nil) and true or false
|
||||
CleveRoids.ClassicAPIMacroDisplay = CleveRoids.useClassicAPIDisplay
|
||||
|
||||
CleveRoids.ParsedMsg = {}
|
||||
CleveRoids.ExpandedGroups = {}
|
||||
|
||||
+11
-1
@@ -1096,7 +1096,17 @@ function CleveRoids.ValidateAllMacros()
|
||||
local results = {}
|
||||
local totalErrors = 0
|
||||
|
||||
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
|
||||
-- Account-wide macros are indexed from 1 up to GetNumMacros().
|
||||
-- Character-specific macros occupy the slots immediately following the account-wide ones.
|
||||
-- In Classic clients, the macro UI has 18 General (Account) slots and 18 Character-Specific slots.
|
||||
local numAccountMacros = GetNumMacros()
|
||||
|
||||
-- The WoW API GetMacroInfo(index) supports indexing up to 36 (1-18 for General, 19-36 for Character)
|
||||
-- in Classic clients, even though the total is GetNumMacros() + GetNumCharacterMacros() in Retail.
|
||||
-- To ensure we check all 36 possible slots:
|
||||
local totalSlots = 36
|
||||
|
||||
for i = 1, totalSlots do
|
||||
local nameSuccess, name = pcall(GetMacroInfo, i)
|
||||
|
||||
-- Check if GetMacroInfo returned a name (i.e., the slot is used)
|
||||
|
||||
@@ -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.8+) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed, unit-filtered events |
|
||||
| [ClassicAPI](https://github.com/brues-code/ClassicAPI/releases) | ✅ | Modern `C_*` API: dispel-type conditionals (`[magic]`, `[curse]`, …), `[moving]` speed |
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
+1203
-311
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
# 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