mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
@@ -124,7 +124,7 @@ function Extension.HookPfUILibdebuff()
|
||||
|
||||
-- Check if pfUI 7.6+ with GetUnitField-based libdebuff is active
|
||||
-- If so, pfUI handles combo durations and Carnage internally - we only override on mismatch
|
||||
local hasPfUI76 = CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil)
|
||||
local hasPfUI76 = CleveRoids.hasPfUI76
|
||||
|
||||
-- pfUI 7.6+ handles all durations internally - no hooks needed
|
||||
if hasPfUI76 then
|
||||
@@ -351,7 +351,7 @@ function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
|
||||
end
|
||||
|
||||
-- pfUI 7.6+ handles all durations internally
|
||||
if CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil) then
|
||||
if CleveRoids.hasPfUI76 then
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
+380
-25
@@ -149,6 +149,33 @@ function CleveRoids.InvalidateSpellNameCache()
|
||||
_spellNameToIDsBuilt = false
|
||||
end
|
||||
|
||||
-- Check if a debuff is shared by spell ID or by name (for custom/Turtle WoW IDs).
|
||||
-- Falls back to checking if the debuff NAME matches any known shared debuff name
|
||||
-- in the spell name cache, even if the specific spell ID is unregistered.
|
||||
local function IsSharedDebuffByIdOrName(lib, spellID, debuffName)
|
||||
if not lib then return false end
|
||||
-- Direct ID check (fast path)
|
||||
if lib.IsPersonalDebuff and lib:IsPersonalDebuff(spellID) == false then
|
||||
return true
|
||||
end
|
||||
-- Name-based fallback: if ANY spell ID for this name is in sharedDebuffs, treat as shared.
|
||||
-- This handles custom server spell IDs (e.g., Turtle WoW Judgement variants) that have
|
||||
-- the same debuff name but different IDs from the hardcoded vanilla entries.
|
||||
if debuffName then
|
||||
local knownIDs = GetSpellIDsForName(debuffName)
|
||||
if knownIDs then
|
||||
for _, kid in ipairs(knownIDs) do
|
||||
if lib.sharedDebuffs and lib.sharedDebuffs[kid] then
|
||||
-- Also register this new spell ID so future checks are fast
|
||||
lib.sharedDebuffs[spellID] = lib.sharedDebuffs[kid]
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call)
|
||||
-- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache()
|
||||
-- Enhanced with Nampower v2.18+ GetEquippedItems when available
|
||||
@@ -849,6 +876,14 @@ CleveRoids.AuraCapStatus = {
|
||||
lastCleanupTime = 0,
|
||||
}
|
||||
|
||||
-- Overflow buff tracking: buffs applied while buff-capped (v2.34+)
|
||||
-- GetUnitField "aura" returns 48 slots: [1-32] = buff slots, [33-48] = debuff slots.
|
||||
-- Beyond these 48 client slots, the server can hold additional overflow buffs with
|
||||
-- no aura slot at all -- invisible to GetPlayerBuff and GetPlayerAuraDuration.
|
||||
-- Populated from AURA_CAST_ON_SELF when auraCapStatus indicates buff bar full.
|
||||
-- Format: [spellId] = { timestamp = GetTime(), durationSec = durationMs/1000 }
|
||||
CleveRoids.OverflowBuffs = {}
|
||||
|
||||
-- All-caster aura duration tracking (from AURA_CAST events)
|
||||
-- Tracks buff/debuff durations from ANY caster, not just player
|
||||
-- Structure: [targetGuid][spellId] = { start, duration, casterGuid }
|
||||
@@ -994,10 +1029,98 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur
|
||||
local now = GetTime()
|
||||
CleveRoids.AuraCapStatus.playerLastUpdate = now
|
||||
|
||||
local buffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
|
||||
|
||||
-- Check buff bar full (bit 1)
|
||||
CleveRoids.AuraCapStatus.playerBuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
|
||||
CleveRoids.AuraCapStatus.playerBuffCapped = buffCapped
|
||||
-- Check debuff bar full (bit 2)
|
||||
CleveRoids.AuraCapStatus.playerDebuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL)
|
||||
|
||||
-- Determine if this aura is a buff (not debuff) — debuffs land in GetPlayerAuraDuration
|
||||
-- slots 32-47, so checking those slots identifies them regardless of cap status.
|
||||
local isBuffNotDebuff = false
|
||||
if spellId and spellId > 0 and effectAuraName and effectAuraName > 0 then
|
||||
local isDebuff = false
|
||||
if _G.GetPlayerAuraDuration then
|
||||
for slot = 32, 47 do
|
||||
local sid = _G.GetPlayerAuraDuration(slot)
|
||||
if sid and sid == spellId then
|
||||
isDebuff = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
isBuffNotDebuff = not isDebuff
|
||||
|
||||
-- Track overflow buffs: if buff bar is full, this buff has no client aura slot.
|
||||
-- Store spellId + duration so /cancelaura and [mybuff] can find it.
|
||||
if buffCapped and isBuffNotDebuff then
|
||||
local entry = CleveRoids.OverflowBuffs[spellId]
|
||||
if not entry then
|
||||
entry = {}
|
||||
CleveRoids.OverflowBuffs[spellId] = entry
|
||||
end
|
||||
entry.timestamp = now
|
||||
entry.durationSec = durationMs and (durationMs / 1000) or 0
|
||||
end
|
||||
end
|
||||
|
||||
if not buffCapped and next(CleveRoids.OverflowBuffs) then
|
||||
-- No longer buff-capped: some overflow buffs may have gotten real slots.
|
||||
-- Only remove entries that now appear in a visible aura slot.
|
||||
-- The server does NOT auto-migrate overflow buffs into freed slots,
|
||||
-- so we can't blindly clear everything.
|
||||
if _G.GetPlayerAuraDuration then
|
||||
local visibleSpells = {}
|
||||
for slot = 0, 31 do
|
||||
local sid = _G.GetPlayerAuraDuration(slot)
|
||||
if sid and sid > 0 then
|
||||
visibleSpells[sid] = true
|
||||
end
|
||||
end
|
||||
for k in pairs(CleveRoids.OverflowBuffs) do
|
||||
if visibleSpells[k] then
|
||||
CleveRoids.OverflowBuffs[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Also prune expired entries
|
||||
for k, entry in pairs(CleveRoids.OverflowBuffs) do
|
||||
if entry.durationSec and entry.durationSec > 0 then
|
||||
local elapsed = now - (entry.timestamp or 0)
|
||||
if elapsed > entry.durationSec then
|
||||
CleveRoids.OverflowBuffs[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- NEW: Populate ownBuffCasts and allBuffAuras for all player buffs (not just overflow)
|
||||
-- Use the isBuffNotDebuff result determined above (avoids redundant slot scanning)
|
||||
local lib = CleveRoids.libdebuff
|
||||
if isBuffNotDebuff and spellId and durationMs and durationMs > 0 and lib and not lib.hasPfUIEnhanced then
|
||||
local spellName = SpellInfo and SpellInfo(spellId)
|
||||
if spellName then
|
||||
local _, playerGuidRaw = UnitExists("player")
|
||||
local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw)
|
||||
if playerGuid then
|
||||
lib.ownBuffCasts[playerGuid] = lib.ownBuffCasts[playerGuid] or {}
|
||||
lib.ownBuffCasts[playerGuid][spellName] = {
|
||||
startTime = now,
|
||||
duration = durationMs / 1000,
|
||||
spellId = spellId,
|
||||
casterGuid = casterGuid,
|
||||
}
|
||||
lib.allBuffAuras[playerGuid] = lib.allBuffAuras[playerGuid] or {}
|
||||
lib.allBuffAuras[playerGuid][spellName] = lib.allBuffAuras[playerGuid][spellName] or {}
|
||||
lib.allBuffAuras[playerGuid][spellName][casterGuid or "unknown"] = {
|
||||
startTime = now,
|
||||
duration = durationMs / 1000,
|
||||
rank = 0,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAuraName,
|
||||
@@ -1033,6 +1156,25 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
|
||||
spellName, spellId, string.sub(tostring(targetGuid), 1, 16), durationMs / 1000
|
||||
))
|
||||
end
|
||||
|
||||
-- NEW: Store in pendingBuffCasts for BUFF_ADDED_OTHER to confirm as buff
|
||||
-- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff)
|
||||
local lib = CleveRoids.libdebuff
|
||||
if lib and not lib.hasPfUIEnhanced then
|
||||
local spellNameForPending = SpellInfo and SpellInfo(spellId)
|
||||
if spellNameForPending then
|
||||
local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid)
|
||||
if normTargetGuid then
|
||||
lib.pendingBuffCasts[normTargetGuid] = lib.pendingBuffCasts[normTargetGuid] or {}
|
||||
lib.pendingBuffCasts[normTargetGuid][spellId] = {
|
||||
casterGuid = CleveRoids.NormalizeGUID(casterGuid),
|
||||
duration = durationMs / 1000,
|
||||
spellName = spellNameForPending,
|
||||
time = now,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Store cap status for this target GUID (if available)
|
||||
@@ -1663,6 +1805,58 @@ end
|
||||
function CleveRoids.CancelAura(auraName)
|
||||
local ix = 0
|
||||
auraName = string.lower(string.gsub(auraName, "_"," "))
|
||||
|
||||
-- v2.34+ path: cancel by spell ID (works for buff-capped overflow auras too)
|
||||
local API = CleveRoids.NampowerAPI
|
||||
if API and API.features.hasCancelPlayerAuraSpellId and CleveRoids.hasSuperwow then
|
||||
-- First scan visible buffs via GetPlayerBuff (fast, covers normal case)
|
||||
while true do
|
||||
local aura_ix = GetPlayerBuff(ix, "HELPFUL")
|
||||
ix = ix + 1
|
||||
if aura_ix == -1 then break end
|
||||
local bid = GetPlayerBuffID(aura_ix)
|
||||
bid = (bid < -1) and (bid + 65536) or bid
|
||||
if string.lower(SpellInfo(bid)) == auraName then
|
||||
_G.CancelPlayerAuraSpellId(bid, 1)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Not found in visible buffs - scan all 32 raw aura slots
|
||||
-- GetPlayerAuraDuration reads unit data fields directly (same 32 slots but bypasses UI filtering)
|
||||
if API.features.hasGetPlayerAuraDuration and _G.GetPlayerAuraDuration then
|
||||
for slot = 0, 31 do
|
||||
local spellId = _G.GetPlayerAuraDuration(slot)
|
||||
if spellId and spellId > 0 then
|
||||
local name = SpellInfo(spellId)
|
||||
if name and string.lower(name) == auraName then
|
||||
_G.CancelPlayerAuraSpellId(spellId, 1)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Final fallback: check overflow buff tracking (buffs applied while buff-capped
|
||||
-- that have NO client aura slot - tracked via AURA_CAST_ON_SELF events)
|
||||
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
|
||||
-- Skip expired overflow entries
|
||||
local elapsed = GetTime() - (entry.timestamp or 0)
|
||||
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
|
||||
CleveRoids.OverflowBuffs[spellId] = nil
|
||||
else
|
||||
local name = SpellInfo(spellId)
|
||||
if name and string.lower(name) == auraName then
|
||||
_G.CancelPlayerAuraSpellId(spellId, 1)
|
||||
CleveRoids.OverflowBuffs[spellId] = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Legacy path for older Nampower versions
|
||||
while true do
|
||||
local aura_ix = GetPlayerBuff(ix,"HELPFUL")
|
||||
ix = ix + 1
|
||||
@@ -2451,11 +2645,16 @@ end
|
||||
--- Falls back to basic position checking if MonkeySpeed isn't available
|
||||
--- @return boolean True if player is moving
|
||||
function CleveRoids.IsPlayerMoving()
|
||||
-- If MonkeySpeed is available, use it for accurate detection
|
||||
-- If MonkeySpeed is available, use it for accurate speed-based detection
|
||||
if CleveRoids.HasMonkeySpeed() then
|
||||
return (MonkeySpeed.m_fSpeed or 0) > 0
|
||||
end
|
||||
|
||||
-- Nampower 2.36+ provides direct movement flag query (catches jumping, falling, pitching)
|
||||
if CleveRoids.NampowerAPI.features.hasPlayerIsMoving then
|
||||
return PlayerIsMoving() == 1
|
||||
end
|
||||
|
||||
-- Fallback: use continuously tracked position history from OnUpdate
|
||||
-- Core.lua tracks position at ~100 Hz with 4-sample circular buffer (matches MonkeySpeed's 0.01s).
|
||||
-- Movement is detected if ANY consecutive pair of samples shows movement.
|
||||
@@ -2465,25 +2664,18 @@ function CleveRoids.IsPlayerMoving()
|
||||
local headIdx = CleveRoids._posHistoryIndex or 0
|
||||
|
||||
if history and count >= 2 then
|
||||
-- Iterate consecutive pairs in circular buffer order (oldest to newest)
|
||||
-- Head is the most recent entry, walk backwards through the ring
|
||||
local samplesAvail = count
|
||||
for i = 1, samplesAvail - 1 do
|
||||
-- prevIdx and currIdx walk from oldest to newest
|
||||
local currOffset = samplesAvail - i -- e.g., 3,2,1,0 for count=4
|
||||
local prevOffset = samplesAvail - i + 1 -- e.g., 4,3,2,1
|
||||
local currIdx = math.mod((headIdx - currOffset - 1), 4) + 1
|
||||
local prevIdx = math.mod((headIdx - prevOffset - 1), 4) + 1
|
||||
local prev = history[prevIdx]
|
||||
local curr = history[currIdx]
|
||||
-- Compare the most recent pair: headIdx (newest) vs. the slot before it.
|
||||
-- Use (headIdx - 2 + 4) mod 4 to safely wrap backwards in the 1-based ring.
|
||||
-- This avoids Lua 5.0 math.mod returning negative values for negative inputs.
|
||||
local prevIdx = math.mod((headIdx - 2 + 4), 4) + 1
|
||||
local curr = history[headIdx]
|
||||
local prev = history[prevIdx]
|
||||
if curr and prev then
|
||||
local dx = curr.x - prev.x
|
||||
local dy = curr.y - prev.y
|
||||
local dist = math.sqrt(dx * dx + dy * dy)
|
||||
if dist > 0.001 then
|
||||
return true -- Any movement in recent history = moving
|
||||
end
|
||||
return math.sqrt(dx * dx + dy * dy) > 0.001
|
||||
end
|
||||
return false -- No movement in any recent sample = stopped
|
||||
return false
|
||||
end
|
||||
|
||||
-- Legacy fallback for code that might not have history yet
|
||||
@@ -3281,6 +3473,112 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
|
||||
end
|
||||
end
|
||||
|
||||
-- Overflow buff fallback: player buffs in server slots 33-48 (no client slot)
|
||||
-- Check AURA_CAST_ON_SELF tracked overflow data for presence + duration
|
||||
if not found and isPlayer and isbuff and next(CleveRoids.OverflowBuffs) then
|
||||
local now = GetTime()
|
||||
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
|
||||
-- Prune expired entries
|
||||
local elapsed = now - (entry.timestamp or 0)
|
||||
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
|
||||
CleveRoids.OverflowBuffs[spellId] = nil
|
||||
else
|
||||
local match = false
|
||||
if searchID then
|
||||
match = (spellId == searchID)
|
||||
elseif searchName then
|
||||
local lowerName = GetLowercaseSpellName(spellId)
|
||||
match = (lowerName and lowerName == searchName)
|
||||
end
|
||||
if match then
|
||||
found = true
|
||||
stacks = 0
|
||||
-- Compute remaining time from apply timestamp + duration
|
||||
if entry.durationSec and entry.durationSec > 0 then
|
||||
remaining = entry.durationSec - elapsed
|
||||
if remaining < 0 then remaining = 0 end
|
||||
else
|
||||
remaining = -1 -- Unknown duration (permanent buff)
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- allBuffAuras fallback for player buff timing: when slow path found the buff but
|
||||
-- returned no remaining time, check lib.allBuffAuras for cached AURA_CAST timing.
|
||||
if found and remaining == nil and isPlayer and isbuff and searchName then
|
||||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||||
if lib and lib.allBuffAuras then
|
||||
local _, playerGuidRaw = UnitExists("player")
|
||||
local playerGuid = CleveRoids.NormalizeGUID(playerGuidRaw)
|
||||
if playerGuid and lib.allBuffAuras[playerGuid] then
|
||||
-- Try exact name match first
|
||||
local casters = lib.allBuffAuras[playerGuid][args.name]
|
||||
-- Try lowercase match if exact didn't work
|
||||
if not casters then
|
||||
for bName, c in pairs(lib.allBuffAuras[playerGuid]) do
|
||||
if _string_lower(bName) == searchName then
|
||||
casters = c
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if casters then
|
||||
for _, cData in pairs(casters) do
|
||||
local elapsed = GetTime() - (cData.startTime or 0)
|
||||
remaining = cData.duration > 0 and (cData.duration - elapsed) or -1
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Non-player overflow fallback: buff exists in server slots 33-48 (no client slot)
|
||||
-- AllCasterAuraTracking already has data from AURA_CAST_ON_OTHER for all aura applications.
|
||||
-- If the normal scan didn't find the buff, check there for presence + duration.
|
||||
-- Guard: verify the spell isn't a visible debuff on the target (AllCasterAuraTracking
|
||||
-- stores both buffs and debuffs, so without this check [buff:DebuffName] could false-positive).
|
||||
if not found and not isPlayer and isbuff and (searchID or searchName) then
|
||||
local _, targetGuid = UnitExists(unit)
|
||||
if targetGuid then
|
||||
-- Check if the spell is in a visible debuff slot — if so, it's a debuff, not a buff
|
||||
local isDebuff = false
|
||||
local di = 1
|
||||
while true do
|
||||
local dtex, _, _, dspellId = UnitDebuff(unit, di)
|
||||
if not dtex then break end
|
||||
if dspellId then
|
||||
if searchID then
|
||||
if dspellId == searchID then
|
||||
isDebuff = true
|
||||
break
|
||||
end
|
||||
elseif searchName then
|
||||
local lowerName = GetLowercaseSpellName(dspellId)
|
||||
if lowerName and lowerName == searchName then
|
||||
isDebuff = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
di = di + 1
|
||||
end
|
||||
|
||||
if not isDebuff then
|
||||
local trackRemaining = CleveRoids.FindAllCasterAuraByName(targetGuid,
|
||||
searchID and tostring(searchID) or args.name)
|
||||
if trackRemaining then
|
||||
found = true
|
||||
stacks = 0
|
||||
remaining = trackRemaining
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ops = CleveRoids.operators
|
||||
local cmp = CleveRoids.comparators
|
||||
|
||||
@@ -3291,6 +3589,46 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
|
||||
-- (showing ~1000s instead of actual remaining time). Skip it for buff time checks
|
||||
-- and rely on all-caster tracking from AURA_CAST events instead.
|
||||
|
||||
-- Fast path: Check lib.allBuffAuras (spellName-indexed, O(1) lookup)
|
||||
-- More efficient than FindAllCasterAuraByName which does ID→name translation
|
||||
if nonPlayerAuraTimeRemaining == nil and isbuff then
|
||||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||||
if lib and lib.allBuffAuras then
|
||||
local _, targetGuid = UnitExists(unit)
|
||||
targetGuid = targetGuid and CleveRoids.NormalizeGUID(targetGuid)
|
||||
if targetGuid then
|
||||
local buffEntries = lib.allBuffAuras[targetGuid]
|
||||
if buffEntries then
|
||||
-- Try exact name match first
|
||||
local casters = buffEntries[args.name]
|
||||
-- Try lowercase match if exact didn't work
|
||||
if not casters and searchName then
|
||||
for bName, c in pairs(buffEntries) do
|
||||
if _string_lower(bName) == searchName then
|
||||
casters = c
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if casters then
|
||||
for _, cData in pairs(casters) do
|
||||
local elapsed = GetTime() - (cData.startTime or 0)
|
||||
local rem = cData.duration > 0 and (cData.duration - elapsed) or -1
|
||||
if rem == nil or rem > 0 or cData.duration <= 0 then
|
||||
nonPlayerAuraTimeRemaining = rem
|
||||
if not found then
|
||||
found = true
|
||||
stacks = 0
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Second try: All-caster tracking from AURA_CAST events (works for any caster)
|
||||
-- Only use if libdebuff didn't find it (libdebuff has more accurate timing for player casts)
|
||||
if nonPlayerAuraTimeRemaining == nil then
|
||||
@@ -3506,7 +3844,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
|
||||
if gufResult ~= nil then
|
||||
-- GetUnitField was available and searched
|
||||
if gufResult then
|
||||
local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(gufSpellId) == false
|
||||
local isShared = IsSharedDebuffByIdOrName(lib, gufSpellId, args.name)
|
||||
if isShared then
|
||||
found = true
|
||||
stacks = gufStacks or 0
|
||||
@@ -3540,7 +3878,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
|
||||
if matched then
|
||||
-- IMPORTANT: Only use fallback for SHARED debuffs
|
||||
-- Personal debuffs must come from tracking table (caster check)
|
||||
local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(debuffSpellID) == false
|
||||
-- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements)
|
||||
local isShared = IsSharedDebuffByIdOrName(lib, debuffSpellID, args.name)
|
||||
if isShared then
|
||||
found = true
|
||||
texture = tex
|
||||
@@ -3570,7 +3909,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
|
||||
end
|
||||
if matched then
|
||||
-- IMPORTANT: Only use fallback for SHARED debuffs
|
||||
local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false
|
||||
-- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements)
|
||||
local isShared = IsSharedDebuffByIdOrName(lib, buffSpellID, args.name)
|
||||
if isShared then
|
||||
found = true
|
||||
texture = tex
|
||||
@@ -4347,6 +4687,7 @@ CleveRoids.CCTypesLossOfControl = {
|
||||
[1] = true, -- charm
|
||||
[2] = true, -- disoriented
|
||||
[5] = true, -- fear
|
||||
[7] = true, -- root (Entangling Roots, Frost Nova, etc.)
|
||||
[10] = true, -- sleep
|
||||
[12] = true, -- stun (Cheap Shot, Kidney Shot, etc.)
|
||||
[13] = true, -- freeze
|
||||
@@ -6219,14 +6560,28 @@ CleveRoids.Keywords = {
|
||||
end, conditionals, "nopet")
|
||||
end,
|
||||
|
||||
-- [swimming] - Player is currently swimming (Nampower v2.36+)
|
||||
swimming = function(conditionals)
|
||||
-- Check if "Aquatic Form" is in the reactive list and usable
|
||||
return CleveRoids.IsReactiveUsable("Aquatic Form")
|
||||
if not CleveRoids.NampowerAPI.features.hasPlayerIsSwimming then
|
||||
if not CleveRoids._swimmingErrorShown then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swimming] conditional requires Nampower v2.36.0 or newer.", 1, 0.5, 0.5)
|
||||
CleveRoids._swimmingErrorShown = true
|
||||
end
|
||||
return false
|
||||
end
|
||||
return PlayerIsSwimming() == 1
|
||||
end,
|
||||
|
||||
-- [noswimming] - Player is NOT swimming (Nampower v2.36+)
|
||||
noswimming = function(conditionals)
|
||||
-- Check if "Aquatic Form" is NOT usable
|
||||
return not CleveRoids.IsReactiveUsable("Aquatic Form")
|
||||
if not CleveRoids.NampowerAPI.features.hasPlayerIsSwimming then
|
||||
if not CleveRoids._swimmingErrorShown then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swimming] conditional requires Nampower v2.36.0 or newer.", 1, 0.5, 0.5)
|
||||
CleveRoids._swimmingErrorShown = true
|
||||
end
|
||||
return false
|
||||
end
|
||||
return PlayerIsSwimming() ~= 1
|
||||
end,
|
||||
|
||||
distance = function(conditionals)
|
||||
|
||||
@@ -490,6 +490,10 @@ frame:SetScript("OnEvent", function()
|
||||
if type(CleveRoidMacros.refresh) ~= "number" then
|
||||
CleveRoidMacros.refresh = 5
|
||||
end
|
||||
|
||||
if type(CleveRoidMacros.macrocheck) ~= "number" then
|
||||
CleveRoidMacros.macrocheck = 1 -- enabled by default
|
||||
end
|
||||
end)
|
||||
|
||||
-- Queues a full update of all action bars.
|
||||
@@ -805,10 +809,10 @@ function CleveRoids.TestForActiveAction(actions)
|
||||
if action.sequence then
|
||||
newSequence = action.sequence
|
||||
newActiveAction = CleveRoids.GetCurrentSequenceAction(newSequence)
|
||||
-- Check if current step is null/pass/noop - if so, skip this sequence for icon
|
||||
-- Check if current step is a stop step - if so, skip this sequence for icon
|
||||
if newActiveAction and newActiveAction.action then
|
||||
local stepLower = string.lower(CleveRoids.Trim(newActiveAction.action))
|
||||
if stepLower == "null" or stepLower == "pass" or stepLower == "noop" then
|
||||
if stepLower == "nil" or stepLower == "null" or stepLower == "pass" or stepLower == "noop" then
|
||||
hasActive = false
|
||||
newActiveAction = nil
|
||||
newSequence = nil
|
||||
@@ -1877,17 +1881,11 @@ function CleveRoids.ParseMsg(msg)
|
||||
|
||||
-- With a condition block present, build out the conditionals table
|
||||
|
||||
-- optional spam/cancel flags (apply only when we actually have a [] block)
|
||||
-- ! prefix with explicit conditionals: store as a flag for execution-time anti-toggle,
|
||||
-- rather than injecting an implicit conditional that gets AND'd with user conditions.
|
||||
-- The anti-toggle check happens in DoWithConditionals before CastSpellByName.
|
||||
if noSpam and noSpam ~= "" then
|
||||
local spamCond = CleveRoids.GetSpammableConditional(action)
|
||||
if spamCond then
|
||||
conditionals[spamCond] = { action }
|
||||
-- Also create _groups entry so Multi() finds it when combined with explicit conditionals
|
||||
if not conditionals._groups then
|
||||
conditionals._groups = {}
|
||||
end
|
||||
conditionals._groups[spamCond] = { { values = { action }, operator = "OR" } }
|
||||
end
|
||||
conditionals.noSpam = true
|
||||
end
|
||||
if cancelAura and cancelAura ~= "" then
|
||||
conditionals.cancelaura = action
|
||||
@@ -2372,7 +2370,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
|
||||
conditionals.target = origTarget
|
||||
return false
|
||||
end
|
||||
-- Set target to the found GUID for soft-casting via SuperWoW
|
||||
-- Set target to the found GUID for soft-casting via SuperWoW or Nampower v2.37+
|
||||
conditionals.target = scanResult
|
||||
-- Don't need to retarget since we're using GUID directly
|
||||
needRetarget = false
|
||||
@@ -2404,7 +2402,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
|
||||
end
|
||||
end
|
||||
|
||||
if conditionals.target ~= nil and targetBeforeAction and not (CleveRoids.hasSuperwow and action == CastSpellByName) then
|
||||
if conditionals.target ~= nil and targetBeforeAction and not (action == CastSpellByName and (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken)) then
|
||||
if not UnitIsUnit("target", conditionals.target) then
|
||||
if SpellIsTargeting() then
|
||||
SpellStopCasting()
|
||||
@@ -2477,11 +2475,33 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
|
||||
end
|
||||
end
|
||||
if action == CastSpellByName then
|
||||
-- Special case: !Attack should use AttackTarget() which doesn't toggle
|
||||
-- This is more reliable than CastSpellByName("Attack") which toggles on/off
|
||||
-- ! prefix anti-toggle: prevent CastSpellByName from toggling off active spells.
|
||||
-- For Attack/AutoShot/Shoot, use the non-toggling AttackTarget() API.
|
||||
-- For self-buffs/shapeshifts, skip if already active on the player.
|
||||
if conditionals.noSpam then
|
||||
local spamType = CleveRoids.spamConditions[msg]
|
||||
if spamType == "checkchanneled" then
|
||||
-- Attack/AutoShot/Shoot: use non-toggling API
|
||||
if msg == CleveRoids.Localized.Attack then
|
||||
AttackTarget()
|
||||
if needRetarget then TargetLastTarget() end
|
||||
conditionals.target = origTarget
|
||||
return true
|
||||
end
|
||||
-- AutoShot/Shoot: checkchanneled prevents toggle (already handled below)
|
||||
else
|
||||
-- Self-buff/shapeshift: skip if already active on player
|
||||
if CleveRoids.ValidatePlayerBuff(msg) then
|
||||
if needRetarget then TargetLastTarget() end
|
||||
conditionals.target = origTarget
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Legacy path: !Attack without explicit conditionals (checkchanneled injected)
|
||||
if msg == CleveRoids.Localized.Attack and conditionals.checkchanneled then
|
||||
AttackTarget()
|
||||
elseif CleveRoids.hasSuperwow and conditionals.target then
|
||||
elseif (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken) and conditionals.target then
|
||||
-- Let Nampower DLL handle queuing natively via its CastSpellByName hook
|
||||
CastSpellByName(castMsg, conditionals.target)
|
||||
else
|
||||
@@ -2581,6 +2601,23 @@ function CleveRoids.DoTarget(msg)
|
||||
-- Save original target GUID for potential restoration (SuperWoW returns GUID as 2nd value)
|
||||
local _, originalTargetGuid = UnitExists("target")
|
||||
|
||||
-- Handle [multiscan:priority] - use ResolveMultiscanTarget for enemy scanning
|
||||
-- ResolveMultiscanTarget handles its own target save/restore internally
|
||||
if conditionals.multiscan then
|
||||
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, conditionals.target)
|
||||
if scanResult then
|
||||
TargetUnit(scanResult)
|
||||
return true
|
||||
end
|
||||
-- No match found - restore original target if scanning changed it
|
||||
if originalTargetGuid and UnitExists(originalTargetGuid) then
|
||||
TargetUnit(originalTargetGuid)
|
||||
elseif not originalTargetGuid then
|
||||
ClearTarget()
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Track if an explicit target was specified via @unit syntax
|
||||
local explicitTarget = conditionals.target
|
||||
|
||||
@@ -3181,16 +3218,15 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
|
||||
end
|
||||
end
|
||||
|
||||
-- Item not found via v2.18 lookup
|
||||
-- Item not found via v2.18 lookup - fall through to legacy path
|
||||
if CleveRoids.equipDebugLog then
|
||||
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot|r")
|
||||
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot, trying legacy fallback|r")
|
||||
-- Debug: Try direct FindPlayerItemSlot call
|
||||
if FindPlayerItemSlot then
|
||||
local bag, slot = FindPlayerItemSlot(msg)
|
||||
CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r")
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Fallback for older Nampower versions
|
||||
@@ -3294,13 +3330,25 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
|
||||
if EquipItemByName then
|
||||
local ok = pcall(EquipItemByName, msg, invslot)
|
||||
if ok then
|
||||
-- Invalidate cache entry if it exists
|
||||
if CleveRoids.Items then
|
||||
CleveRoids.Items[msg] = nil
|
||||
CleveRoids.Items[string_lower(msg)] = nil
|
||||
-- Verify the item actually landed in the target slot
|
||||
-- EquipItemByName may silently no-op for same-named items in paired slots
|
||||
-- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot)
|
||||
local newLink = GetInventoryItemLink("player", invslot)
|
||||
if newLink then
|
||||
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
|
||||
if newName and string_lower(newName) == string_lower(msg) then
|
||||
if CleveRoids.Items then
|
||||
CleveRoids.Items[msg] = nil
|
||||
CleveRoids.Items[string_lower(msg)] = nil
|
||||
end
|
||||
InvalidateDisplacedItem()
|
||||
return true
|
||||
end
|
||||
end
|
||||
-- Verification failed - EquipItemByName didn't place item in target slot
|
||||
if CleveRoids.equipDebugLog then
|
||||
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. msg .. "', " .. invslot .. ") succeeded but item not in target slot, trying fallback|r")
|
||||
end
|
||||
InvalidateDisplacedItem()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3336,12 +3384,23 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
|
||||
if item.name and EquipItemByName and item.name ~= msg then
|
||||
local ok = pcall(EquipItemByName, item.name, invslot)
|
||||
if ok then
|
||||
if CleveRoids.Items then
|
||||
CleveRoids.Items[item.name] = nil
|
||||
CleveRoids.Items[string_lower(item.name)] = nil
|
||||
-- Verify the item actually landed in the target slot (same guard as above)
|
||||
local newLink = GetInventoryItemLink("player", invslot)
|
||||
if newLink then
|
||||
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
|
||||
if newName and string_lower(newName) == string_lower(item.name) then
|
||||
if CleveRoids.Items then
|
||||
CleveRoids.Items[item.name] = nil
|
||||
CleveRoids.Items[string_lower(item.name)] = nil
|
||||
end
|
||||
InvalidateDisplacedItem()
|
||||
return true
|
||||
end
|
||||
end
|
||||
-- Verification failed - fall through to manual cursor-based equip
|
||||
if CleveRoids.equipDebugLog then
|
||||
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. item.name .. "', " .. invslot .. ") succeeded but item not in target slot, trying manual fallback|r")
|
||||
end
|
||||
InvalidateDisplacedItem()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3634,15 +3693,15 @@ function CleveRoids.DoCastSequence(sequence)
|
||||
))
|
||||
end
|
||||
|
||||
-- Check for "null" step - does nothing, allows macro to fall through
|
||||
-- Usage: /castsequence reset=target Sunder Armor, null
|
||||
-- Check for stop steps - does nothing, allows macro to fall through
|
||||
-- Sequence stays on this step until a reset condition fires (target/combat/time/modifier)
|
||||
-- Usage: /castsequence reset=target Sunder Armor, nil
|
||||
local actionLower = string.lower(CleveRoids.Trim(active.action))
|
||||
if actionLower == "null" or actionLower == "pass" or actionLower == "noop" then
|
||||
-- Update sequence state (so per-target tracking works)
|
||||
if actionLower == "nil" or actionLower == "null" or actionLower == "pass" or actionLower == "noop" then
|
||||
sequence.status = 0
|
||||
sequence.lastUpdate = GetTime()
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit null step, falling through")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit stop step, falling through")
|
||||
end
|
||||
-- Don't cast anything, just return - allows macro to continue to next line
|
||||
return
|
||||
@@ -3840,7 +3899,7 @@ function CleveRoids.OnUpdate(self)
|
||||
|
||||
-- Maintain legacy vars for any code that uses them directly
|
||||
if CleveRoids._posHistoryCount >= 2 then
|
||||
local prevIdx = math.mod((idx - 2), 4) + 1
|
||||
local prevIdx = math.mod((idx - 2 + 4), 4) + 1
|
||||
CleveRoids._previousPlayerPos = history[prevIdx]
|
||||
CleveRoids._currentPlayerPos = entry
|
||||
end
|
||||
@@ -5149,6 +5208,18 @@ function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
|
||||
end
|
||||
end
|
||||
|
||||
-- v2.38+: Override channelDuration with the accurate server-provided value stored by
|
||||
-- SPELL_START_SELF (which fires before this event). This beats the tooltip scan or
|
||||
-- UNIT_CASTEVENT cast_time fallback, because the server already applied all modifiers.
|
||||
if CleveRoids._v238ChannelDuration then
|
||||
if not spellId or not CleveRoids._v238ChannelSpellId
|
||||
or CleveRoids._v238ChannelSpellId == spellId then
|
||||
CleveRoids.channelDuration = CleveRoids._v238ChannelDuration
|
||||
end
|
||||
CleveRoids._v238ChannelDuration = nil
|
||||
CleveRoids._v238ChannelSpellId = nil
|
||||
end
|
||||
|
||||
-- Force immediate action update
|
||||
CleveRoids.TestForAllActiveActions()
|
||||
end
|
||||
@@ -5785,11 +5856,13 @@ SlashCmdList["CLEVEROID"] = function(msg)
|
||||
CleveRoids.Print("Current Settings:")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("realtime (force fast updates, CPU intensive) = " .. CleveRoidMacros.realtime .. " (Default: 0)")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("refresh (updates per second) = " .. CleveRoidMacros.refresh .. " (Default: 5)")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("macrocheck (syntax checker) = " .. CleveRoidMacros.macrocheck .. " (Default: 1)")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("debug (show learning messages) = " .. (CleveRoids.debug and "1" or "0") .. " (Default: 0)")
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" ")
|
||||
CleveRoids.Print("Available Commands:")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1-10 updates/sec)")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker")
|
||||
if CleveRoids.hasSuperwow then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
|
||||
@@ -5824,6 +5897,8 @@ SlashCmdList["CLEVEROID"] = function(msg)
|
||||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid tankdebug - Debug tank targeting conditionals')
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00All-Caster Aura Tracking:|r")
|
||||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid auradebug - Debug buff/debuff time tracking from all casters')
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Overflow Buff Frame:|r")
|
||||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowtest - Toggle overflow buff frame test mode')
|
||||
return
|
||||
end
|
||||
|
||||
@@ -5854,6 +5929,23 @@ SlashCmdList["CLEVEROID"] = function(msg)
|
||||
return
|
||||
end
|
||||
|
||||
-- macrocheck
|
||||
if cmd == "macrocheck" then
|
||||
local num = tonumber(val)
|
||||
if num == 0 or num == 1 then
|
||||
CleveRoidMacros.macrocheck = num
|
||||
if num == 1 then
|
||||
CleveRoids.Print("Macro syntax checker |cff00ff00enabled|r")
|
||||
else
|
||||
CleveRoids.Print("Macro syntax checker |cffff0000disabled|r (reload UI to take effect)")
|
||||
end
|
||||
else
|
||||
CleveRoids.Print("Usage: /cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker (Default: 1)")
|
||||
CleveRoids.Print("Current macrocheck = " .. tostring(CleveRoidMacros.macrocheck))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- learn (manual set duration)
|
||||
if cmd == "learn" then
|
||||
if not CleveRoids.hasSuperwow then
|
||||
@@ -6559,6 +6651,22 @@ SlashCmdList["CLEVEROID"] = function(msg)
|
||||
return
|
||||
end
|
||||
|
||||
-- overflowtest (toggle overflow buff frame test mode)
|
||||
if cmd == "overflowtest" then
|
||||
if CleveRoids.ToggleOverflowTest then
|
||||
CleveRoids.ToggleOverflowTest()
|
||||
else
|
||||
CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Delegate to extension console handlers (chain pattern)
|
||||
if CleveRoids.HandleConsoleCommand then
|
||||
CleveRoids.HandleConsoleCommand(msg)
|
||||
return
|
||||
end
|
||||
|
||||
-- Unknown command fallback
|
||||
CleveRoids.Print("Usage:")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
|
||||
|
||||
+361
-6
@@ -29,11 +29,21 @@ local pendingValidation = false
|
||||
local lastSelectedMacro = nil
|
||||
local updateFrame = nil
|
||||
local hooked = false
|
||||
local measureFs = nil -- hidden FontString for measuring text width (wrap detection)
|
||||
|
||||
-- Cache for current errors (avoids re-validation on every frame)
|
||||
local currentErrors = nil
|
||||
local nameHighlight = nil -- Yellow backdrop behind macro name when name has errors
|
||||
|
||||
-- Whitelist GUI state
|
||||
local whitelistButton = nil
|
||||
local whitelistPopup = nil
|
||||
local WHITELIST_ROW_POOL_SIZE = 8
|
||||
local whitelistRows = {} -- pre-allocated label+remove rows
|
||||
|
||||
-- Forward declarations (defined later, referenced by whitelist GUI)
|
||||
local RequestValidation
|
||||
|
||||
-- ============================================================================
|
||||
-- Error Panel (Option A)
|
||||
-- ============================================================================
|
||||
@@ -45,7 +55,7 @@ local function CreateErrorPanel()
|
||||
-- Anchor to the scroll frame (text edit area) instead of the full MacroFrame,
|
||||
-- which can extend far below the visible UI on extended macro clients
|
||||
local scrollRef = MacroFrameScrollFrame or MacroFrame
|
||||
panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", 0, -27)
|
||||
panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", -20, -27)
|
||||
panel:SetPoint("TOPRIGHT", scrollRef, "BOTTOMRIGHT", 0, -27)
|
||||
panel:SetHeight(1)
|
||||
panel:SetFrameStrata("DIALOG")
|
||||
@@ -186,18 +196,61 @@ local function UpdateLineHighlights(errors)
|
||||
local editWidth = MacroFrameText:GetWidth()
|
||||
if editWidth < 10 then editWidth = 260 end -- fallback
|
||||
|
||||
-- Create measurement FontString lazily (same font as EditBox, hidden)
|
||||
if not measureFs then
|
||||
measureFs = (MacroFrame or UIParent):CreateFontString(nil, "OVERLAY")
|
||||
measureFs:Hide()
|
||||
end
|
||||
local fontPath, fontSize, fontFlags = MacroFrameText:GetFont()
|
||||
if fontPath then
|
||||
measureFs:SetFont(fontPath, fontSize, fontFlags)
|
||||
end
|
||||
|
||||
-- Split macro text into logical lines for wrap calculation
|
||||
local text = MacroFrameText:GetText() or ""
|
||||
local logicalLines = {}
|
||||
local lineStart = 1
|
||||
while true do
|
||||
local nlPos = string.find(text, "\n", lineStart, true)
|
||||
if nlPos then
|
||||
table.insert(logicalLines, string.sub(text, lineStart, nlPos - 1))
|
||||
lineStart = nlPos + 1
|
||||
else
|
||||
table.insert(logicalLines, string.sub(text, lineStart))
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate visual line count for each logical line
|
||||
local visualCounts = {}
|
||||
for i, line in ipairs(logicalLines) do
|
||||
if line == "" or editWidth <= 0 then
|
||||
visualCounts[i] = 1
|
||||
else
|
||||
measureFs:SetText(line)
|
||||
local textWidth = measureFs:GetStringWidth()
|
||||
visualCounts[i] = math.max(1, math.ceil(textWidth / editWidth))
|
||||
end
|
||||
end
|
||||
|
||||
local highlightIdx = 1
|
||||
for lineNum, _ in pairs(errorLines) do
|
||||
if highlightIdx > HIGHLIGHT_POOL_SIZE then break end
|
||||
|
||||
local tex = lineHighlights[highlightIdx]
|
||||
-- Position: top text inset + (lineNum-1) lines down
|
||||
local yOffset = -topInset - ((lineNum - 1) * lineHeight)
|
||||
|
||||
-- Y offset: sum visual lines of all preceding logical lines
|
||||
local yLines = 0
|
||||
for i = 1, lineNum - 1 do
|
||||
yLines = yLines + (visualCounts[i] or 1)
|
||||
end
|
||||
local yOffset = -topInset - (yLines * lineHeight)
|
||||
local highlightHeight = (visualCounts[lineNum] or 1) * lineHeight
|
||||
|
||||
tex:ClearAllPoints()
|
||||
tex:SetPoint("TOPLEFT", MacroFrameText, "TOPLEFT", -2, yOffset)
|
||||
tex:SetWidth(editWidth + 4)
|
||||
tex:SetHeight(lineHeight)
|
||||
tex:SetHeight(highlightHeight)
|
||||
tex:Show()
|
||||
|
||||
highlightIdx = highlightIdx + 1
|
||||
@@ -246,6 +299,300 @@ local function UpdateNameHighlight(hasNameErrors)
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Command Whitelist GUI
|
||||
-- ============================================================================
|
||||
|
||||
local function RefreshWhitelistDisplay()
|
||||
if not whitelistPopup then return end
|
||||
|
||||
local commands = CleveRoids.GetWhitelistedCommandsList()
|
||||
local count = table.getn(commands)
|
||||
|
||||
-- Update rows
|
||||
for i = 1, WHITELIST_ROW_POOL_SIZE do
|
||||
local row = whitelistRows[i]
|
||||
if row then
|
||||
if i <= count then
|
||||
row.label:SetText(commands[i])
|
||||
row.frame:Show()
|
||||
else
|
||||
row.label:SetText("")
|
||||
row.frame:Hide()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Show overflow indicator
|
||||
local overflowText = whitelistPopup.overflowText
|
||||
if count > WHITELIST_ROW_POOL_SIZE then
|
||||
overflowText:SetText("|cff888888... and " .. (count - WHITELIST_ROW_POOL_SIZE) .. " more|r")
|
||||
overflowText:Show()
|
||||
else
|
||||
overflowText:SetText("")
|
||||
overflowText:Hide()
|
||||
end
|
||||
|
||||
-- Dynamic height: title(20) + gap(8) + editbox(18) + rows(18 each) + padding
|
||||
local rowsShown = count
|
||||
if rowsShown > WHITELIST_ROW_POOL_SIZE then rowsShown = WHITELIST_ROW_POOL_SIZE end
|
||||
local height = 8 + 20 + 8 + 18 + 2 + (rowsShown * 18)
|
||||
if count > WHITELIST_ROW_POOL_SIZE then
|
||||
height = height + 14
|
||||
end
|
||||
height = height + 8 -- bottom padding
|
||||
whitelistPopup:SetHeight(height)
|
||||
|
||||
-- Re-trigger error checking so results update in real time
|
||||
RequestValidation()
|
||||
end
|
||||
|
||||
local function CreateWhitelistPopup()
|
||||
if whitelistPopup then return end
|
||||
|
||||
local popup = CreateFrame("Frame", "CleveRoidsWhitelistPopup", UIParent)
|
||||
popup:SetWidth(220)
|
||||
popup:SetHeight(120)
|
||||
popup:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
|
||||
popup:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
popup:SetMovable(true)
|
||||
popup:EnableMouse(true)
|
||||
|
||||
-- pfUI pixel-perfect flat style
|
||||
popup:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8x8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
||||
tile = false, tileSize = 0, edgeSize = 1,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
||||
})
|
||||
popup:SetBackdropColor(0.08, 0.08, 0.1, 0.95)
|
||||
popup:SetBackdropBorderColor(0.2, 0.2, 0.2, 1)
|
||||
popup:Hide()
|
||||
|
||||
-- Drag handling
|
||||
popup:RegisterForDrag("LeftButton")
|
||||
popup:SetScript("OnDragStart", function()
|
||||
this:StartMoving()
|
||||
end)
|
||||
popup:SetScript("OnDragStop", function()
|
||||
this:StopMovingOrSizing()
|
||||
end)
|
||||
|
||||
-- Title
|
||||
local title = popup:CreateFontString(nil, "OVERLAY")
|
||||
title:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
|
||||
title:SetPoint("TOPLEFT", popup, "TOPLEFT", 8, -8)
|
||||
title:SetText("Whitelisted Commands")
|
||||
title:SetTextColor(0.9, 0.8, 0.5, 1)
|
||||
|
||||
-- Close button (flat X)
|
||||
local closeBtn = CreateFrame("Button", nil, popup)
|
||||
closeBtn:SetWidth(16)
|
||||
closeBtn:SetHeight(16)
|
||||
closeBtn:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -4, -4)
|
||||
local closeLbl = closeBtn:CreateFontString(nil, "OVERLAY")
|
||||
closeLbl:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
|
||||
closeLbl:SetPoint("CENTER", 0, 0)
|
||||
closeLbl:SetText("x")
|
||||
closeLbl:SetTextColor(0.6, 0.6, 0.6, 1)
|
||||
closeBtn:SetScript("OnEnter", function()
|
||||
closeLbl:SetTextColor(1, 0.3, 0.3, 1)
|
||||
end)
|
||||
closeBtn:SetScript("OnLeave", function()
|
||||
closeLbl:SetTextColor(0.6, 0.6, 0.6, 1)
|
||||
end)
|
||||
closeBtn:SetScript("OnClick", function()
|
||||
popup:Hide()
|
||||
end)
|
||||
|
||||
-- EditBox (flat style)
|
||||
local editBox = CreateFrame("EditBox", "CleveRoidsWhitelistEditBox", popup)
|
||||
editBox:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
|
||||
editBox:SetWidth(148)
|
||||
editBox:SetHeight(18)
|
||||
editBox:SetAutoFocus(false)
|
||||
editBox:SetMaxLetters(60)
|
||||
editBox:SetFont("Fonts\\FRIZQT__.TTF", 10, "")
|
||||
editBox:SetTextColor(1, 1, 1, 1)
|
||||
editBox:SetTextInsets(4, 4, 0, 0)
|
||||
editBox:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8x8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
||||
tile = false, tileSize = 0, edgeSize = 1,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
||||
})
|
||||
editBox:SetBackdropColor(0.05, 0.05, 0.07, 0.9)
|
||||
editBox:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
|
||||
|
||||
-- Add button (flat style)
|
||||
local addBtn = CreateFrame("Button", nil, popup)
|
||||
addBtn:SetPoint("LEFT", editBox, "RIGHT", 4, 0)
|
||||
addBtn:SetWidth(40)
|
||||
addBtn:SetHeight(18)
|
||||
addBtn:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8x8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
||||
tile = false, tileSize = 0, edgeSize = 1,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
||||
})
|
||||
addBtn:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
|
||||
addBtn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
|
||||
local addLbl = addBtn:CreateFontString(nil, "OVERLAY")
|
||||
addLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
|
||||
addLbl:SetPoint("CENTER", 0, 0)
|
||||
addLbl:SetText("Add")
|
||||
addLbl:SetTextColor(0.8, 0.8, 0.8, 1)
|
||||
addBtn:SetScript("OnEnter", function()
|
||||
this:SetBackdropColor(0.25, 0.25, 0.25, 0.95)
|
||||
this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1)
|
||||
addLbl:SetTextColor(1, 1, 1, 1)
|
||||
end)
|
||||
addBtn:SetScript("OnLeave", function()
|
||||
this:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
|
||||
this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
|
||||
addLbl:SetTextColor(0.8, 0.8, 0.8, 1)
|
||||
end)
|
||||
|
||||
local function AddCommand()
|
||||
local text = editBox:GetText()
|
||||
if not text or CleveRoids.Trim(text) == "" then return end
|
||||
text = CleveRoids.Trim(text)
|
||||
CleveRoids.AddWhitelistedCommand(text)
|
||||
editBox:SetText("")
|
||||
editBox:ClearFocus()
|
||||
RefreshWhitelistDisplay()
|
||||
end
|
||||
|
||||
addBtn:SetScript("OnClick", AddCommand)
|
||||
editBox:SetScript("OnEnterPressed", function()
|
||||
AddCommand()
|
||||
end)
|
||||
editBox:SetScript("OnEscapePressed", function()
|
||||
this:ClearFocus()
|
||||
end)
|
||||
|
||||
-- Anchor for rows: below the editbox row
|
||||
local rowAnchor = editBox
|
||||
|
||||
-- Pre-allocate row pool
|
||||
for i = 1, WHITELIST_ROW_POOL_SIZE do
|
||||
local rowFrame = CreateFrame("Frame", nil, popup)
|
||||
rowFrame:SetHeight(16)
|
||||
rowFrame:SetPoint("TOPLEFT", rowAnchor, "BOTTOMLEFT", 0, -2)
|
||||
rowFrame:SetPoint("RIGHT", popup, "RIGHT", -8, 0)
|
||||
rowFrame:Hide()
|
||||
|
||||
local label = rowFrame:CreateFontString(nil, "OVERLAY")
|
||||
label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
|
||||
label:SetPoint("LEFT", rowFrame, "LEFT", 2, 0)
|
||||
label:SetJustifyH("LEFT")
|
||||
label:SetTextColor(0.7, 0.7, 0.7, 1)
|
||||
|
||||
-- Flat X remove button
|
||||
local removeBtn = CreateFrame("Button", nil, rowFrame)
|
||||
removeBtn:SetWidth(14)
|
||||
removeBtn:SetHeight(14)
|
||||
removeBtn:SetPoint("RIGHT", rowFrame, "RIGHT", -1, 0)
|
||||
local removeLbl = removeBtn:CreateFontString(nil, "OVERLAY")
|
||||
removeLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
|
||||
removeLbl:SetPoint("CENTER", 0, 0)
|
||||
removeLbl:SetText("x")
|
||||
removeLbl:SetTextColor(0.5, 0.5, 0.5, 1)
|
||||
removeBtn.removeLbl = removeLbl
|
||||
removeBtn.rowIndex = i
|
||||
|
||||
removeBtn:SetScript("OnEnter", function()
|
||||
this.removeLbl:SetTextColor(1, 0.3, 0.3, 1)
|
||||
end)
|
||||
removeBtn:SetScript("OnLeave", function()
|
||||
this.removeLbl:SetTextColor(0.5, 0.5, 0.5, 1)
|
||||
end)
|
||||
removeBtn:SetScript("OnClick", function()
|
||||
local idx = this.rowIndex
|
||||
local row = whitelistRows[idx]
|
||||
if row and row.label then
|
||||
local cmd = row.label:GetText()
|
||||
if cmd and cmd ~= "" then
|
||||
CleveRoids.RemoveWhitelistedCommand(cmd)
|
||||
RefreshWhitelistDisplay()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
whitelistRows[i] = { frame = rowFrame, label = label, removeBtn = removeBtn }
|
||||
rowAnchor = rowFrame
|
||||
end
|
||||
|
||||
-- Overflow text
|
||||
local overflowText = popup:CreateFontString(nil, "OVERLAY")
|
||||
overflowText:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
|
||||
overflowText:SetPoint("TOPLEFT", whitelistRows[WHITELIST_ROW_POOL_SIZE].frame, "BOTTOMLEFT", 2, -2)
|
||||
overflowText:SetJustifyH("LEFT")
|
||||
overflowText:SetTextColor(0.5, 0.5, 0.5, 1)
|
||||
overflowText:Hide()
|
||||
popup.overflowText = overflowText
|
||||
|
||||
whitelistPopup = popup
|
||||
end
|
||||
|
||||
local function CreateWhitelistButton()
|
||||
if whitelistButton then return end
|
||||
if not MacroFrame then return end
|
||||
|
||||
local btn = CreateFrame("Button", "CleveRoidsWhitelistButton", MacroFrame)
|
||||
btn:SetWidth(80)
|
||||
btn:SetHeight(18)
|
||||
btn:SetPoint("BOTTOMRIGHT", MacroFrame, "BOTTOMRIGHT", 8, 52)
|
||||
btn:SetFrameStrata("DIALOG")
|
||||
|
||||
-- pfUI pixel-perfect flat style
|
||||
btn:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8x8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
||||
tile = false, tileSize = 0, edgeSize = 1,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
||||
})
|
||||
btn:SetBackdropColor(0.1, 0.1, 0.1, 0.85)
|
||||
btn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
|
||||
|
||||
local label = btn:CreateFontString(nil, "OVERLAY")
|
||||
label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
|
||||
label:SetPoint("CENTER", btn, "CENTER", 0, 0)
|
||||
label:SetText("Whitelist")
|
||||
label:SetTextColor(0.8, 0.8, 0.8, 1)
|
||||
btn.label = label
|
||||
|
||||
-- Hover highlight
|
||||
btn:SetScript("OnEnter", function()
|
||||
this:SetBackdropColor(0.2, 0.2, 0.2, 0.95)
|
||||
this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1)
|
||||
this.label:SetTextColor(1, 1, 1, 1)
|
||||
GameTooltip:SetOwner(this, "ANCHOR_TOP")
|
||||
GameTooltip:SetText("Command Whitelist")
|
||||
GameTooltip:AddLine("Add third-party addon commands so the", 1, 1, 1, true)
|
||||
GameTooltip:AddLine("error checker stops flagging them.", 1, 1, 1, true)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
btn:SetScript("OnLeave", function()
|
||||
this:SetBackdropColor(0.1, 0.1, 0.1, 0.85)
|
||||
this:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
|
||||
this.label:SetTextColor(0.8, 0.8, 0.8, 1)
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
|
||||
btn:SetScript("OnClick", function()
|
||||
CreateWhitelistPopup()
|
||||
if whitelistPopup:IsVisible() then
|
||||
whitelistPopup:Hide()
|
||||
else
|
||||
RefreshWhitelistDisplay()
|
||||
whitelistPopup:Show()
|
||||
end
|
||||
end)
|
||||
|
||||
whitelistButton = btn
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Validation & Debounce
|
||||
-- ============================================================================
|
||||
@@ -382,7 +729,7 @@ local function RunValidation()
|
||||
UpdateNameHighlight(hasNameErrors)
|
||||
end
|
||||
|
||||
local function RequestValidation()
|
||||
RequestValidation = function()
|
||||
lastKeystroke = GetTime()
|
||||
pendingValidation = true
|
||||
end
|
||||
@@ -563,6 +910,10 @@ local function ClearAll()
|
||||
if nameHighlight then
|
||||
nameHighlight:Hide()
|
||||
end
|
||||
|
||||
if whitelistPopup then
|
||||
whitelistPopup:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
@@ -591,8 +942,9 @@ local function InstallHooks()
|
||||
if origOnShow then
|
||||
origOnShow()
|
||||
end
|
||||
-- Create panel lazily on first show
|
||||
-- Create UI lazily on first show
|
||||
CreateErrorPanel()
|
||||
CreateWhitelistButton()
|
||||
-- Reset state and validate
|
||||
lastSelectedMacro = MacroFrame.selectedMacro
|
||||
RunValidation()
|
||||
@@ -633,6 +985,9 @@ function Extension.OnAddonLoaded()
|
||||
end
|
||||
|
||||
function Extension.OnLoad()
|
||||
-- Skip if macro checker is disabled
|
||||
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then return end
|
||||
|
||||
-- Skip if SuperMacro is loaded (detected at load time)
|
||||
if SuperMacroFrame ~= nil then return end
|
||||
|
||||
|
||||
@@ -0,0 +1,635 @@
|
||||
--[[
|
||||
OverflowBuffFrame Extension
|
||||
Two frames displaying server-side overflow buffs (no client aura slot) for
|
||||
the player and the current target (if a group/party member).
|
||||
Player data: CleveRoids.OverflowBuffs (AURA_CAST_ON_SELF when buff-capped).
|
||||
Target data: CleveRoids.AllCasterAuraTracking filtered by UnitBuff visibility.
|
||||
Each frame shows 2 rows of 8 icons = 16 slots.
|
||||
|
||||
Author: Mewtiny
|
||||
License: MIT License
|
||||
]]
|
||||
local _G = _G or getfenv(0)
|
||||
local CleveRoids = _G.CleveRoids or {}
|
||||
|
||||
-- ============================================================================
|
||||
-- Constants
|
||||
-- ============================================================================
|
||||
local ICON_SIZE = 24
|
||||
local ICON_SPACING = 2
|
||||
local ICONS_PER_ROW = 8
|
||||
local NUM_ROWS = 2
|
||||
local ICONS_PER_FRAME = ICONS_PER_ROW * NUM_ROWS -- 16
|
||||
local ROW_SPACING = 16 -- vertical gap between rows (icon + duration text)
|
||||
local DURATION_FONT_SIZE = 10
|
||||
local DURATION_OFFSET_Y = -3
|
||||
local UPDATE_INTERVAL = 0.2
|
||||
local LABEL_HEIGHT = 12
|
||||
|
||||
-- ============================================================================
|
||||
-- State
|
||||
-- ============================================================================
|
||||
local playerFrame = nil
|
||||
local targetFrame = nil
|
||||
local playerIcons = {} -- [1..16]
|
||||
local targetIcons = {} -- [1..16]
|
||||
local playerLabelFs = nil
|
||||
local targetLabelFs = nil
|
||||
local updateFrame = nil
|
||||
local updateElapsed = 0
|
||||
local testMode = false
|
||||
local dirty = false -- Flag for deferred rebuild (avoids event ordering race)
|
||||
local lastPlayerCount = 0
|
||||
local lastTargetCount = 0
|
||||
|
||||
-- ============================================================================
|
||||
-- Duration Formatting (matches pfUI api.lua:1324 style)
|
||||
-- ============================================================================
|
||||
local function FormatDuration(seconds)
|
||||
if not seconds or seconds <= 0 then return "" end
|
||||
if seconds >= 100 then
|
||||
return math.floor(seconds / 60 + 0.5) .. "m"
|
||||
elseif seconds > 5 then
|
||||
return math.floor(seconds + 0.5) .. "s"
|
||||
else
|
||||
return string.format("%.1f", seconds)
|
||||
end
|
||||
end
|
||||
|
||||
-- Forward declarations (needed by OnClick closures in CreateIconButton)
|
||||
local RebuildFrame
|
||||
|
||||
-- ============================================================================
|
||||
-- Icon Button Creation
|
||||
-- ============================================================================
|
||||
local function CreateIconButton(parent, index, iconTable)
|
||||
local btn = CreateFrame("Button", nil, parent)
|
||||
btn:SetWidth(ICON_SIZE)
|
||||
btn:SetHeight(ICON_SIZE)
|
||||
btn:RegisterForClicks("RightButtonUp")
|
||||
|
||||
-- Dark 1px border backdrop (pfUI style)
|
||||
btn:SetBackdrop({
|
||||
bgFile = "Interface\\Buttons\\WHITE8x8",
|
||||
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
||||
tile = false, tileSize = 0, edgeSize = 1,
|
||||
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
||||
})
|
||||
btn:SetBackdropColor(0, 0, 0, 0.6)
|
||||
btn:SetBackdropBorderColor(0.1, 0.1, 0.1, 0.9)
|
||||
|
||||
-- Icon texture (inset by 1px for border)
|
||||
local icon = btn:CreateTexture(nil, "ARTWORK")
|
||||
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -1)
|
||||
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
|
||||
icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
|
||||
icon:Hide()
|
||||
|
||||
-- Duration text below icon
|
||||
local dur = btn:CreateFontString(nil, "OVERLAY")
|
||||
dur:SetFont("Fonts\\FRIZQT__.TTF", DURATION_FONT_SIZE, "OUTLINE")
|
||||
dur:SetPoint("TOP", btn, "BOTTOM", 0, DURATION_OFFSET_Y)
|
||||
dur:SetJustifyH("CENTER")
|
||||
dur:SetTextColor(1, 1, 1, 1)
|
||||
dur:SetText("")
|
||||
|
||||
-- Right-click to cancel player overflow buffs
|
||||
btn:SetScript("OnClick", function()
|
||||
local data = iconTable[index]
|
||||
if not data or not data.spellId then return end
|
||||
if data.source ~= "player" then return end
|
||||
|
||||
-- CancelPlayerAuraSpellId(spellId, ignoreMissing)
|
||||
-- ignoreMissing=1 is required for overflow buffs that have no client aura slot
|
||||
if CancelPlayerAuraSpellId then
|
||||
CancelPlayerAuraSpellId(data.spellId, 1)
|
||||
end
|
||||
|
||||
-- Remove from overflow tracking
|
||||
if CleveRoids.OverflowBuffs then
|
||||
CleveRoids.OverflowBuffs[data.spellId] = nil
|
||||
end
|
||||
|
||||
-- Hide tooltip and rebuild
|
||||
GameTooltip:Hide()
|
||||
RebuildFrame()
|
||||
end)
|
||||
|
||||
-- Tooltip
|
||||
btn:SetScript("OnEnter", function()
|
||||
local data = iconTable[index]
|
||||
if not data or not data.spellId then return end
|
||||
GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT")
|
||||
local spellName = SpellInfo and SpellInfo(data.spellId) or ("Spell " .. data.spellId)
|
||||
if spellName then
|
||||
local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "")
|
||||
GameTooltip:AddLine(baseName, 1, 1, 1)
|
||||
end
|
||||
if data.remaining and data.remaining > 0 then
|
||||
GameTooltip:AddLine(FormatDuration(data.remaining) .. " remaining", 0.7, 0.7, 0.7)
|
||||
end
|
||||
if data.source == "player" then
|
||||
GameTooltip:AddLine("Right-click to cancel", 0.8, 0.8, 0.5)
|
||||
else
|
||||
GameTooltip:AddLine("Overflow buff (target)", 0.5, 1.0, 0.8)
|
||||
end
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
btn:SetScript("OnLeave", function()
|
||||
GameTooltip:Hide()
|
||||
end)
|
||||
|
||||
btn:Hide()
|
||||
|
||||
return {
|
||||
button = btn,
|
||||
icon = icon,
|
||||
duration = dur,
|
||||
spellId = nil,
|
||||
remaining = nil,
|
||||
source = nil,
|
||||
}
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Frame Creation
|
||||
-- ============================================================================
|
||||
|
||||
-- Compute frame dimensions
|
||||
local FRAME_WIDTH = ICONS_PER_ROW * (ICON_SIZE + ICON_SPACING) - ICON_SPACING
|
||||
local FRAME_HEIGHT = LABEL_HEIGHT + NUM_ROWS * (ICON_SIZE + ROW_SPACING)
|
||||
|
||||
local function CreateBuffFrame(name, savedPosKey, defaultY, labelColor, iconTable)
|
||||
local frame = CreateFrame("Frame", name, UIParent)
|
||||
frame:SetWidth(FRAME_WIDTH)
|
||||
frame:SetHeight(FRAME_HEIGHT)
|
||||
frame:SetPoint("TOP", UIParent, "TOP", 0, defaultY)
|
||||
frame:SetFrameStrata("MEDIUM")
|
||||
frame:SetMovable(true)
|
||||
frame:EnableMouse(false)
|
||||
frame:Hide()
|
||||
|
||||
-- Section label
|
||||
local label = frame:CreateFontString(nil, "OVERLAY")
|
||||
label:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
|
||||
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
|
||||
label:SetTextColor(labelColor[1], labelColor[2], labelColor[3], 0.8)
|
||||
label:SetText("")
|
||||
|
||||
-- Create 16 icon slots in 2 rows of 8
|
||||
for i = 1, ICONS_PER_FRAME do
|
||||
iconTable[i] = CreateIconButton(frame, i, iconTable)
|
||||
local row = math.floor((i - 1) / ICONS_PER_ROW)
|
||||
local col = math.mod(i - 1, ICONS_PER_ROW)
|
||||
local xOff = col * (ICON_SIZE + ICON_SPACING)
|
||||
local yOff = -(LABEL_HEIGHT + row * (ICON_SIZE + ROW_SPACING))
|
||||
iconTable[i].button:SetPoint("TOPLEFT", frame, "TOPLEFT", xOff, yOff)
|
||||
end
|
||||
|
||||
-- Shift+drag to move
|
||||
frame:SetScript("OnMouseDown", function()
|
||||
if IsShiftKeyDown() then
|
||||
frame:StartMoving()
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnMouseUp", function()
|
||||
frame:StopMovingOrSizing()
|
||||
local _, _, _, x, y = frame:GetPoint(1)
|
||||
CleveRoidMacros = CleveRoidMacros or {}
|
||||
CleveRoidMacros[savedPosKey] = { x = x, y = y }
|
||||
end)
|
||||
|
||||
-- Restore saved position
|
||||
CleveRoidMacros = CleveRoidMacros or {}
|
||||
if CleveRoidMacros[savedPosKey] then
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint("TOP", UIParent, "TOP",
|
||||
CleveRoidMacros[savedPosKey].x or 0,
|
||||
CleveRoidMacros[savedPosKey].y or defaultY)
|
||||
end
|
||||
|
||||
return frame, label
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Data Collection
|
||||
-- ============================================================================
|
||||
|
||||
local function GetPlayerOverflowBuffs()
|
||||
local results = {}
|
||||
local overflowBuffs = CleveRoids.OverflowBuffs
|
||||
if not overflowBuffs then return results end
|
||||
|
||||
local now = GetTime()
|
||||
for spellId, entry in pairs(overflowBuffs) do
|
||||
if entry.timestamp and entry.durationSec then
|
||||
local remaining = entry.durationSec - (now - entry.timestamp)
|
||||
if remaining > 0 then
|
||||
table.insert(results, {
|
||||
spellId = spellId,
|
||||
remaining = remaining,
|
||||
source = "player",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(results, function(a, b) return a.remaining > b.remaining end)
|
||||
return results
|
||||
end
|
||||
|
||||
local function GetTargetOverflowBuffs()
|
||||
local results = {}
|
||||
|
||||
if not UnitExists("target") then return results end
|
||||
if not testMode and not (UnitInParty("target") or UnitInRaid("target")) then return results end
|
||||
|
||||
local _, targetGuid = UnitExists("target")
|
||||
if not targetGuid then return results end
|
||||
|
||||
local trackingData = CleveRoids.AllCasterAuraTracking
|
||||
if not trackingData or not trackingData[targetGuid] then return results end
|
||||
|
||||
-- Build set of visible buff textures on target
|
||||
local visibleBuffs = {}
|
||||
for i = 1, 32 do
|
||||
local texture = UnitBuff("target", i)
|
||||
if not texture then break end
|
||||
visibleBuffs[texture] = true
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local lib = CleveRoids.libdebuff
|
||||
|
||||
for spellId, auraData in pairs(trackingData[targetGuid]) do
|
||||
if auraData.start and auraData.duration then
|
||||
local remaining = auraData.duration + auraData.start - now
|
||||
if remaining > 0 then
|
||||
local isVisible = false
|
||||
if not auraData._testEntry then
|
||||
local spellIcon = lib and lib:GetCachedIcon(spellId)
|
||||
isVisible = spellIcon and visibleBuffs[spellIcon]
|
||||
end
|
||||
if not isVisible then
|
||||
table.insert(results, {
|
||||
spellId = spellId,
|
||||
remaining = remaining,
|
||||
source = "target",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(results, function(a, b) return a.remaining > b.remaining end)
|
||||
return results
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Layout
|
||||
-- ============================================================================
|
||||
|
||||
local function PopulateFrame(frame, label, labelText, iconTable, buffs, countRef)
|
||||
local count = table.getn(buffs)
|
||||
if count > ICONS_PER_FRAME then count = ICONS_PER_FRAME end
|
||||
|
||||
local lib = CleveRoids.libdebuff
|
||||
|
||||
for i = 1, count do
|
||||
local data = iconTable[i]
|
||||
local buff = buffs[i]
|
||||
|
||||
local texture = lib and lib:GetCachedIcon(buff.spellId)
|
||||
if texture then
|
||||
data.icon:SetTexture(texture)
|
||||
else
|
||||
data.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
end
|
||||
data.icon:Show()
|
||||
|
||||
data.spellId = buff.spellId
|
||||
data.remaining = buff.remaining
|
||||
data.source = buff.source
|
||||
data.duration:SetText(FormatDuration(buff.remaining))
|
||||
data.button:EnableMouse(true)
|
||||
data.button:Show()
|
||||
end
|
||||
|
||||
-- Hide unused
|
||||
for i = count + 1, ICONS_PER_FRAME do
|
||||
local data = iconTable[i]
|
||||
data.button:Hide()
|
||||
data.icon:Hide()
|
||||
data.duration:SetText("")
|
||||
data.spellId = nil
|
||||
data.remaining = nil
|
||||
data.source = nil
|
||||
end
|
||||
|
||||
if count > 0 then
|
||||
label:SetText(labelText)
|
||||
frame:EnableMouse(true)
|
||||
frame:Show()
|
||||
else
|
||||
label:SetText("")
|
||||
frame:EnableMouse(false)
|
||||
frame:Hide()
|
||||
end
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
RebuildFrame = function()
|
||||
if not playerFrame then return end
|
||||
|
||||
local pBuffs = GetPlayerOverflowBuffs()
|
||||
lastPlayerCount = PopulateFrame(playerFrame, playerLabelFs, "Overflow (You)", playerIcons, pBuffs)
|
||||
|
||||
local tBuffs = GetTargetOverflowBuffs()
|
||||
local targetName = UnitExists("target") and UnitName("target") or "Target"
|
||||
lastTargetCount = PopulateFrame(targetFrame, targetLabelFs, "Overflow (" .. targetName .. ")", targetIcons, tBuffs)
|
||||
end
|
||||
|
||||
-- Refresh only duration text (no full rebuild)
|
||||
local function RefreshDurations()
|
||||
local anyExpired = false
|
||||
local now = GetTime()
|
||||
|
||||
-- Player durations
|
||||
if playerFrame and playerFrame:IsVisible() then
|
||||
for i = 1, lastPlayerCount do
|
||||
local data = playerIcons[i]
|
||||
if data.spellId then
|
||||
local entry = CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[data.spellId]
|
||||
if entry and entry.timestamp and entry.durationSec then
|
||||
local remaining = entry.durationSec - (now - entry.timestamp)
|
||||
if remaining > 0 then
|
||||
data.remaining = remaining
|
||||
data.duration:SetText(FormatDuration(remaining))
|
||||
else
|
||||
anyExpired = true
|
||||
end
|
||||
else
|
||||
anyExpired = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Target durations
|
||||
if targetFrame and targetFrame:IsVisible() then
|
||||
local targetGuid = nil
|
||||
if UnitExists("target") then
|
||||
local _, guid = UnitExists("target")
|
||||
targetGuid = guid
|
||||
end
|
||||
for i = 1, lastTargetCount do
|
||||
local data = targetIcons[i]
|
||||
if data.spellId and targetGuid then
|
||||
local remaining = CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, data.spellId) or 0
|
||||
if remaining > 0 then
|
||||
data.remaining = remaining
|
||||
data.duration:SetText(FormatDuration(remaining))
|
||||
else
|
||||
anyExpired = true
|
||||
end
|
||||
else
|
||||
anyExpired = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if anyExpired then
|
||||
RebuildFrame()
|
||||
end
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Test Mode
|
||||
-- ============================================================================
|
||||
|
||||
local TEST_PLAYER_SPELL_IDS = {
|
||||
1126, -- Mark of the Wild
|
||||
1243, -- Power Word: Fortitude
|
||||
1461, -- Arcane Intellect
|
||||
14752, -- Divine Spirit
|
||||
976, -- Shadow Protection
|
||||
6307, -- Blood Pact
|
||||
20217, -- Blessing of Kings
|
||||
19740, -- Blessing of Might
|
||||
21849, -- Gift of the Wild
|
||||
25898, -- Greater Blessing of Kings
|
||||
25899, -- Greater Blessing of Sanctuary
|
||||
25890, -- Greater Blessing of Light
|
||||
10938, -- Greater Power Word: Fortitude
|
||||
10157, -- Greater Arcane Intellect
|
||||
20911, -- Blessing of Sanctuary
|
||||
25782, -- Greater Blessing of Might
|
||||
}
|
||||
|
||||
local TEST_TARGET_SPELL_IDS = {
|
||||
467, -- Thorns
|
||||
10060, -- Power Infusion
|
||||
17007, -- Leader of the Pack
|
||||
24932, -- Leader of the Pack aura
|
||||
19506, -- Trueshot Aura
|
||||
8936, -- Regrowth
|
||||
774, -- Rejuvenation
|
||||
139, -- Renew
|
||||
1022, -- Blessing of Protection
|
||||
6346, -- Fear Ward
|
||||
1044, -- Blessing of Freedom
|
||||
10958, -- Shadow Protection (rank)
|
||||
27681, -- Prayer of Spirit
|
||||
21562, -- Prayer of Fortitude
|
||||
20914, -- Blessing of Sanctuary (rank)
|
||||
25916, -- Greater Blessing of Wisdom
|
||||
}
|
||||
|
||||
local testTargetGuid = nil
|
||||
local testStartTime = 0 -- when test mode was enabled (for duration calc)
|
||||
|
||||
-- Remove test entries from a specific GUID in AllCasterAuraTracking
|
||||
local function ClearTestTargetData(guid)
|
||||
if not guid or not CleveRoids.AllCasterAuraTracking then return end
|
||||
local targetData = CleveRoids.AllCasterAuraTracking[guid]
|
||||
if not targetData then return end
|
||||
for spellId, entry in pairs(targetData) do
|
||||
if entry._testEntry then
|
||||
targetData[spellId] = nil
|
||||
end
|
||||
end
|
||||
if not next(targetData) then
|
||||
CleveRoids.AllCasterAuraTracking[guid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Inject test entries for the current target
|
||||
local function InjectTestTargetData()
|
||||
if not UnitExists("target") then return end
|
||||
local _, targetGuid = UnitExists("target")
|
||||
if not targetGuid then return end
|
||||
|
||||
-- Clean up old test target if it changed
|
||||
if testTargetGuid and testTargetGuid ~= targetGuid then
|
||||
ClearTestTargetData(testTargetGuid)
|
||||
end
|
||||
|
||||
testTargetGuid = targetGuid
|
||||
CleveRoids.AllCasterAuraTracking = CleveRoids.AllCasterAuraTracking or {}
|
||||
if not CleveRoids.AllCasterAuraTracking[targetGuid] then
|
||||
CleveRoids.AllCasterAuraTracking[targetGuid] = {}
|
||||
end
|
||||
|
||||
local targetDurations = { 8, 15, 25, 40, 55, 90, 150, 240, 400, 500, 700, 1000, 1300, 1600, 2000, 3000 }
|
||||
for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do
|
||||
local spellId = TEST_TARGET_SPELL_IDS[i]
|
||||
local dur = targetDurations[i] or 60
|
||||
CleveRoids.AllCasterAuraTracking[targetGuid][spellId] = {
|
||||
start = testStartTime,
|
||||
duration = dur,
|
||||
casterGuid = "test",
|
||||
_testEntry = true,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local function EnableTestMode()
|
||||
testMode = true
|
||||
testStartTime = GetTime()
|
||||
CleveRoids.OverflowBuffs = CleveRoids.OverflowBuffs or {}
|
||||
|
||||
local playerDurations = { 5, 10, 30, 45, 60, 120, 180, 300, 450, 600, 900, 1200, 1500, 1800, 2400, 3600 }
|
||||
|
||||
for i = 1, table.getn(TEST_PLAYER_SPELL_IDS) do
|
||||
local spellId = TEST_PLAYER_SPELL_IDS[i]
|
||||
local dur = playerDurations[i] or 60
|
||||
CleveRoids.OverflowBuffs[spellId] = {
|
||||
timestamp = testStartTime,
|
||||
durationSec = dur,
|
||||
_testEntry = true,
|
||||
}
|
||||
end
|
||||
|
||||
-- Inject target test data if we have a target
|
||||
InjectTestTargetData()
|
||||
|
||||
CleveRoids.Print("|cff00ff00Overflow buff frame test mode enabled|r")
|
||||
CleveRoids.Print(" Shift+drag to reposition the frames")
|
||||
|
||||
RebuildFrame()
|
||||
end
|
||||
|
||||
local function DisableTestMode()
|
||||
testMode = false
|
||||
|
||||
if CleveRoids.OverflowBuffs then
|
||||
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
|
||||
if entry._testEntry then
|
||||
CleveRoids.OverflowBuffs[spellId] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ClearTestTargetData(testTargetGuid)
|
||||
testTargetGuid = nil
|
||||
|
||||
CleveRoids.Print("|cffff9900Overflow buff frame test mode disabled|r")
|
||||
|
||||
RebuildFrame()
|
||||
end
|
||||
|
||||
local function ToggleTestMode()
|
||||
if testMode then
|
||||
DisableTestMode()
|
||||
else
|
||||
EnableTestMode()
|
||||
end
|
||||
end
|
||||
|
||||
CleveRoids.ToggleOverflowTest = ToggleTestMode
|
||||
|
||||
-- ============================================================================
|
||||
-- OnUpdate Handler
|
||||
-- ============================================================================
|
||||
local function OnUpdate()
|
||||
updateElapsed = updateElapsed + arg1
|
||||
-- Check dirty flag every frame for responsive rebuilds after AURA_CAST events.
|
||||
-- By the time OnUpdate fires, the core handler has already updated OverflowBuffs.
|
||||
if dirty then
|
||||
dirty = false
|
||||
updateElapsed = 0
|
||||
RebuildFrame()
|
||||
return
|
||||
end
|
||||
if updateElapsed < UPDATE_INTERVAL then return end
|
||||
updateElapsed = 0
|
||||
RefreshDurations()
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Extension Registration
|
||||
-- ============================================================================
|
||||
local ext = CleveRoids.RegisterExtension("OverflowBuffFrame")
|
||||
|
||||
ext.OnTargetChanged = function()
|
||||
-- Re-inject test data for the new target during test mode
|
||||
if testMode then
|
||||
InjectTestTargetData()
|
||||
end
|
||||
RebuildFrame()
|
||||
end
|
||||
|
||||
ext.OnAuraCast = function()
|
||||
-- Defer rebuild to OnUpdate: the core handler that populates OverflowBuffs
|
||||
-- registers for AURA_CAST_ON_SELF later (PLAYER_ENTERING_WORLD) than this
|
||||
-- extension (ADDON_LOADED), so it fires AFTER us. A direct RebuildFrame()
|
||||
-- here would read stale data.
|
||||
dirty = true
|
||||
end
|
||||
|
||||
ext.OnGroupChanged = function()
|
||||
RebuildFrame()
|
||||
end
|
||||
|
||||
ext.OnPlayerDead = function()
|
||||
if CleveRoids.OverflowBuffs then
|
||||
if testMode then
|
||||
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
|
||||
if not entry._testEntry then
|
||||
CleveRoids.OverflowBuffs[spellId] = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
CleveRoids.OverflowBuffs = {}
|
||||
end
|
||||
end
|
||||
RebuildFrame()
|
||||
end
|
||||
|
||||
ext.OnLoad = function()
|
||||
playerFrame, playerLabelFs = CreateBuffFrame(
|
||||
"CleveRoidsOverflowPlayer", "overflowPlayerPos", -100,
|
||||
{ 0.4, 0.7, 1.0 }, playerIcons)
|
||||
|
||||
targetFrame, targetLabelFs = CreateBuffFrame(
|
||||
"CleveRoidsOverflowTarget", "overflowTargetPos", -170,
|
||||
{ 0.4, 1.0, 0.7 }, targetIcons)
|
||||
|
||||
ext.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged")
|
||||
ext.RegisterEvent("PARTY_MEMBERS_CHANGED", "OnGroupChanged")
|
||||
ext.RegisterEvent("RAID_ROSTER_UPDATE", "OnGroupChanged")
|
||||
ext.RegisterEvent("PLAYER_DEAD", "OnPlayerDead")
|
||||
|
||||
if CleveRoids.hasNampower then
|
||||
ext.RegisterEvent("AURA_CAST_ON_SELF", "OnAuraCast")
|
||||
end
|
||||
|
||||
-- Shared OnUpdate frame
|
||||
updateFrame = CreateFrame("Frame")
|
||||
updateFrame:SetScript("OnUpdate", OnUpdate)
|
||||
|
||||
RebuildFrame()
|
||||
end
|
||||
|
||||
_G["CleveRoids"] = CleveRoids
|
||||
@@ -42,7 +42,7 @@ CleveRoids.spell_tracking = {}
|
||||
-- Format: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime}
|
||||
CleveRoids.castTracking = {}
|
||||
|
||||
-- pfUI 7.6+ with Nampower 2.27.2+ detected (GUID-based cast tracking available)
|
||||
-- pfUI 7.6+ with Nampower 2.31.0+ detected (GUID-based cast tracking available)
|
||||
CleveRoids.hasPfUI76 = false
|
||||
|
||||
-- Combo point tracking (initialized early for /cast hook)
|
||||
@@ -123,6 +123,8 @@ CleveRoids.dynamicCmds = {
|
||||
["/equip12"] = true,
|
||||
["/equip13"] = true,
|
||||
["/equip14"] = true,
|
||||
["/applymain"] = true,
|
||||
["/applyoff"] = true,
|
||||
}
|
||||
|
||||
-- Equipment swap queue system
|
||||
@@ -161,6 +163,7 @@ CleveRoids.ignoreKeywords = {
|
||||
action = true,
|
||||
ignoretooltip = true,
|
||||
cancelaura = true,
|
||||
noSpam = true, -- ! prefix flag: prevent toggle-off at execution time
|
||||
_operators = true, -- Metadata for AND/OR operator tracking
|
||||
_groups = true, -- Grouped conditional values for AND/OR evaluation
|
||||
multiscan = true, -- Processed before Keywords loop (target resolution)
|
||||
@@ -321,6 +324,8 @@ initFrame:SetScript("OnEvent", function()
|
||||
CleveRoids.hasGetUnitData = API.features.hasGetUnitData
|
||||
CleveRoids.hasGetSpellModifiers = API.features.hasGetSpellModifiers
|
||||
CleveRoids.hasEnhancedSpellFunctions = API.features.hasEnhancedSpellFunctions
|
||||
-- v2.37+: CastSpellByName supports unit token strings as 2nd param
|
||||
CleveRoids.hasCastSpellByNameUnitToken = API.features.hasCastSpellByNameUnitToken
|
||||
end
|
||||
|
||||
-- Initialize the API
|
||||
|
||||
+62
-2
@@ -422,6 +422,59 @@ local VALID_COMMANDS = {
|
||||
["/yes"] = true,
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- Command Whitelist API (for third-party addon commands)
|
||||
-- ============================================================================
|
||||
|
||||
local function GetWhitelistedCommands()
|
||||
if not CleveRoidMacros then CleveRoidMacros = {} end
|
||||
if not CleveRoidMacros.whitelistedCommands then
|
||||
CleveRoidMacros.whitelistedCommands = {}
|
||||
end
|
||||
return CleveRoidMacros.whitelistedCommands
|
||||
end
|
||||
|
||||
function CleveRoids.AddWhitelistedCommand(cmd)
|
||||
if not cmd or cmd == "" then return end
|
||||
cmd = string.lower(cmd)
|
||||
-- Auto-prepend "/" if missing
|
||||
if string.sub(cmd, 1, 1) ~= "/" then
|
||||
cmd = "/" .. cmd
|
||||
end
|
||||
local whitelist = GetWhitelistedCommands()
|
||||
whitelist[cmd] = true
|
||||
end
|
||||
|
||||
function CleveRoids.RemoveWhitelistedCommand(cmd)
|
||||
if not cmd or cmd == "" then return end
|
||||
cmd = string.lower(cmd)
|
||||
if string.sub(cmd, 1, 1) ~= "/" then
|
||||
cmd = "/" .. cmd
|
||||
end
|
||||
local whitelist = GetWhitelistedCommands()
|
||||
whitelist[cmd] = nil
|
||||
end
|
||||
|
||||
function CleveRoids.IsWhitelistedCommand(cmd)
|
||||
if not cmd or cmd == "" then return false end
|
||||
cmd = string.lower(cmd)
|
||||
if string.sub(cmd, 1, 1) ~= "/" then
|
||||
cmd = "/" .. cmd
|
||||
end
|
||||
local whitelist = GetWhitelistedCommands()
|
||||
return whitelist[cmd] == true
|
||||
end
|
||||
|
||||
function CleveRoids.GetWhitelistedCommandsList()
|
||||
local whitelist = GetWhitelistedCommands()
|
||||
local list = {}
|
||||
for cmd, _ in pairs(whitelist) do
|
||||
table.insert(list, cmd)
|
||||
end
|
||||
table.sort(list)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Commands that can have conditionals without actions
|
||||
-- e.g., /petattack [harm] or /target [exists,hp:<=20]
|
||||
local COMMANDS_NO_ACTION_NEEDED = {
|
||||
@@ -679,7 +732,7 @@ local function validateLine(line, lineNum)
|
||||
local _, _, cmd = safeStringFind(line, "^(/[a-z]+%d*)")
|
||||
if cmd then
|
||||
local lowerCmd = string.lower(cmd)
|
||||
if not VALID_COMMANDS[lowerCmd] then
|
||||
if not VALID_COMMANDS[lowerCmd] and not CleveRoids.IsWhitelistedCommand(lowerCmd) then
|
||||
table.insert(localErrors, {
|
||||
type = ERROR_TYPES.INVALID_COMMAND,
|
||||
line = lineNum,
|
||||
@@ -1102,6 +1155,11 @@ end
|
||||
-- Slash command
|
||||
SLASH_MACROCHECK1 = "/macrocheck"
|
||||
SlashCmdList.MACROCHECK = function(msg)
|
||||
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then
|
||||
CleveRoids.Print("Macro syntax checker is disabled. Enable with /cleveroid macrocheck 1")
|
||||
return
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
msg = safeTrim(msg or "")
|
||||
|
||||
@@ -1117,4 +1175,6 @@ SlashCmdList.MACROCHECK = function(msg)
|
||||
end
|
||||
end
|
||||
|
||||
CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all")
|
||||
if not CleveRoidMacros or CleveRoidMacros.macrocheck ~= 0 then
|
||||
CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all")
|
||||
end
|
||||
|
||||
+137
-1
@@ -85,7 +85,43 @@
|
||||
- Reads from NampowerSettings addon when available
|
||||
- Falls back to CVars when addon not present
|
||||
|
||||
Current version: v2.33.0
|
||||
Aura Cancel Functions (v2.34+):
|
||||
- CancelPlayerAuraSlot(auraSlot) - Cancel buff/debuff by raw 0-based aura slot
|
||||
- CancelPlayerAuraSpellId(spellId, [ignoreMissing]) - Cancel buff/debuff by spell ID
|
||||
Pass ignoreMissing=1 to skip aura-slot presence check (for server-side
|
||||
overflow buffs that have no client aura slot in the 48-slot array)
|
||||
|
||||
GUID String Fix & Talent Helper (v2.35+):
|
||||
- GetUnitData/GetUnitField now return GUID-type fields (charm, summon, charmedBy,
|
||||
summonedBy, createdBy, target, persuaded, channelObject) as hex strings
|
||||
(e.g., "0x0600000012345678") instead of raw numbers to avoid Lua 64-bit precision loss
|
||||
- LearnTalentRank(talentPage, talentIndex, rank) - Learn a specific talent rank directly
|
||||
|
||||
Player State Functions (v2.36+):
|
||||
- PlayerIsMoving() - Returns 1 if player is moving, nil if stationary
|
||||
- PlayerIsRooted() - Returns 1 if player is rooted, nil if not
|
||||
- PlayerIsSwimming() - Returns 1 if player is swimming, nil if not
|
||||
|
||||
Unit Token Cast & Mouseover (v2.37+):
|
||||
- CastSpellByName(spellName, unitToken) - 2nd param now accepts unit token strings
|
||||
(e.g. "mouseover", "party1", "focus") in addition to GUIDs and 1 (self).
|
||||
Enables soft-casting on unit tokens without changing the player's visible target.
|
||||
- SetMouseoverUnit(unitToken) - Programmatically set the mouseover unit.
|
||||
Previously only available via SuperWoW; now also provided by Nampower.
|
||||
|
||||
Spell Duration & Enhanced SPELL_START (v2.38+):
|
||||
- GetSpellDuration(spellId, [ignoreModifiers]) - Returns duration in ms.
|
||||
For channeling spells: channel duration. For others: first aura effect duration.
|
||||
Pass ignoreModifiers=1 to get base duration (ignores talents/buffs).
|
||||
Returns 0 if spell has no duration, nil if spellId invalid.
|
||||
- SPELL_START_SELF / SPELL_START_OTHER now include two new parameters:
|
||||
arg7 = duration (ms) - channel duration for channeling spells, 0 otherwise
|
||||
arg8 = spellType - 0=Normal, 1=Channeling, 2=Autorepeating
|
||||
Use spellType to distinguish channeling spells (SPELL_START_OTHER carries both
|
||||
cast-time and channeling spells since there is no separate SPELL_CHANNEL_START
|
||||
packet for other players).
|
||||
|
||||
Current version: v2.38.0
|
||||
]]
|
||||
|
||||
local _G = _G or getfenv(0)
|
||||
@@ -242,6 +278,27 @@ API.VERSION_REQUIREMENTS = {
|
||||
-- v2.33+ - UPDATE_DURATION spellId parameter and GetCastInfo guid fix
|
||||
["DurationEventSpellId"] = { 2, 33, 0 },
|
||||
["GetCastInfoGuidFix"] = { 2, 33, 0 },
|
||||
|
||||
-- v2.34+ - Aura cancel functions
|
||||
["CancelPlayerAuraSlot"] = { 2, 34, 0, "CancelPlayerAuraSlot" },
|
||||
["CancelPlayerAuraSpellId"] = { 2, 34, 0, "CancelPlayerAuraSpellId" },
|
||||
|
||||
-- v2.35+ - GUID string fix and talent helper
|
||||
["GuidStringFormat"] = { 2, 35, 0 }, -- GetUnitData/GetUnitField return GUID fields as hex strings
|
||||
["LearnTalentRank"] = { 2, 35, 0, "LearnTalentRank" },
|
||||
|
||||
-- v2.36+ - Player state functions
|
||||
["PlayerIsMoving"] = { 2, 36, 0, "PlayerIsMoving" },
|
||||
["PlayerIsRooted"] = { 2, 36, 0, "PlayerIsRooted" },
|
||||
["PlayerIsSwimming"] = { 2, 36, 0, "PlayerIsSwimming" },
|
||||
|
||||
-- v2.37+ - Unit token cast support and SetMouseoverUnit
|
||||
["CastSpellByNameUnitToken"]= { 2, 37, 0 }, -- CastSpellByName accepts unit token strings as 2nd param
|
||||
["SetMouseoverUnit"] = { 2, 37, 0, "SetMouseoverUnit" },
|
||||
|
||||
-- v2.38+ - GetSpellDuration and enhanced SPELL_START parameters
|
||||
["GetSpellDuration"] = { 2, 38, 0, "GetSpellDuration" },
|
||||
["SpellStartSpellType"] = { 2, 38, 0 }, -- duration + spellType params added to SPELL_START events
|
||||
}
|
||||
|
||||
-- Check if a specific feature is available
|
||||
@@ -382,6 +439,27 @@ local function InitializeFeatures()
|
||||
f.hasDurationEventSpellId = API.HasFeature("DurationEventSpellId")
|
||||
f.hasGetCastInfoGuidFix = API.HasFeature("GetCastInfoGuidFix")
|
||||
|
||||
-- v2.34+ Aura cancel functions
|
||||
f.hasCancelPlayerAuraSlot = API.HasFeature("CancelPlayerAuraSlot")
|
||||
f.hasCancelPlayerAuraSpellId = API.HasFeature("CancelPlayerAuraSpellId")
|
||||
|
||||
-- v2.35+ GUID string format and talent helper
|
||||
f.hasGuidStringFormat = API.HasFeature("GuidStringFormat")
|
||||
f.hasLearnTalentRank = API.HasFeature("LearnTalentRank")
|
||||
|
||||
-- v2.36+ Player state functions
|
||||
f.hasPlayerIsMoving = API.HasFeature("PlayerIsMoving")
|
||||
f.hasPlayerIsRooted = API.HasFeature("PlayerIsRooted")
|
||||
f.hasPlayerIsSwimming = API.HasFeature("PlayerIsSwimming")
|
||||
|
||||
-- v2.37+ Unit token cast support and SetMouseoverUnit
|
||||
f.hasCastSpellByNameUnitToken = API.HasFeature("CastSpellByNameUnitToken")
|
||||
f.hasSetMouseoverUnit = API.HasFeature("SetMouseoverUnit")
|
||||
|
||||
-- v2.38+ GetSpellDuration and enhanced SPELL_START parameters
|
||||
f.hasGetSpellDuration = API.HasFeature("GetSpellDuration")
|
||||
f.hasSpellStartSpellType = API.HasFeature("SpellStartSpellType")
|
||||
|
||||
-- Runtime detection for enhanced spell functions (verify by testing)
|
||||
if f.hasEnhancedSpellFunctions and GetSpellTexture then
|
||||
local success, result = pcall(function()
|
||||
@@ -2982,5 +3060,63 @@ function API.GetSpellPower(mode)
|
||||
return _G.GetSpellPower(mode)
|
||||
end
|
||||
|
||||
-- Get duration of a spell in milliseconds (v2.38+)
|
||||
-- For channeling spells: returns the channel duration.
|
||||
-- For non-channeling spells: returns the first aura effect duration.
|
||||
-- spellId: spell ID to look up
|
||||
-- ignoreModifiers: pass 1 to ignore talent/buff modifiers (returns base duration)
|
||||
-- Returns: duration in ms (0 if spell has no duration), or nil if spellId invalid
|
||||
function API.GetSpellDuration(spellId, ignoreModifiers)
|
||||
if not API.features.hasGetSpellDuration or not _G.GetSpellDuration then
|
||||
return nil
|
||||
end
|
||||
return _G.GetSpellDuration(spellId, ignoreModifiers)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- AURA CANCEL FUNCTIONS (v2.34+)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Cancel a player aura by raw 0-based aura slot (v2.34+)
|
||||
-- auraSlot: 0-31 for buffs, 32-47 for debuffs
|
||||
function API.CancelPlayerAuraSlot(auraSlot)
|
||||
if not API.features.hasCancelPlayerAuraSlot or not _G.CancelPlayerAuraSlot then
|
||||
return false
|
||||
end
|
||||
_G.CancelPlayerAuraSlot(auraSlot)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Cancel a player aura by spell ID (v2.34+)
|
||||
-- spellId: the spell ID to cancel
|
||||
-- ignoreMissing: pass 1 to skip aura-slot presence check (useful for buff-capped hidden auras)
|
||||
function API.CancelPlayerAuraSpellId(spellId, ignoreMissing)
|
||||
if not API.features.hasCancelPlayerAuraSpellId or not _G.CancelPlayerAuraSpellId then
|
||||
return false
|
||||
end
|
||||
_G.CancelPlayerAuraSpellId(spellId, ignoreMissing)
|
||||
return true
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- TALENT HELPER (v2.35+)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Learn a specific talent rank directly (v2.35+)
|
||||
-- talentPage: 1-3 (talent tab)
|
||||
-- talentIndex: 1-32 (talent position within tab)
|
||||
-- rank: 1-5 (rank to learn)
|
||||
function API.LearnTalentRank(talentPage, talentIndex, rank)
|
||||
if not API.features.hasLearnTalentRank or not _G.LearnTalentRank then
|
||||
return false
|
||||
end
|
||||
if not talentPage or not talentIndex or not rank then return false end
|
||||
if talentPage < 1 or talentPage > 3 then return false end
|
||||
if talentIndex < 1 or talentIndex > 32 then return false end
|
||||
if rank < 1 or rank > 5 then return false end
|
||||
_G.LearnTalentRank(talentPage, talentIndex, rank)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Expose API globally for other addons
|
||||
_G.CleveRoidsNampowerAPI = API
|
||||
|
||||
@@ -154,63 +154,56 @@ These always evaluate against the player. Cannot be redirected with `@unit`.
|
||||
|
||||
| Conditional | Description | No | Multi | Example | NP | Addon |
|
||||
|-------------|-------------|:--:|:-----:|---------|:--:|-------|
|
||||
| ***State & Modifiers*** | | | | | | |
|
||||
| `mod` | Modifier key pressed | ✅ | ✅ | `[mod:alt/ctrl]` | | |
|
||||
| `combat` | In combat (player default, or unit) | ✅ | ✅ | `[combat]` `[combat:target]` | | |
|
||||
| `form` / `stance` | Shapeshift form or stance index | ✅ | ✅ | `[form:1/3]` `[stance:2]` | | |
|
||||
| `stealth` | In stealth (Rogue/Druid) | ✅ | — | `[stealth]` | | |
|
||||
| `group` | Player is in group type | ✅ | ✅ | `[group:party/raid]` | | |
|
||||
| `resting` | In rest area | ✅ | — | `[resting]` | | |
|
||||
| `swimming` | Swimming (Druid only) | ✅ | — | `[swimming]` | | |
|
||||
| `moving` | Moving / speed % | ✅ | ✅ | `[moving]` `[moving:>100&<200]` | | MonkeySpeed (speed %) |
|
||||
| `zone` | Current zone name | ✅ | ✅ | `[zone:Ironforge/Stormwind]` | | |
|
||||
| ***Resources*** | | | | | | |
|
||||
| `myhp` | Player HP % | — | ✅ | `[myhp:<30]` `[myhp:>20&<50]` | | |
|
||||
| `myrawhp` | Player raw HP value | — | ✅ | `[myrawhp:>1000]` | | |
|
||||
| `myhplost` | Player HP deficit (max - current) | — | ✅ | `[myhplost:>500]` | | |
|
||||
| `mypower` | Player mana/rage/energy % | — | ✅ | `[mypower:>50]` | | |
|
||||
| `myrawpower` | Player raw power value | — | ✅ | `[myrawpower:>=40]` | | |
|
||||
| `mypowerlost` | Player power deficit | — | ✅ | `[mypowerlost:>200]` | | |
|
||||
| `druidmana` | Druid mana while shapeshifted | — | ✅ | `[druidmana:>=500]` | | |
|
||||
| `combo` | Combo points | ✅ | ✅ | `[combo:>=4]` | | |
|
||||
| `stat` | Player stat value | ✅ | ✅ | `[stat:agi>100]` `[stat:ap>1000]` | | |
|
||||
| `mylevel` | Player level | — | ✅ | `[mylevel:=60]` | | |
|
||||
| ***Player Auras*** | | | | | | |
|
||||
| `mybuff` | Player has buff (with time/stacks) | ✅ | ✅ | `[mybuff:Thorns<5]` `[nomybuff:MotW/GotW]` | | |
|
||||
| `mydebuff` | Player has debuff (with time/stacks) | ✅ | ✅ | `[mydebuff:Curse_of_Agony]` | | |
|
||||
| `mybuffcount` | Player buff slot count (32 max) | ✅ | ✅ | `[mybuffcount:>15]` `[nomybuffcount:>28]` | | |
|
||||
| `mybuffcapped` | Player buff bar full (32) | ✅ | — | `[nomybuffcapped]` | v2.20 | |
|
||||
| `mydebuffcapped` | Player debuff bar full (16) | ✅ | — | `[nomydebuffcapped]` | v2.20 | |
|
||||
| `mycc` | Player has CC effect | ✅ | ✅ | `[mycc:stun/silence]` `[nomycc]` | | |
|
||||
| ***Spells & Cooldowns*** | | | | | | |
|
||||
| `cooldown` | Spell/item CD remaining (ignores GCD) | ✅ | ✅ | `[cooldown:Sprint<5]` | | |
|
||||
| `cdgcd` | Spell/item CD remaining (includes GCD) | ✅ | ✅ | `[cdgcd:Sprint>0]` | | |
|
||||
| `gcd` | GCD is active / time remaining | ✅ | ✅ | `[gcd]` `[gcd:<1]` | | |
|
||||
| `usable` | Spell/item is usable now | ✅ | ✅ | `[usable:Overpower]` | | |
|
||||
| `reactive` | Reactive ability available (on bar) | ✅ | ✅ | `[reactive:Overpower]` | | |
|
||||
| `known` | Spell/talent known (with rank) | ✅ | ✅ | `[known:Berserk]` `[known:Frostbolt>#2]` | | |
|
||||
| `selfcasting` | Player is casting or channeling | ✅ | ✅ | `[selfcasting]` `[noselfcasting:Hearthstone]` | | |
|
||||
| `checkcasting` | True if NOT casting (specific spell) | — | ✅ | `[checkcasting]` `[checkcasting:Frostbolt]` | | |
|
||||
| `channeled` | Player is channeling | ✅ | ✅ | `[channeled]` `[channeled:Arcane_Missiles]` | | |
|
||||
| `checkcasting` | True if NOT casting (specific spell) | — | ✅ | `[checkcasting]` `[checkcasting:Frostbolt]` | | |
|
||||
| `checkchanneled` | True if NOT channeling (specific spell) | — | ✅ | `[checkchanneled]` `[checkchanneled:Evocation]` | | |
|
||||
| `spellcasttime` | Spell cast time from tooltip (real-time) | ✅ | ✅ | `[spellcasttime:>1.5]` `[spellcasttime:Frostbolt<2]` | | |
|
||||
| `queuedspell` | Spell is queued | ✅ | ✅ | `[queuedspell]` `[queuedspell:Fireball]` | v2.12 | |
|
||||
| `onswingpending` | On-swing spell pending | ✅ | — | `[onswingpending]` | v2.12 | |
|
||||
| ***Equipment*** | | | | | | |
|
||||
| `combat` | In combat (player default, or unit) | ✅ | ✅ | `[combat]` `[combat:target]` | | |
|
||||
| `combo` | Combo points | ✅ | ✅ | `[combo:>=4]` | | |
|
||||
| `cooldown` | Spell/item CD remaining (ignores GCD) | ✅ | ✅ | `[cooldown:Sprint<5]` | | |
|
||||
| `druidmana` | Druid mana while shapeshifted | — | ✅ | `[druidmana:>=500]` | | |
|
||||
| `equipped` | Item or weapon type equipped | ✅ | ✅ | `[equipped:Daggers/Swords]` | | |
|
||||
| `mhimbue` | Main hand has temporary imbue | ✅ | ✅ | `[mhimbue:Instant_Poison<300]` `[mhimbue:>#5]` | | |
|
||||
| `ohimbue` | Off hand has temporary imbue | ✅ | ✅ | `[noohimbue]` `[ohimbue:Crippling_Poison]` | | |
|
||||
| `pet` | Has active pet (with family) | ✅ | ✅ | `[pet]` `[pet:Cat/Wolf]` | | |
|
||||
| ***Melee Tracking*** | | | | | | |
|
||||
| `lastswing` | Player melee swing type/timing | ✅ | ✅ | `[lastswing:dodge]` `[lastswing:<2]` | v2.24 | |
|
||||
| `incominghit` | Incoming attack type/timing | ✅ | ✅ | `[incominghit:crushing]` `[noincominghit:crit]` | v2.24 | |
|
||||
| `swingtimer` | Swing timer % elapsed | ✅ | ✅ | `[swingtimer:>80]` | | SP_SwingTimer |
|
||||
| `stimer` | Alias for swingtimer | ✅ | ✅ | `[stimer:<15]` | | SP_SwingTimer |
|
||||
| `slamclip` / `noslamclip` | Slam now will/won't clip auto-attack | ✅ | — | `[noslamclip]` | | SP_SwingTimer |
|
||||
| `nextslamclip` / `nonextslamclip` | Instant now will/won't cause next Slam to clip | ✅ | — | `[nonextslamclip]` | | SP_SwingTimer |
|
||||
| ***Target Existence*** | | | | | | |
|
||||
| `form` / `stance` | Shapeshift form or stance index | ✅ | ✅ | `[form:1/3]` `[stance:2]` | | |
|
||||
| `gcd` | GCD is active / time remaining | ✅ | ✅ | `[gcd]` `[gcd:<1]` | | |
|
||||
| `group` | Player is in group type | ✅ | ✅ | `[group:party/raid]` | | |
|
||||
| `hastarget` | Player has a target | — | — | `[hastarget]` | | |
|
||||
| `incominghit` | Incoming attack type/timing | ✅ | ✅ | `[incominghit:crushing]` `[noincominghit:crit]` | v2.24 | |
|
||||
| `known` | Spell/talent known (with rank) | ✅ | ✅ | `[known:Berserk]` `[known:Frostbolt>#2]` | | |
|
||||
| `lastswing` | Player melee swing type/timing | ✅ | ✅ | `[lastswing:dodge]` `[lastswing:<2]` | v2.24 | |
|
||||
| `mhimbue` | Main hand has temporary imbue | ✅ | ✅ | `[mhimbue:Instant_Poison<300]` `[mhimbue:>#5]` | | |
|
||||
| `mod` | Modifier key pressed | ✅ | ✅ | `[mod:alt/ctrl]` | | |
|
||||
| `moving` | Moving / speed % | ✅ | ✅ | `[moving]` `[moving:>100&<200]` | | MonkeySpeed (speed %) |
|
||||
| `mybuff` | Player has buff (with time/stacks) | ✅ | ✅ | `[mybuff:Thorns<5]` `[nomybuff:MotW/GotW]` | | |
|
||||
| `mybuffcapped` | Player buff bar full (32) | ✅ | — | `[nomybuffcapped]` | v2.20 | |
|
||||
| `mybuffcount` | Player buff slot count (32 max) | ✅ | ✅ | `[mybuffcount:>15]` `[nomybuffcount:>28]` | | |
|
||||
| `mycc` | Player has CC effect | ✅ | ✅ | `[mycc:stun/silence]` `[nomycc]` | | |
|
||||
| `mydebuff` | Player has debuff (with time/stacks) | ✅ | ✅ | `[mydebuff:Curse_of_Agony]` | | |
|
||||
| `mydebuffcapped` | Player debuff bar full (16) | ✅ | — | `[nomydebuffcapped]` | v2.20 | |
|
||||
| `myhp` | Player HP % | — | ✅ | `[myhp:<30]` `[myhp:>20&<50]` | | |
|
||||
| `myhplost` | Player HP deficit (max - current) | — | ✅ | `[myhplost:>500]` | | |
|
||||
| `mylevel` | Player level | — | ✅ | `[mylevel:=60]` | | |
|
||||
| `mypower` | Player mana/rage/energy % | — | ✅ | `[mypower:>50]` | | |
|
||||
| `mypowerlost` | Player power deficit | — | ✅ | `[mypowerlost:>200]` | | |
|
||||
| `myrawhp` | Player raw HP value | — | ✅ | `[myrawhp:>1000]` | | |
|
||||
| `myrawpower` | Player raw power value | — | ✅ | `[myrawpower:>=40]` | | |
|
||||
| `nextslamclip` / `nonextslamclip` | Instant now will/won't cause next Slam to clip | ✅ | — | `[nonextslamclip]` | | SP_SwingTimer |
|
||||
| `notarget` | Player has no target | — | — | `[notarget]` | | |
|
||||
| `ohimbue` | Off hand has temporary imbue | ✅ | ✅ | `[noohimbue]` `[ohimbue:Crippling_Poison]` | | |
|
||||
| `onswingpending` | On-swing spell pending | ✅ | — | `[onswingpending]` | v2.12 | |
|
||||
| `pet` | Has active pet (with family) | ✅ | ✅ | `[pet]` `[pet:Cat/Wolf]` | | |
|
||||
| `queuedspell` | Spell is queued | ✅ | ✅ | `[queuedspell]` `[queuedspell:Fireball]` | v2.12 | |
|
||||
| `reactive` | Reactive ability available (on bar) | ✅ | ✅ | `[reactive:Overpower]` | | |
|
||||
| `resting` | In rest area | ✅ | — | `[resting]` | | |
|
||||
| `selfcasting` | Player is casting or channeling | ✅ | ✅ | `[selfcasting]` `[noselfcasting:Hearthstone]` | | |
|
||||
| `slamclip` / `noslamclip` | Slam now will/won't clip auto-attack | ✅ | — | `[noslamclip]` | | SP_SwingTimer |
|
||||
| `spellcasttime` | Spell cast time from tooltip (real-time) | ✅ | ✅ | `[spellcasttime:>1.5]` `[spellcasttime:Frostbolt<2]` | | |
|
||||
| `stat` | Player stat value | ✅ | ✅ | `[stat:agi>100]` `[stat:ap>1000]` | | |
|
||||
| `stealth` | In stealth (Rogue/Druid) | ✅ | — | `[stealth]` | | |
|
||||
| `stimer` | Alias for swingtimer | ✅ | ✅ | `[stimer:<15]` | | SP_SwingTimer |
|
||||
| `swimming` | Swimming (Druid only) | ✅ | — | `[swimming]` | | |
|
||||
| `swingtimer` | Swing timer % elapsed | ✅ | ✅ | `[swingtimer:>80]` | | SP_SwingTimer |
|
||||
| `usable` | Spell/item is usable now | ✅ | ✅ | `[usable:Overpower]` | | |
|
||||
| `zone` | Current zone name | ✅ | ✅ | `[zone:Ironforge/Stormwind]` | | |
|
||||
|
||||
### Target Conditionals (default @target)
|
||||
|
||||
@@ -218,62 +211,53 @@ These default to checking the current target. Most can be redirected with `@unit
|
||||
|
||||
| Conditional | Description | No | Multi | Example | NP | Addon |
|
||||
|-------------|-------------|:--:|:-----:|---------|:--:|-------|
|
||||
| ***Existence & Disposition*** | | | | | | |
|
||||
| `exists` | Unit exists | ✅ | — | `[@mouseover,exists]` | | |
|
||||
| `alive` | Unit is alive | ✅ | — | `[alive]` | | |
|
||||
| `dead` | Unit is dead | ✅ | — | `[dead]` | | |
|
||||
| `help` | Unit is friendly | ✅ | — | `[help]` | | |
|
||||
| `harm` | Unit is hostile | ✅ | — | `[harm]` | | |
|
||||
| `isplayer` | Unit is a player | ✅ | — | `[isplayer]` | | |
|
||||
| `isnpc` | Unit is an NPC | ✅ | — | `[isnpc]` | | |
|
||||
| ***Resources*** | | | | | | |
|
||||
| `hp` | Target HP % | — | ✅ | `[hp:<20]` `[hp:>30&<70]` | | |
|
||||
| `rawhp` | Target raw HP value | — | ✅ | `[rawhp:>5000]` | | |
|
||||
| `hplost` | Target HP deficit (max - current) | — | ✅ | `[hplost:>1000]` | | |
|
||||
| `power` | Target mana/rage/energy % | — | ✅ | `[power:<30]` | | |
|
||||
| `rawpower` | Target raw power value | — | ✅ | `[rawpower:>500]` | | |
|
||||
| `powerlost` | Target power deficit | — | ✅ | `[powerlost:>100]` | | |
|
||||
| `powertype` | Target power type | ✅ | ✅ | `[powertype:mana/rage]` | | |
|
||||
| `level` | Target level (skull = 63) | — | ✅ | `[level:>60]` | | |
|
||||
| ***Classification*** | | | | | | |
|
||||
| `class` | Target class (players only) | ✅ | ✅ | `[class:Warrior/Priest]` | | |
|
||||
| `type` | Creature type | ✅ | ✅ | `[type:Undead/Beast]` | | |
|
||||
| `name` | Exact name match (case-insensitive) | ✅ | ✅ | `[name:Onyxia]` `[noname:Critter/Dummy]` | | |
|
||||
| ***Target Auras*** | | | | | | |
|
||||
| `behind` | Behind target / count behind | ✅ | ✅ | `[behind]` `[behind:>=2]` | | UnitXP_SP3 |
|
||||
| `buff` | Target has buff (with time/stacks) | ✅ | ✅ | `[buff:Shield>#3]` `[nobuff:Renew/Rejuv]` | | |
|
||||
| `debuff` | Target has debuff (with time/stacks) | ✅ | ✅ | `[debuff:Moonfire<4]` `[nodebuff:Rake]` | | |
|
||||
| `buffcapped` | Target buff bar full (32) | ✅ | — | `[nobuffcapped]` | v2.20 | |
|
||||
| `debuffcapped` | Target debuff bar full | ✅ | — | `[nodebuffcapped]` | v2.20 | |
|
||||
| `cc` | Target has CC effect | ✅ | ✅ | `[cc:stun/fear]` `[nocc:polymorph]` | | |
|
||||
| `cursive` | GUID-based debuff tracking (time remaining) | ✅ | ✅ | `[cursive:Rake<3]` `[nocursive:Rip]` | | Cursive |
|
||||
| ***Casting*** | | | | | | |
|
||||
| `casting` | Target is casting (specific spell) | ✅ | ✅ | `[casting]` `[casting:Heal]` | | |
|
||||
| `casttime` | Target cast time remaining (seconds) | ✅ | ✅ | `[casttime:<0.5]` | | |
|
||||
| `cc` | Target has CC effect | ✅ | ✅ | `[cc:stun/fear]` `[nocc:polymorph]` | | |
|
||||
| `channeltime` | Target channel time remaining (seconds) | ✅ | ✅ | `[channeltime:<0.5]` | | |
|
||||
| ***Relationship*** | | | | | | |
|
||||
| `party` | Unit is in your party | ✅ | ✅ | `[party]` `[party:focus]` | | |
|
||||
| `raid` | Unit is in your raid | ✅ | ✅ | `[raid]` `[raid:mouseover]` | | |
|
||||
| `member` | Target is in party or raid | ✅ | — | `[member]` | | |
|
||||
| `targeting` | Unit is targeting you or a tank | ✅ | ✅ | `[targeting:player]` `[notargeting:tank]` | | pfUI (for tank) |
|
||||
| `istank` | Unit is marked as tank | ✅ | — | `[istank]` `[@focus,istank]` | | pfUI |
|
||||
| `tag` | Target is tapped by anyone | ✅ | — | `[tag]` `[notag]` | | |
|
||||
| `mytag` | Target is tapped by you | ✅ | — | `[mytag]` `[nomytag]` | | |
|
||||
| `othertag` | Target is tapped by someone else | ✅ | — | `[othertag]` `[noothertag]` | | |
|
||||
| ***Range & Position*** | | | | | | |
|
||||
| `class` | Target class (players only) | ✅ | ✅ | `[class:Warrior/Priest]` | | |
|
||||
| `cursive` | GUID-based debuff tracking (time remaining) | ✅ | ✅ | `[cursive:Rake<3]` `[nocursive:Rip]` | | Cursive |
|
||||
| `dead` | Unit is dead | ✅ | — | `[dead]` | | |
|
||||
| `debuff` | Target has debuff (with time/stacks) | ✅ | ✅ | `[debuff:Moonfire<4]` `[nodebuff:Rake]` | | |
|
||||
| `debuffcapped` | Target debuff bar full | ✅ | — | `[nodebuffcapped]` | v2.20 | |
|
||||
| `distance` | Distance in yards | ✅ | ✅ | `[distance:<40]` | | UnitXP_SP3 |
|
||||
| `meleerange` | In melee range / count in melee | ✅ | ✅ | `[meleerange]` `[meleerange:>1]` | | UnitXP_SP3 |
|
||||
| `inrange` | In spell range / count in range | ✅ | ✅ | `[inrange:Charge]` `[inrange:Multi-Shot>1]` | | UnitXP_SP3 (count) |
|
||||
| `outrange` | Out of spell range / count out | ✅ | ✅ | `[outrange:Charge]` `[outrange:Charge>0]` | | UnitXP_SP3 (count) |
|
||||
| `insight` | In line of sight / count in LoS | ✅ | ✅ | `[insight]` `[insight:>0]` | | UnitXP_SP3 |
|
||||
| `behind` | Behind target / count behind | ✅ | ✅ | `[behind]` `[behind:>=2]` | | UnitXP_SP3 |
|
||||
| `multiscan` | Scan enemies by priority, soft-cast | — | ✅ | `[multiscan:nearest]` `[multiscan:skull]` | | UnitXP_SP3 |
|
||||
| ***Immunity*** | | | | | | |
|
||||
| `exists` | Unit exists | ✅ | — | `[@mouseover,exists]` | | |
|
||||
| `harm` | Unit is hostile | ✅ | — | `[harm]` | | |
|
||||
| `help` | Unit is friendly | ✅ | — | `[help]` | | |
|
||||
| `hp` | Target HP % | — | ✅ | `[hp:<20]` `[hp:>30&<70]` | | |
|
||||
| `hplost` | Target HP deficit (max - current) | — | ✅ | `[hplost:>1000]` | | |
|
||||
| `immune` | Target is immune (school or CC type) | ✅ | ✅ | `[noimmune:fire]` `[noimmune:stun]` | | |
|
||||
| `inrange` | In spell range / count in range | ✅ | ✅ | `[inrange:Charge]` `[inrange:Multi-Shot>1]` | | UnitXP_SP3 (count) |
|
||||
| `insight` | In line of sight / count in LoS | ✅ | ✅ | `[insight]` `[insight:>0]` | | UnitXP_SP3 |
|
||||
| `isnpc` | Unit is an NPC | ✅ | — | `[isnpc]` | | |
|
||||
| `isplayer` | Unit is a player | ✅ | — | `[isplayer]` | | |
|
||||
| `istank` | Unit is marked as tank | ✅ | — | `[istank]` `[@focus,istank]` | | pfUI |
|
||||
| `level` | Target level (skull = 63) | — | ✅ | `[level:>60]` | | |
|
||||
| `meleerange` | In melee range / count in melee | ✅ | ✅ | `[meleerange]` `[meleerange:>1]` | | UnitXP_SP3 |
|
||||
| `member` | Target is in party or raid | ✅ | — | `[member]` | | |
|
||||
| `multiscan` | Scan enemies by priority, soft-cast | — | ✅ | `[multiscan:nearest]` `[multiscan:skull]` | | UnitXP_SP3 |
|
||||
| `mytag` | Target is tapped by you | ✅ | — | `[mytag]` `[nomytag]` | | |
|
||||
| `name` | Exact name match (case-insensitive) | ✅ | ✅ | `[name:Onyxia]` `[noname:Critter/Dummy]` | | |
|
||||
| `othertag` | Target is tapped by someone else | ✅ | — | `[othertag]` `[noothertag]` | | |
|
||||
| `outrange` | Out of spell range / count out | ✅ | ✅ | `[outrange:Charge]` `[outrange:Charge>0]` | | UnitXP_SP3 (count) |
|
||||
| `party` | Unit is in your party | ✅ | ✅ | `[party]` `[party:focus]` | | |
|
||||
| `power` | Target mana/rage/energy % | — | ✅ | `[power:<30]` | | |
|
||||
| `powerlost` | Target power deficit | — | ✅ | `[powerlost:>100]` | | |
|
||||
| `powertype` | Target power type | ✅ | ✅ | `[powertype:mana/rage]` | | |
|
||||
| `raid` | Unit is in your raid | ✅ | ✅ | `[raid]` `[raid:mouseover]` | | |
|
||||
| `rawhp` | Target raw HP value | — | ✅ | `[rawhp:>5000]` | | |
|
||||
| `rawpower` | Target raw power value | — | ✅ | `[rawpower:>500]` | | |
|
||||
| `resisted` | Last spell was resisted | ✅ | ✅ | `[resisted]` `[resisted:full/partial]` | | |
|
||||
| ***Addon Integrations*** | | | | | | |
|
||||
| `tag` | Target is tapped by anyone | ✅ | — | `[tag]` `[notag]` | | |
|
||||
| `targeting` | Unit is targeting you or a tank | ✅ | ✅ | `[targeting:player]` `[notargeting:tank]` | | pfUI (for tank) |
|
||||
| `threat` | Threat % on target (100 = pull) | ✅ | ✅ | `[threat:>80]` | | TWThreat |
|
||||
| `ttk` | Time to kill target (seconds) | ✅ | ✅ | `[ttk:<10]` | | TimeToKill |
|
||||
| `tte` | Time to execute threshold (seconds) | ✅ | ✅ | `[tte:<5]` | | TimeToKill |
|
||||
| `ttk` | Time to kill target (seconds) | ✅ | ✅ | `[ttk:<10]` | | TimeToKill |
|
||||
| `type` | Creature type | ✅ | ✅ | `[type:Undead/Beast]` | | |
|
||||
|
||||
### Multi-Unit Count Mode
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Author: DWG, _brain, Torio, yani9o, Avitasia
|
||||
## Title: [Super]CleveRoidMacros
|
||||
## Notes: /cleveroid for settings
|
||||
## Version: 2.0
|
||||
## Version: 2.1
|
||||
## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar, Cursive, XPerl, LunaUnitFrames, DragonflightReloaded, -Dragonflight3
|
||||
## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools
|
||||
Localization.lua
|
||||
@@ -43,4 +43,5 @@ Extensions\Mouseover\Cursive.lua
|
||||
|
||||
Extensions\Tooltip\Generic.lua
|
||||
Extensions\CursiveCustomSpells.lua
|
||||
Extensions\OverflowBuffFrame.lua
|
||||
|
||||
|
||||
+265
-16
@@ -328,7 +328,8 @@ do
|
||||
local isUpdatingMouseover = false
|
||||
|
||||
local function apply(unit)
|
||||
if CleveRoids.hasSuperwow and _G.SetMouseoverUnit then
|
||||
if _G.SetMouseoverUnit then
|
||||
-- Available via SuperWoW or Nampower v2.37+
|
||||
-- Set flag so UPDATE_MOUSEOVER_UNIT handler knows we triggered this
|
||||
CleveRoids.__mo.selfTriggered = true
|
||||
-- Use empty string instead of nil to properly clear mouseover.
|
||||
@@ -658,7 +659,7 @@ lib.guidToName = lib.guidToName or {}
|
||||
-- duration calculation, and miss/dodge/parry/resist/immune detection.
|
||||
|
||||
-- Enhanced tables (populated by pfUI or standalone Nampower handlers)
|
||||
lib.ownDebuffs = lib.ownDebuffs or {} -- [targetGUID][spellName] = {startTime, duration, texture, rank, slot}
|
||||
lib.ownDebuffs = lib.ownDebuffs or {} -- [targetGUID][spellName] = {startTime, duration, texture, rank, spellId}
|
||||
lib.ownSlots = lib.ownSlots or {} -- [targetGUID][slot] = spellName (LEGACY - empty when pfUI 7.6+ is active)
|
||||
lib.allSlots = lib.allSlots or {} -- [targetGUID][slot] = {spellName, casterGuid, isOurs} (LEGACY - empty when pfUI 7.6+ is active)
|
||||
lib.slotOwnership = lib.slotOwnership or {} -- [targetGUID][auraSlot] = {casterGuid, spellName, spellId, isOurs} (pfUI 7.6+ GetUnitField edition)
|
||||
@@ -667,6 +668,16 @@ lib.pendingCasts = lib.pendingCasts or {} -- [targetGUID][spellName] = {caster
|
||||
lib.recentMisses = lib.recentMisses or {} -- [targetGUID][spellName] = {time, spellId, targetName, reason} for miss/dodge/parry detection
|
||||
lib.iconCache = lib.iconCache or {} -- [spellId] = texture (shared with pfUI 7.6 or standalone)
|
||||
|
||||
-- Buff tracking tables (parallel to debuff tables, standalone Nampower mode only)
|
||||
-- Buff tables are NOT linked to pfUI — pfUI has no public buff tracking tables.
|
||||
-- They remain empty when hasPfUIEnhanced=true since all buff event handlers are gated by it.
|
||||
lib.ownBuffCasts = lib.ownBuffCasts or {} -- [targetGUID][buffName] = {startTime, duration, spellId, casterGuid}
|
||||
-- Player's own buff casts on self or others (AURA_CAST events)
|
||||
lib.allBuffAuras = lib.allBuffAuras or {} -- [targetGUID][buffName][casterGuid] = {startTime, duration, rank}
|
||||
-- All buffs from all casters on any unit (confirmed by BUFF_ADDED events)
|
||||
lib.pendingBuffCasts = lib.pendingBuffCasts or {} -- [targetGUID][spellId] = {casterGuid, duration, spellName, time}
|
||||
-- Temp storage from AURA_CAST_ON_OTHER, consumed by BUFF_ADDED_OTHER
|
||||
|
||||
-- Flag indicating whether enhanced pfUI tracking is available
|
||||
lib.hasPfUIEnhanced = false
|
||||
lib.hasStandaloneNampower = false
|
||||
@@ -702,7 +713,7 @@ function lib:HasEnhancedPfUILibdebuff()
|
||||
end
|
||||
|
||||
-- Verify Nampower version based on pfUI version:
|
||||
-- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.27.2+
|
||||
-- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.37.0+
|
||||
-- - pfUI 7.4.3 to 7.5.x (legacy): requires Nampower v2.26+
|
||||
if not GetNampowerVersion then return false end
|
||||
local npMajor, npMinor, npPatch = GetNampowerVersion()
|
||||
@@ -712,10 +723,9 @@ function lib:HasEnhancedPfUILibdebuff()
|
||||
local isPfUI76 = pfUI.libdebuff_slot_ownership ~= nil
|
||||
|
||||
if isPfUI76 then
|
||||
-- pfUI 7.6+ requires Nampower v2.27.2+
|
||||
-- pfUI 7.6+ requires Nampower v2.37.0+
|
||||
if npMajor < 2 then return false end
|
||||
if npMajor == 2 and npMinor < 27 then return false end
|
||||
if npMajor == 2 and npMinor == 27 and npPatch < 2 then return false end
|
||||
if npMajor == 2 and npMinor < 37 then return false end
|
||||
else
|
||||
-- Legacy pfUI 7.4.3-7.5.x requires Nampower v2.26+
|
||||
if npMajor < 2 or (npMajor == 2 and npMinor < 26) then return false end
|
||||
@@ -733,7 +743,7 @@ function lib:HasEnhancedPfUILibdebuff()
|
||||
end
|
||||
|
||||
-- Check if pfUI v7.6+ with enhanced cast tracking is available
|
||||
-- pfUI 7.6 requires Nampower v2.27.2+ and exposes additional tables
|
||||
-- pfUI 7.6+ requires Nampower v2.37.0+ and exposes additional tables
|
||||
function lib:HasPfUI76()
|
||||
if not pfUI then return false end
|
||||
|
||||
@@ -744,13 +754,12 @@ function lib:HasPfUI76()
|
||||
if v.major < 7 then return false end
|
||||
if v.major == 7 and (v.minor or 0) < 6 then return false end
|
||||
|
||||
-- Verify Nampower v2.27.2+ (pfUI 7.6 hard requirement)
|
||||
-- Verify Nampower v2.38.0+ (pfUI 7.6+ hard requirement, bumped from 2.37 in pfUI update 2026-02-18)
|
||||
if not GetNampowerVersion then return false end
|
||||
local npMajor, npMinor, npPatch = GetNampowerVersion()
|
||||
npPatch = npPatch or 0
|
||||
if npMajor < 2 then return false end
|
||||
if npMajor == 2 and npMinor < 27 then return false end
|
||||
if npMajor == 2 and npMinor == 27 and npPatch < 2 then return false end
|
||||
if npMajor == 2 and npMinor < 38 then return false end
|
||||
|
||||
-- Verify the new tables exist
|
||||
if not pfUI.libdebuff_casts then return false end
|
||||
@@ -1184,6 +1193,58 @@ function lib:CleanupStaleTrackingData()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean ownBuffCasts (player-cast buffs on targets)
|
||||
if lib.ownBuffCasts then
|
||||
for guid, buffs in pairs(lib.ownBuffCasts) do
|
||||
for buffName, data in pairs(buffs) do
|
||||
local elapsed = now - (data.startTime or 0)
|
||||
local dur = data.duration or 0
|
||||
-- Remove if expired (duration > 0 and past end time) or stale (no duration and old)
|
||||
if (dur > 0 and elapsed > dur) or (dur <= 0 and elapsed > staleTime) then
|
||||
buffs[buffName] = nil
|
||||
end
|
||||
end
|
||||
if not next(buffs) then
|
||||
lib.ownBuffCasts[guid] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean allBuffAuras (all-caster buff tracking)
|
||||
if lib.allBuffAuras then
|
||||
for guid, buffs in pairs(lib.allBuffAuras) do
|
||||
for buffName, casters in pairs(buffs) do
|
||||
for cGuid, data in pairs(casters) do
|
||||
local elapsed = now - (data.startTime or 0)
|
||||
local dur = data.duration or 0
|
||||
if (dur > 0 and elapsed > dur) or (dur <= 0 and elapsed > staleTime) then
|
||||
casters[cGuid] = nil
|
||||
end
|
||||
end
|
||||
if not next(casters) then
|
||||
buffs[buffName] = nil
|
||||
end
|
||||
end
|
||||
if not next(buffs) then
|
||||
lib.allBuffAuras[guid] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean pendingBuffCasts (short-lived correlation data, 2 second TTL)
|
||||
if lib.pendingBuffCasts then
|
||||
for guid, spells in pairs(lib.pendingBuffCasts) do
|
||||
for spellId, data in pairs(spells) do
|
||||
if (now - (data.time or 0)) > 2 then
|
||||
spells[spellId] = nil
|
||||
end
|
||||
end
|
||||
if not next(spells) then
|
||||
lib.pendingBuffCasts[guid] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get the caster GUID for a debuff on a target
|
||||
@@ -2996,10 +3057,16 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
|
||||
if not lib.detectedJudgementDebuffIDs[debuffSpellID] then
|
||||
lib.detectedJudgementDebuffIDs[debuffSpellID] = true
|
||||
lib.judgementSpells[debuffSpellID] = true -- Add to refresh list
|
||||
-- Also register as shared debuff so [debuff]/[nodebuff] fallback detection works
|
||||
-- for custom Turtle WoW spell IDs not in the hardcoded sharedDebuffs table
|
||||
if not lib.sharedDebuffs[debuffSpellID] then
|
||||
lib.sharedDebuffs[debuffSpellID] = 10 -- Standard judgement duration
|
||||
CleveRoids.InvalidateSpellNameCache()
|
||||
end
|
||||
|
||||
if debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
_string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh list",
|
||||
_string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh + shared list",
|
||||
debuffName, debuffSpellID, pending.castSpellID)
|
||||
)
|
||||
end
|
||||
@@ -3685,6 +3752,13 @@ if CleveRoids.hasNampower then
|
||||
ev:RegisterEvent("DEBUFF_REMOVED_OTHER")
|
||||
ev:RegisterEvent("UNIT_DIED") -- Instant cleanup on target death
|
||||
|
||||
-- BUFF_ADDED/REMOVED events require v2.30+ for auraSlot and state args
|
||||
if npMajor > 2 or (npMajor == 2 and npMinor >= 30) then
|
||||
ev:RegisterEvent("BUFF_ADDED_OTHER")
|
||||
ev:RegisterEvent("BUFF_REMOVED_SELF")
|
||||
ev:RegisterEvent("BUFF_REMOVED_OTHER")
|
||||
end
|
||||
|
||||
if CleveRoids.debug then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Registered UNIT_DIED for instant cleanup (v2.26+)")
|
||||
end
|
||||
@@ -4043,7 +4117,13 @@ ev:SetScript("OnEvent", function()
|
||||
})
|
||||
|
||||
-- If this is a Judgement spell cast by a Paladin, schedule a scan to find the actual debuff ID
|
||||
if CleveRoids.playerClass == "PALADIN" and lib.judgementSpells[spellID] then
|
||||
-- Check by ID (known judgement debuffs) OR by name (Turtle WoW custom cast spell IDs)
|
||||
local isJudgementCast = lib.judgementSpells[spellID]
|
||||
if not isJudgementCast and CleveRoids.playerClass == "PALADIN" then
|
||||
local castName = _SpellInfo(spellID)
|
||||
isJudgementCast = castName and _string_find(castName, "^Judgement")
|
||||
end
|
||||
if CleveRoids.playerClass == "PALADIN" and isJudgementCast then
|
||||
table.insert(lib.pendingJudgements, {
|
||||
timestamp = GetTime(),
|
||||
castSpellID = spellID,
|
||||
@@ -4370,19 +4450,38 @@ ev:SetScript("OnEvent", function()
|
||||
-- and these handlers return early (pfUI handles it).
|
||||
|
||||
elseif event == "SPELL_START_SELF" or event == "SPELL_START_OTHER" then
|
||||
-- v2.38+: For player's own channeling spells, pre-store the accurate server channel duration
|
||||
-- BEFORE the pfUI early-return so SPELLCAST_CHANNEL_START (which fires later) can use it.
|
||||
-- arg8 == 1 means spellType=Channeling; arg7 is the channel duration in ms.
|
||||
if event == "SPELL_START_SELF" and arg8 == 1 and arg7 and arg7 > 0 then
|
||||
CleveRoids._v238ChannelDuration = arg7 / 1000
|
||||
CleveRoids._v238ChannelSpellId = arg2
|
||||
end
|
||||
-- pfUI 7.6 manages castTracking via its own SPELL_START handler
|
||||
if lib.hasPfUI76 then return end
|
||||
|
||||
local spellId = arg2
|
||||
local casterGuid = arg3
|
||||
local castTime = arg6 -- milliseconds
|
||||
local castTime = arg6 -- cast time in milliseconds
|
||||
-- v2.38+: arg7 = channel duration (ms, channeling spells only), arg8 = spellType (0=Normal,1=Channeling,2=Autorepeating)
|
||||
local channelDuration = arg7
|
||||
local spellType = arg8
|
||||
|
||||
if not casterGuid or not spellId then return end
|
||||
|
||||
local spellName = SpellInfo and SpellInfo(spellId)
|
||||
local icon = lib:GetCachedIcon(spellId)
|
||||
local now = GetTime()
|
||||
local durationSec = castTime and (castTime / 1000) or 0
|
||||
|
||||
-- For channeling spells (v2.38+: spellType==1), use the channel duration.
|
||||
-- For normal cast-time spells, use castTime. Fall back to castTime if params unavailable.
|
||||
local durationMs
|
||||
if spellType == 1 and channelDuration and channelDuration > 0 then
|
||||
durationMs = channelDuration
|
||||
else
|
||||
durationMs = castTime or 0
|
||||
end
|
||||
local durationSec = durationMs / 1000
|
||||
|
||||
CleveRoids.castTracking[casterGuid] = {
|
||||
spellID = spellId,
|
||||
@@ -4394,9 +4493,10 @@ ev:SetScript("OnEvent", function()
|
||||
}
|
||||
|
||||
if CleveRoids.debug then
|
||||
local typeStr = spellType == 1 and "channel" or (spellType == 2 and "auto" or "cast")
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
string.format("|cff00ccff[SPELL_START]|r %s (ID:%d) cast by %s - %.1fs",
|
||||
spellName or "Unknown", spellId, casterGuid, durationSec)
|
||||
string.format("|cff00ccff[SPELL_START]|r %s (ID:%d) cast by %s - %.1fs [%s]",
|
||||
spellName or "Unknown", spellId, casterGuid, durationSec, typeStr)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -4876,6 +4976,125 @@ ev:SetScript("OnEvent", function()
|
||||
end
|
||||
end
|
||||
|
||||
-- NAMPOWER v2.30+ BUFF_ADDED_OTHER - Confirm pending AURA_CAST_ON_OTHER data as buff
|
||||
elseif event == "BUFF_ADDED_OTHER" then
|
||||
if lib.hasPfUIEnhanced then return end
|
||||
|
||||
local guid = CleveRoids.NormalizeGUID(arg1)
|
||||
local spellId = arg3
|
||||
local state = arg7 -- 0=added, 1=removed, 2=modified
|
||||
|
||||
if not guid or not spellId then return end
|
||||
|
||||
-- Consume pending AURA_CAST data for this buff
|
||||
local pending = lib.pendingBuffCasts[guid] and lib.pendingBuffCasts[guid][spellId]
|
||||
if pending then
|
||||
local spellName = pending.spellName or (SpellInfo and SpellInfo(spellId))
|
||||
if spellName then
|
||||
local casterGuid = pending.casterGuid
|
||||
local duration = pending.duration
|
||||
local now = GetTime()
|
||||
|
||||
-- allBuffAuras: confirmed buff from any caster
|
||||
lib.allBuffAuras[guid] = lib.allBuffAuras[guid] or {}
|
||||
lib.allBuffAuras[guid][spellName] = lib.allBuffAuras[guid][spellName] or {}
|
||||
lib.allBuffAuras[guid][spellName][casterGuid or "unknown"] = {
|
||||
startTime = now,
|
||||
duration = duration or 0,
|
||||
rank = 0,
|
||||
}
|
||||
|
||||
-- ownBuffCasts: only if player is the caster
|
||||
local _, playerGuidRaw = UnitExists("player")
|
||||
local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw)
|
||||
if playerGuid and casterGuid == playerGuid then
|
||||
lib.ownBuffCasts[guid] = lib.ownBuffCasts[guid] or {}
|
||||
lib.ownBuffCasts[guid][spellName] = {
|
||||
startTime = now,
|
||||
duration = duration or 0,
|
||||
spellId = spellId,
|
||||
casterGuid = casterGuid,
|
||||
}
|
||||
end
|
||||
end
|
||||
lib.pendingBuffCasts[guid][spellId] = nil
|
||||
end
|
||||
|
||||
-- NAMPOWER v2.30+ BUFF_REMOVED_SELF - Player's own buffs removed
|
||||
elseif event == "BUFF_REMOVED_SELF" then
|
||||
if lib.hasPfUIEnhanced then return end
|
||||
|
||||
local spellId = arg3
|
||||
local state = arg7
|
||||
if not spellId then return end
|
||||
if state == 2 then return end -- Stack decrease only, not full removal
|
||||
|
||||
local spellName = SpellInfo and SpellInfo(spellId)
|
||||
local _, playerGuidRaw = UnitExists("player")
|
||||
local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw)
|
||||
|
||||
if spellName and playerGuid then
|
||||
if lib.ownBuffCasts[playerGuid] then
|
||||
lib.ownBuffCasts[playerGuid][spellName] = nil
|
||||
if not next(lib.ownBuffCasts[playerGuid]) then
|
||||
lib.ownBuffCasts[playerGuid] = nil
|
||||
end
|
||||
end
|
||||
if lib.allBuffAuras[playerGuid] then
|
||||
lib.allBuffAuras[playerGuid][spellName] = nil
|
||||
if not next(lib.allBuffAuras[playerGuid]) then
|
||||
lib.allBuffAuras[playerGuid] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Also prune from OverflowBuffs (existing system)
|
||||
if spellId and CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[spellId] then
|
||||
CleveRoids.OverflowBuffs[spellId] = nil
|
||||
end
|
||||
|
||||
-- NAMPOWER v2.30+ BUFF_REMOVED_OTHER - Buffs removed from other units
|
||||
elseif event == "BUFF_REMOVED_OTHER" then
|
||||
if lib.hasPfUIEnhanced then return end
|
||||
|
||||
local guid = CleveRoids.NormalizeGUID(arg1)
|
||||
local spellId = arg3
|
||||
local state = arg7
|
||||
if not guid or not spellId then return end
|
||||
if state == 2 then return end -- Stack decrease only, not full removal
|
||||
|
||||
local spellName = SpellInfo and SpellInfo(spellId)
|
||||
if spellName then
|
||||
if lib.allBuffAuras[guid] then
|
||||
lib.allBuffAuras[guid][spellName] = nil
|
||||
if not next(lib.allBuffAuras[guid]) then
|
||||
lib.allBuffAuras[guid] = nil
|
||||
end
|
||||
end
|
||||
if lib.ownBuffCasts[guid] then
|
||||
lib.ownBuffCasts[guid][spellName] = nil
|
||||
if not next(lib.ownBuffCasts[guid]) then
|
||||
lib.ownBuffCasts[guid] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clean pending correlation data
|
||||
if lib.pendingBuffCasts[guid] then
|
||||
lib.pendingBuffCasts[guid][spellId] = nil
|
||||
if not next(lib.pendingBuffCasts[guid]) then
|
||||
lib.pendingBuffCasts[guid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Also clean AllCasterAuraTracking (existing system)
|
||||
if CleveRoids.AllCasterAuraTracking[guid] then
|
||||
CleveRoids.AllCasterAuraTracking[guid][spellId] = nil
|
||||
if not next(CleveRoids.AllCasterAuraTracking[guid]) then
|
||||
CleveRoids.AllCasterAuraTracking[guid] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- NAMPOWER v2.26+ UNIT_DIED - Instant cleanup on target death
|
||||
elseif event == "UNIT_DIED" then
|
||||
local guid = arg1
|
||||
@@ -4908,11 +5127,41 @@ ev:SetScript("OnEvent", function()
|
||||
lib.recentMisses[guid] = nil
|
||||
end
|
||||
|
||||
-- Clean up buff tracking tables
|
||||
if lib.ownBuffCasts[guid] then
|
||||
lib.ownBuffCasts[guid] = nil
|
||||
end
|
||||
if lib.allBuffAuras[guid] then
|
||||
lib.allBuffAuras[guid] = nil
|
||||
end
|
||||
if lib.pendingBuffCasts[guid] then
|
||||
lib.pendingBuffCasts[guid] = nil
|
||||
end
|
||||
|
||||
-- Clean up cast tracking for this unit (they can't be casting if dead)
|
||||
if not lib.hasPfUI76 and CleveRoids.castTracking[guid] then
|
||||
CleveRoids.castTracking[guid] = nil
|
||||
end
|
||||
|
||||
-- Clean up overflow buff tracking (death removes all buffs)
|
||||
local _, playerGUID = UnitExists("player")
|
||||
if playerGUID and guid == playerGUID then
|
||||
-- Player died: clear all overflow buff entries and reset cap status
|
||||
if CleveRoids.OverflowBuffs then
|
||||
for k in pairs(CleveRoids.OverflowBuffs) do
|
||||
CleveRoids.OverflowBuffs[k] = nil
|
||||
end
|
||||
end
|
||||
if CleveRoids.AuraCapStatus then
|
||||
CleveRoids.AuraCapStatus.playerBuffCapped = false
|
||||
CleveRoids.AuraCapStatus.playerDebuffCapped = false
|
||||
end
|
||||
end
|
||||
-- Clean up all-caster aura tracking for the dead unit
|
||||
if CleveRoids.AllCasterAuraTracking and CleveRoids.AllCasterAuraTracking[guid] then
|
||||
CleveRoids.AllCasterAuraTracking[guid] = nil
|
||||
end
|
||||
|
||||
-- Clean up GUID to name mapping (after 5 seconds to allow final lookups)
|
||||
-- Actually, keep it for a bit in case we need it for immunity detection
|
||||
-- lib.guidToName[guid] = nil
|
||||
|
||||
Reference in New Issue
Block a user