Merge pull request #110 from jrc13245/enhanced

Enhanced
This commit is contained in:
Torio
2026-02-09 14:43:51 -05:00
committed by GitHub
6 changed files with 746 additions and 658 deletions
+12 -2
View File
@@ -467,8 +467,18 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID)
-- If combo points are 0, try multiple fallback sources
if comboPoints == 0 then
-- First, try lastComboPoints (don't reset immediately - let it persist)
if CleveRoids.lastComboPoints > 0 then
-- PRIMARY: Check SPELL_CAST_EVENT capture (most reliable - client-side pre-server)
local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellID]
if pending and pending.comboPoints and pending.comboPoints > 0 then
comboPoints = pending.comboPoints
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff88[TrackComboByID]|r Using SPELL_CAST_EVENT CP: %d for spell ID %d",
comboPoints, spellID)
)
end
-- Fallback: lastComboPoints (don't reset immediately - let it persist)
elseif CleveRoids.lastComboPoints > 0 then
comboPoints = CleveRoids.lastComboPoints
-- Don't reset here - let it persist for multiple events
if CleveRoids.debug then
+15 -8
View File
@@ -126,9 +126,14 @@ function Extension.HookPfUILibdebuff()
-- If so, pfUI handles combo durations and Carnage internally - we only override on mismatch
local hasPfUI76 = CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil)
-- pfUI 7.6+ handles all durations internally - no hooks needed
if hasPfUI76 then
Extension.DLOG("Skipped all libdebuff hooks (pfUI 7.6+ handles internally)")
return false
end
-- Hook GetDuration if it exists
-- pfUI's GetDuration signature: function(effect, rank) where effect is spell NAME
-- pfUI 7.6+ already handles combo durations via GetStoredComboPoints(), so only override on mismatch
if pflib.GetDuration and not Extension.pfLibDebuffHooked then
local originalGetDuration = pflib.GetDuration
@@ -171,10 +176,8 @@ function Extension.HookPfUILibdebuff()
Extension.DLOG("Hooked pfUI.api.libdebuff.GetDuration (mismatch-only mode)")
end
-- Hook AddEffect if it exists
-- NOTE: In pfUI 7.6+, AddEffect is LEGACY (rarely called - events handle tracking)
-- We only hook for pre-7.6 compatibility and rank checking fallback
if pflib.AddEffect and not Extension.pfLibAddEffectHooked and not hasPfUI76 then
-- Hook AddEffect if it exists (pre-7.6 only - 7.6+ returns early above)
if pflib.AddEffect and not Extension.pfLibAddEffectHooked then
local originalAddEffect = pflib.AddEffect
pflib.AddEffect = function(self, unit, unitlevel, effect, duration, caster)
@@ -258,9 +261,7 @@ function Extension.HookPfUILibdebuff()
end
Extension.pfLibAddEffectHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect (pre-7.6 legacy mode)")
elseif hasPfUI76 then
Extension.DLOG("Skipped AddEffect hook (pfUI 7.6+ handles internally)")
Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect")
end
-- Hook UnitDebuff to return Carnage override duration to display code
@@ -343,11 +344,17 @@ function Extension.HookPfUILibdebuff()
end
-- Synchronize combo durations to pfUI's libdebuff objects
-- NOTE: pfUI 7.6+ handles combo durations internally - skip sync entirely
function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
if not pfUI or not pfUI.api or not pfUI.api.libdebuff then
return
end
-- pfUI 7.6+ handles all durations internally
if CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil) then
return
end
-- Get unit name from GUID
local unitName = nil
local unitLevel = 0
+93 -26
View File
@@ -835,6 +835,23 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName)
if not targetData then return nil, nil end
local now = GetTime()
-- Direct spell ID lookup: O(1) hash table access instead of iterating all entries
local searchID = tonumber(searchName)
if searchID then
local auraData = targetData[searchID]
if auraData and auraData.start and auraData.duration then
local remaining = auraData.duration + auraData.start - now
if remaining > 0 then
return remaining, auraData.casterGuid
else
targetData[searchID] = nil
end
end
return nil, nil
end
-- Name-based search (existing behavior)
local searchLower = string.lower(searchName)
for spellId, auraData in pairs(targetData) do
@@ -867,10 +884,16 @@ local HITINFO_GLANCING = 16384 -- 0x4000
local HITINFO_CRUSHING = 32768 -- 0x8000
local HITINFO_LEFTSWING = 4 -- 0x4 (Off-hand attack)
-- VictimState values
-- VictimState values (from AUTO_ATTACK / SPELL_GO events)
local VICTIMSTATE_UNAFFECTED = 0 -- Generic miss (seen with HITINFO_MISS)
local VICTIMSTATE_NORMAL = 1 -- Hit landed
local VICTIMSTATE_DODGE = 2
local VICTIMSTATE_PARRY = 3
local VICTIMSTATE_INTERRUPT = 4
local VICTIMSTATE_BLOCKS = 5
local VICTIMSTATE_EVADES = 6
local VICTIMSTATE_IS_IMMUNE = 7
local VICTIMSTATE_DEFLECTS = 8
-- Aura cap status bitfield
local AURA_CAP_BUFF_FULL = 1
@@ -2991,7 +3014,9 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
end
-- PERFORMANCE: Cache lowercased search name to avoid repeated string.lower calls
local searchName = args.name and _string_lower(args.name)
-- Support spell ID matching: [mybuff:17941] matches by spell ID instead of name
local searchID = args.name and tonumber(args.name)
local searchName = not searchID and args.name and _string_lower(args.name) or nil
-- Primary search: BUFFS if isbuff==true, DEBUFFS if isbuff==false
while true do
@@ -3014,12 +3039,20 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
if not texture then break end
if current_spellID and searchName then
-- PERFORMANCE: Use cached lowercase spell name lookup
local lowerName = GetLowercaseSpellName(current_spellID)
if lowerName and lowerName == searchName then
found = true
break
if current_spellID then
if searchID then
-- Spell ID matching: [mybuff:17941]
if current_spellID == searchID then
found = true
break
end
elseif searchName then
-- PERFORMANCE: Use cached lowercase spell name lookup
local lowerName = GetLowercaseSpellName(current_spellID)
if lowerName and lowerName == searchName then
found = true
break
end
end
end
@@ -3027,7 +3060,7 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
end
-- Overflow handling: when searching DEBUFFS on non-players, also scan BUFFS
if not isbuff and not isPlayer and not found and searchName then
if not isbuff and not isPlayer and not found and (searchID or searchName) then
i = 1
while true do
local texture
@@ -3038,11 +3071,18 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
if not texture then break end
if current_spellID then
-- PERFORMANCE: Use cached lowercase spell name lookup
local lowerName = GetLowercaseSpellName(current_spellID)
if lowerName and lowerName == searchName then
found = true
break
if searchID then
if current_spellID == searchID then
found = true
break
end
elseif searchName then
-- PERFORMANCE: Use cached lowercase spell name lookup
local lowerName = GetLowercaseSpellName(current_spellID)
if lowerName and lowerName == searchName then
found = true
break
end
end
end
@@ -3148,12 +3188,15 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
-- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap")
args.name = string.gsub(args.name, "_", " ")
-- Support spell ID matching: [debuff:9904] matches by spell ID instead of name
local searchID = tonumber(args.name)
local found = false
local texture, stacks, spellID, remaining
local i
-- PERFORMANCE: For non-SuperWoW, early return if no texture registered
if not CleveRoids.hasSuperwow and not CleveRoids.auraTextures[args.name] then
if not searchID and not CleveRoids.hasSuperwow and not CleveRoids.auraTextures[args.name] then
return false
end
@@ -3170,7 +3213,13 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
if not guid then return false end
-- PERFORMANCE: Use cached spell name -> ID mapping instead of iterating every call
local matchingSpellIDs = GetSpellIDsForName(args.name) or {}
-- For spell ID input, use the ID directly instead of name->ID lookup
local matchingSpellIDs
if searchID then
matchingSpellIDs = {searchID}
else
matchingSpellIDs = GetSpellIDsForName(args.name) or {}
end
-- Check tracking table for ANY rank of this spell: Did player cast this? Is timer valid?
if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then
@@ -3266,9 +3315,14 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
if not tex then break end
if debuffSpellID then
-- PERFORMANCE: Use cached spell name lookup
local baseName, fullName = GetSpellNames(debuffSpellID)
if baseName and (baseName == args.name or fullName == args.name) then
local matched = false
if searchID then
matched = (debuffSpellID == searchID)
else
local baseName, fullName = GetSpellNames(debuffSpellID)
matched = baseName and (baseName == args.name or fullName == args.name)
end
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
@@ -3292,9 +3346,14 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
if not tex then break end
if buffSpellID then
-- PERFORMANCE: Use cached spell name lookup
local baseName, fullName = GetSpellNames(buffSpellID)
if baseName and (baseName == args.name or fullName == args.name) then
local matched = false
if searchID then
matched = (buffSpellID == searchID)
else
local baseName, fullName = GetSpellNames(buffSpellID)
matched = baseName and (baseName == args.name or fullName == args.name)
end
if matched then
-- IMPORTANT: Only use fallback for SHARED debuffs
local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false
if isShared then
@@ -3318,8 +3377,12 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, false)
if not texture then break end
if CleveRoids.hasSuperwow then
-- PERFORMANCE: Use cached spell name lookup
if searchID then
if spellID == searchID then
found = true
break
end
elseif CleveRoids.hasSuperwow then
local baseName, fullName = GetSpellNames(spellID)
if baseName and (baseName == args.name or fullName == args.name) then
found = true
@@ -3339,8 +3402,12 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, true)
if not texture then break end
if CleveRoids.hasSuperwow then
-- PERFORMANCE: Use cached spell name lookup
if searchID then
if spellID == searchID then
found = true
break
end
elseif CleveRoids.hasSuperwow then
local baseName, fullName = GetSpellNames(spellID)
if baseName and (baseName == args.name or fullName == args.name) then
found = true
+47 -3
View File
@@ -4894,13 +4894,12 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
)
end
-- Also sync to pfUI if it's loaded
if pfUI and pfUI.api and pfUI.api.libdebuff then
-- Also sync to pfUI if it's loaded (pre-7.6 only)
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
local targetLevel = UnitLevel("target") or 0
if targetName then
-- Refresh in pfUI's tracking (by name)
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player")
if CleveRoids.debug then
@@ -5533,6 +5532,8 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
if event == "SPELL_CAST_EVENT" then
local success = arg1
local spellId = arg2
local castType = arg3
local targetGuid = arg4
-- BUGFIX: Update casting state on spell cast events (for [casting] conditional)
if CleveRoids.UpdateCastingState then
@@ -5542,6 +5543,8 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
if success == 1 then
CleveRoids.lastCastSpell = {
spellId = spellId,
castType = castType,
targetGuid = targetGuid,
timestamp = GetTime()
}
if SpellInfo then
@@ -5550,6 +5553,47 @@ function CleveRoids.Frame:SPELL_CAST_EVENT()
CleveRoids.lastCastSpell.spellName = name
end
end
-- Track pending cast for SPELL_GO correlation (reactive ability detection)
-- Keyed by spellId so concurrent casts don't overwrite each other
CleveRoids.pendingCasts = CleveRoids.pendingCasts or {}
-- Clean up consumed entries older than 5 seconds (lightweight, runs per-cast)
local cleanupTime = GetTime() - 5
for id, entry in pairs(CleveRoids.pendingCasts) do
if entry.consumed and entry.consumedAt and entry.consumedAt < cleanupTime then
CleveRoids.pendingCasts[id] = nil
end
end
CleveRoids.pendingCasts[spellId] = {
castType = castType,
targetGuid = targetGuid,
timestamp = GetTime(),
comboPoints = nil,
}
-- Capture combo points NOW (before server consumes them)
-- SPELL_CAST_EVENT fires client-side, so CP are guaranteed available
if (CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellId)) or
(CleveRoids.FerociousBiteSpellIDs and CleveRoids.FerociousBiteSpellIDs[spellId]) then
local cp = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0
if cp > 0 then
CleveRoids.pendingCasts[spellId].comboPoints = cp
if CleveRoids.debug then
local castSpellName = SpellInfo and SpellInfo(spellId) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff88[SPELL_CAST_EVENT]|r Captured %d CP for %s (ID:%d)",
cp, castSpellName, spellId)
)
end
end
end
else
-- Cast failed - clear pending entry for this spell
if CleveRoids.pendingCasts then
CleveRoids.pendingCasts[spellId] = nil
end
end
end
end
+289 -561
View File
@@ -15,6 +15,14 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
1. Download and extract to `Interface/AddOns/SuperCleveRoidMacros`
2. **Recommended pfUI:** Use [jrc13245/pfUI](https://github.com/jrc13245/pfUI) for full compatibility with macro spell scanning and action bar features
## Known Issues
- Unique macro names required (no blanks, duplicates, or spell names)
- Reactive abilities must be on action bars for detection
- Debuff time-left conditionals only work on own debuffs unless pfUI libdebuff or Cursive has data
- HealComm requires MarcelineVQ's [LunaUnitFrames](https://github.com/MarcelineVQ/LunaUnitFrames) for SuperWoW compatibility
- Macro line length: 261 characters max (MacroLengthWarn extension prevents crashes)
---
## Quick Start
@@ -24,7 +32,6 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
#showtooltip
/cast [mod:alt] Frostbolt; [mod:ctrl] Fire Blast; Blink
```
- **Conditionals** in `[]` brackets, space or comma separated
- **Arguments** use colon: `[mod:alt]`, `[hp:>50]`
- **Negation** with `no` prefix: `[nobuff]`, `[nomod:alt]`
@@ -32,17 +39,27 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
- **Spell names** with spaces: `"Mark of the Wild"` or `Mark_of_the_Wild`
### Multi-Value Logic
| Syntax | Logic | Example | Meaning |
|--------|-------|---------|---------|
| `[buff:X/Y]` | OR | `[buff:Renew/Rejuvenation]` | Has Renew **or** Rejuvenation |
| `[buff:X&Y]` | AND | `[buff:Fortitude&Mark_of_the_Wild]` | Has Fort **and** MotW |
| `[nobuff:X/Y]` | NOR | `[nobuff:Renew/Rejuvenation]` | Missing **both** Renew **and** Rejuvenation |
| `[nobuff:X&Y]` | NAND | `[nobuff:Fortitude&Mark_of_the_Wild]` | Missing **at least one** of Fort or MotW |
> **De Morgan's Law:** Negation flips the operator. `no` + `/`(OR) → AND (must lack all). `no` + `&`(AND) → OR (must lack at least one).
**Multiple instances = AND between groups:**
```lua
[buff:X/Y] -- X OR Y (has either)
[buff:X&Y] -- X AND Y (has both)
[nobuff:X/Y] -- NOT X AND NOT Y (missing both) - operators flip for negation
[mybuff:X/Y mybuff:Z] -- (has X OR Y) AND (has Z)
```
### Comparisons
```lua
[hp:>50] -- Health above 50%
[buff:"Name"<5] -- Less than 5 seconds remaining
[debuff:"Name">#3] -- 3+ stacks (use ># for stacks)
[hp:>50] -- Health above 50%
[hp:>20&<50] -- Health between 20% and 50%
[buff:"Name"<5] -- Less than 5 seconds remaining
[debuff:"Name">#3] -- 3+ stacks (use # for stacks)
```
### Special Prefixes
@@ -54,112 +71,213 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna
---
## Slash Commands
| Command | Cond | Example | Description |
|---------|:----:|---------|-------------|
| `/cast` | ✅ | `/cast [mod:alt] Frostbolt` | Cast spell |
| `/use` | ✅ | `/use [hp:<30] Healing Potion` | Use item by name/ID/slot |
| `/equip` | ✅ | `/equip [nocombat] Fishing Pole` | Equip item |
| `/target` | ✅ | `/target [harm,nodead]` | Target with UnitXP 3D scanning |
| `/castsequence` | ✅ | `/castsequence reset=3/target Spell1, Spell2` | Cast spells in sequence |
| `/castpet` | ✅ | `/castpet [combat] Claw` | Cast pet spell |
| `/startattack` | ✅ | `/startattack [harm]` | Start auto-attack |
| `/stopattack` | ✅ | `/stopattack [noothertag]` | Stop auto-attack |
| `/stopcasting` | ✅ | `/stopcasting [mycc:silence]` | Stop casting |
| `/unqueue` | ✅ | `/unqueue [mod:shift]` | Cancel queued spell (Nampower) |
| `/cleartarget` | ✅ | `/cleartarget [dead]` | Clear target |
| `/cancelaura` | ✅ | `/cancelaura [combat] Ice Block` | Cancel buff (alias: `/unbuff`) |
| `/unshift` | ✅ | `/unshift [myhp:<30]` | Cancel shapeshift form |
| `/stopmacro` | ✅ | `/stopmacro [nocombat]` | Stop ALL macro execution |
| `/skipmacro` | ✅ | `/skipmacro [nogroup]` | Stop current submacro only |
| `/firstaction` | ✅ | `/firstaction [group]` | Priority: stop on first successful `/cast` |
| `/nofirstaction` | ✅ | `/nofirstaction` | Re-enable multi-queue after `/firstaction` |
| `/quickheal` | ✅ | `/quickheal [combat]` | Smart heal (alias: `/qh`, requires QuickHeal) |
| `/petattack` | ✅ | `/petattack [harm]` | Pet attack |
| `/petfollow` | ✅ | `/petfollow [nocombat]` | Pet follow |
| `/petwait` | ✅ | `/petwait [mod:ctrl]` | Pet stay |
| `/petpassive` | ✅ | `/petpassive` | Pet passive mode |
| `/petdefensive` | ✅ | `/petdefensive` | Pet defensive mode |
| `/petaggressive` | ✅ | `/petaggressive [group]` | Pet aggressive mode |
| `/equipmh` | ✅ | `/equipmh [nocombat] Dagger` | Equip to main hand |
| `/equipoh` | ✅ | `/equipoh [nocombat] Dagger` | Equip to off hand |
| `/equip11`-`14` | ✅ | `/equip13 [combat] Trinket` | Equip to ring/trinket slot |
| `/applymain` | ✅ | `/applymain [nomhimbue] Instant Poison` | Apply poison/oil to main hand |
| `/applyoff` | ✅ | `/applyoff [noohimbue] Crippling Poison` | Apply poison/oil to off hand |
| `/retarget` | — | `/retarget` | Clear invalid target, target nearest enemy |
| `/runmacro` | — | `/runmacro MyMacro` | Execute macro by name |
| `/rl` | — | `/rl` | Reload UI |
| `/clearequipqueue` | — | `/clearequipqueue` | Clear equipment swap queue |
| `/equipqueuestatus` | — | `/equipqueuestatus` | Show equipment queue status |
### Priority-Based Macro Evaluation
By default, all `/cast` lines evaluate and may queue spells with Nampower. `/firstaction` enables "first successful cast wins" mode.
```lua
/firstaction
/cast [myrawpower:>48] Shred -- Priority: only one of these
/cast [myrawpower:>40] Claw
/nofirstaction
/cast Tiger's Fury -- Always evaluates alongside
/startattack
```
`/firstaction [cond]` accepts conditionals. Child macros (via `{MacroName}`) inherit priority state.
### UnitXP 3D Enemy Scanning
`/target` with any conditionals automatically uses UnitXP 3D scanning to find enemies in line of sight. Exception: `[help]` without `[harm]` (friendly-only). If no match is found, your original target is preserved.
```lua
/target [name:Onyxia] -- Scan for exact name
/target [nodead,harm] -- Scan for living enemies
/target [cc:stun] -- Scan for stunned enemies
```
---
## Conditionals Reference
All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff]`, `[nohelp]`). Some also have semantic opposites: `help`/`harm`, `isplayer`/`isnpc`, `alive`/`dead`, `inrange`/`outrange`.
All conditionals support negation with `no` prefix unless noted. Some have semantic opposites: `help`/`harm`, `alive`/`dead`, `isplayer`/`isnpc`, `inrange`/`outrange`.
### Modifiers & Player State
| Conditional | Example | Description |
|-------------|---------|-------------|
| `mod` | `[mod:alt/ctrl/shift]` | Modifier key pressed |
| `combat` | `[combat]` `[combat:target]` | In combat (player or unit) |
| `form/stance` | `[form:1]` `[stance:2]` | Shapeshift/stance index |
| `stealth` | `[stealth]` | In stealth (Rogue/Druid) |
| `group` | `[group]` `[group:party/raid]` | Player in group type |
| `resting` | `[resting]` | In rest area |
| `swimming` | `[swimming]` | Can use aquatic form |
| `moving` | `[moving]` `[moving:>100]` | Moving / speed % (MonkeySpeed) |
| `zone` | `[zone:"Ironforge"]` | Current zone name |
**Table columns:**
- **Description** — what the conditional checks
- **No** — supports `no` prefix negation
- **Multi** — supports multi-value OR (`/`), AND (`&`), or comparison operators (`>`, `<`, `>=`, `<=`, `=`, `~=`)
- **NP** — minimum Nampower version required
- **Addon** — addon required or recommended
### Resources
| Conditional | Example | Description |
|-------------|---------|-------------|
| `myhp` | `[myhp:<30]` | Player HP % |
| `myrawhp` | `[myrawhp:>1000]` | Player raw HP value |
| `myhplost` | `[myhplost:>500]` | Player HP lost (max - current) |
| `mypower` | `[mypower:>50]` | Player mana/rage/energy % |
| `myrawpower` | `[myrawpower:>500]` | Player raw power value |
| `mypowerlost` | `[mypowerlost:>200]` | Player power lost |
| `druidmana` | `[druidmana:>=500]` | Druid mana while shapeshifted |
| `combo` | `[combo:>=4]` | Combo points |
| `stat` | `[stat:agi>100]` `[stat:ap>1000]` | Player stats (see below) |
### Player Conditionals (@player)
**Stat types:** `str`, `agi`, `stam`, `int`, `spi`, `ap`, `rap`, `healing`, `armor`, `defense`, `arcane_power`, `fire_power`, `frost_power`, `nature_power`, `shadow_power`, `arcane_res`, `fire_res`, `frost_res`, `nature_res`, `shadow_res`
These always evaluate against the player. Cannot be redirected with `@unit`.
### Buffs & Debuffs
| Conditional | Example | Description |
|-------------|---------|-------------|
| `mybuff` | `[mybuff:"Name"<5]` | Player has buff (with time check) |
| `mydebuff` | `[mydebuff:"Name"]` | Player has debuff |
| `mybuffcount` | `[mybuffcount:>15]` `[nomybuffcount:>28]` | Player buff slot count (32 max) |
| `buff` | `[buff:"Name">#3]` | Target has buff (with stacks) |
| `debuff` | `[debuff:"Sunder">20]` | Target has debuff (with time) |
| `cursive` | `[cursive:Rake<3]` | Cursive addon tracking (GUID-based) |
| 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 | ✅ | — | `[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]` | | |
| `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*** | | | | | | |
| `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*** | | | | | | |
| `hastarget` | Player has a target | — | — | `[hastarget]` | | |
| `notarget` | Player has no target | — | — | `[notarget]` | | |
### Cooldowns & Casting
| Conditional | Example | Description |
|-------------|---------|-------------|
| `cooldown` | `[cooldown:"Spell"<5]` | CD remaining (ignores GCD) |
| `cdgcd` | `[cdgcd:"Spell">0]` | CD remaining (includes GCD) |
| `gcd` | `[gcd]` `[gcd:<1]` | GCD is active / remaining time |
| `usable` | `[usable:"Spell"]` | Spell/item is usable |
| `reactive` | `[reactive:Overpower]` | Reactive ability available |
| `known` | `[known:"Spell">#2]` | Spell/talent known (with rank) |
| `channeled` | `[channeled]` | Currently channeling |
| `channeltime` | `[channeltime:<0.5]` | Channel time remaining (seconds) |
| `selfcasting` | `[selfcasting]` | Player is casting/channeling |
| `casttime` | `[casttime:<0.5]` | Player cast time remaining |
| `checkcasting` | `[checkcasting]` `[checkcasting:Frostbolt]` | NOT casting (specific spell) |
| `checkchanneled` | `[checkchanneled]` | NOT channeling (specific spell) |
| `queuedspell` | `[queuedspell]` `[queuedspell:Fireball]` | Spell queued (Nampower) |
| `onswingpending` | `[onswingpending]` | On-swing spell pending |
### Target Conditionals (default @target)
### Target Checks
| Conditional | Example | Description |
|-------------|---------|-------------|
| `exists` | `[@mouseover,exists]` | Unit exists |
| `alive/dead` | `[alive]` `[dead]` | Alive or dead |
| `help/harm` | `[help]` `[harm]` | Friendly or hostile |
| `hp` | `[hp:<20]` `[hp:>30&<70]` | Target HP % |
| `rawhp` | `[rawhp:>5000]` | Target raw HP value |
| `hplost` | `[hplost:>1000]` | Target HP lost |
| `power` | `[power:<30]` | Target power % |
| `rawpower` | `[rawpower:>500]` | Target raw power value |
| `powerlost` | `[powerlost:>100]` | Target power lost |
| `powertype` | `[powertype:mana/rage/energy]` | Target's power type |
| `level` | `[level:>60]` `[mylevel:=60]` | Unit level (skull = 63) |
| `class` | `[class:Warrior/Priest]` | Target class (players only) |
| `type` | `[type:Undead/Beast]` | Creature type |
| `isplayer` | `[isplayer]` | Target is a player |
| `isnpc` | `[isnpc]` | Target is an NPC |
| `targeting` | `[targeting:player]` `[targeting:tank]` | Unit targeting you / any tank |
| `istank` | `[istank]` `[@focus,istank]` | Unit is marked as tank (pfUI) |
| `casting` | `[casting:"Spell"]` | Unit casting spell |
| `party` | `[party]` `[party:focus]` | Unit in your party |
| `raid` | `[raid]` `[raid:mouseover]` | Unit in your raid |
| `member` | `[member]` | Target in party OR raid |
| `hastarget` | `[hastarget]` | Player has a target |
| `notarget` | `[notarget]` | Player has no target |
| `pet` | `[pet]` `[pet:Cat/Wolf]` | Has pet (with family) |
| `name` | `[name:Onyxia]` | Exact name match (case-insensitive) |
| `tag` | `[tag]` | Target is tapped (by anyone) |
| `notag` | `[notag]` | Target is not tapped |
| `mytag` | `[mytag]` | Target is tapped by you |
| `nomytag` | `[nomytag]` | Target is not tapped by you |
| `othertag` | `[othertag]` | Target is tapped by someone else |
| `noothertag` | `[noothertag]` | Not tapped by others (yours or unclaimed) |
These default to checking the current target. Most can be redirected with `@unit` (e.g., `[@focus,hp:<30]`, `[@mouseover,exists]`).
### Range & Position
| Conditional | Example | Description |
|-------------|---------|-------------|
| `distance` | `[distance:<40]` | Distance in yards (UnitXP) |
| `inrange` | `[inrange:"Spell"]` `[inrange:Spell>N]` | In spell range / count in range |
| `outrange` | `[outrange:"Spell"]` `[outrange:Spell>N]` | Out of spell range / count out |
| `meleerange` | `[meleerange]` `[meleerange:>N]` | In melee range / count in melee |
| `behind` | `[behind]` `[behind:>N]` | Behind target / count behind |
| `insight` | `[insight]` `[insight:>N]` | In line of sight / count in LoS |
| 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*** | | | | | | |
| `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]` | | |
| `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*** | | | | | | |
| `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*** | | | | | | |
| `immune` | Target is immune (school or CC type) | ✅ | ✅ | `[noimmune:fire]` `[noimmune:stun]` | | |
| `resisted` | Last spell was resisted | ✅ | ✅ | `[resisted]` `[resisted:full/partial]` | | |
| ***Addon Integrations*** | | | | | | |
| `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 |
**Multi-Unit Count Mode:** Add operator + number to count enemies matching the condition (requires UnitXP). Operators: `>`, `<`, `>=`, `<=`, `=`, `~=`
### Multi-Unit Count Mode
Range and position conditionals (`meleerange`, `behind`, `insight`, `inrange`, `outrange`) support counting enemies when given an operator + number. Requires UnitXP_SP3. Operators: `>`, `<`, `>=`, `<=`, `=`, `~=`
```lua
/cast [meleerange:>1] Whirlwind -- AoE if 2+ enemies in melee
@@ -168,126 +286,53 @@ All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff
/cast [insight:>0] Arcane Explosion -- AoE if any enemy in LoS
```
### Equipment
| Conditional | Example | Description |
|-------------|---------|-------------|
| `equipped` | `[equipped:Daggers]` | Item/type equipped |
| `mhimbue` | `[mhimbue]` `[mhimbue:Instant_Poison]` | Main-hand has temporary imbue |
| `ohimbue` | `[ohimbue]` `[ohimbue:Crippling_Poison]` | Off-hand has temporary imbue |
### Multiscan (Target Scanning)
Scans enemies and soft-casts without changing your target. Requires UnitXP_SP3. Scanned targets must be in combat with player (exceptions: current target and `@unit` specified in macro).
| Priority | Description |
|----------|-------------|
| `nearest` | Closest enemy |
| `farthest` | Farthest enemy |
| `highesthp` / `lowesthp` | Highest/lowest HP % |
| `highestrawhp` / `lowestrawhp` | Highest/lowest raw HP |
| `markorder` | First mark in kill order (skull→cross→square→moon→triangle→diamond→circle→star) |
| `skull` `cross` `square` `moon` `triangle` `diamond` `circle` `star` | Specific raid mark |
**Imbue Conditionals (Poisons, Oils, Sharpening Stones):**
```lua
[mhimbue] -- Has any temporary imbue
[mhimbue:Instant_Poison] -- Has specific imbue (tooltip match)
[mhimbue:<300] -- Imbue expires in < 300 seconds (5 min)
[mhimbue:Instant_Poison<300] -- Specific imbue with time check
[mhimbue:>#5] -- Has > 5 charges remaining
[nomhimbue] -- No temporary imbue (apply needed)
/cast [multiscan:nearest,nodebuff:Rake] Rake
/cast [multiscan:skull,harm] Eviscerate
/cast [multiscan:nearest,notargeting:tank,harm] Taunt
```
**Note:** Temporary imbues are detected via tooltip time/charge markers, filtering out permanent enchants like Crusader.
### Reference Tables
### CC & Immunity
| Conditional | Example | Description |
|-------------|---------|-------------|
| `cc` | `[cc]` `[cc:stun/fear]` | Target has CC effect |
| `mycc` | `[mycc]` `[mycc:silence]` | Player has CC effect |
| `immune` | `[immune:fire]` `[immune:stun]` | Target IS immune (skip cast) |
| `noimmune` | `[noimmune]` `[noimmune:bleed]` | Target NOT immune (allow cast) |
| `resisted` | `[resisted]` `[resisted:full/partial]` | Last spell was resisted |
**CC Types** (for `[cc]`, `[mycc]`, `[immune]`):
**CC Types:** stun, fear, root, snare/slow, sleep, charm, polymorph, banish, horror, disorient, silence, disarm, daze, freeze, shackle
| Type | Examples |
|------|----------|
| `stun` | Cheap Shot, Kidney Shot, HoJ, Bash, Gouge, Sap |
| `fear` | Fear, Psychic Scream, Howl of Terror |
| `root` | Entangling Roots, Frost Nova |
| `snare` / `slow` | Hamstring, Wing Clip, Crippling Poison |
| `sleep` | Hibernate, Wyvern Sting |
| `charm` | Mind Control, Seduction |
| `polymorph` | Polymorph (all variants) |
| `banish` | Banish |
| `horror` | Death Coil, Intimidating Shout |
| `disorient` | Scatter Shot, Blind |
| `silence` | Silence, Counterspell |
| `disarm` | Disarm |
| `daze` | Daze effects |
| `freeze` | Freeze effects |
| `shackle` | Shackle Undead |
**Loss-of-control** (checked by bare `[cc]`): stun, fear, sleep, charm, polymorph, banish, horror, freeze, disorient, shackle
**Damage Schools:** physical, fire, frost, nature, shadow, arcane, holy, bleed
**Damage Schools** (for `[immune]`): physical, fire, frost, nature, shadow, arcane, holy, bleed
### Addon Integrations
| Conditional | Addon | Example | Description |
|-------------|-------|---------|-------------|
| `swingtimer` | SP_SwingTimer | `[swingtimer:<15]` | Swing % elapsed |
| `stimer` | SP_SwingTimer | `[stimer:>80]` | Alias for swingtimer |
| `threat` | TWThreat | `[threat:>80]` | Threat % (100=pull) |
| `ttk` | TimeToKill | `[ttk:<10]` | Time to kill (seconds) |
| `tte` | TimeToKill | `[tte:<5]` | Time to execute (20% HP) |
| `cursive` | Cursive | `[cursive:Rake>3]` | GUID debuff tracking |
| `moving` | MonkeySpeed | `[moving:>100]` | Speed % (100=normal run) |
| `targeting:tank` | pfUI | `[targeting:tank]` | Target is attacking any tank |
| `istank` | pfUI | `[istank]` | Target is marked as tank |
**Swing/Hit Types** (for `[lastswing]`, `[incominghit]`):
### pfUI Tank Integration
Identify loose mobs in trash pulls by checking if enemies are targeting designated tanks. Works with pfUI's tank marking systems.
**Tank Conditionals:**
| Conditional | Description |
|-------------|-------------|
| `[targeting:tank]` | Target IS attacking any player marked as tank |
| `[notargeting:tank]` | Target is NOT attacking any tank (loose mob!) |
| `[istank]` | Target unit IS marked as tank |
| `[noistank]` | Target unit is NOT marked as tank |
**Setting Up Tanks in pfUI:**
1. **Nameplate Off-Tank Names** (recommended): `/pfui` → Nameplates → Off-Tank Names
- Add tank names separated by `#`: `#TankName1#TankName2#TankName3`
- These names will show different colored nameplates AND work with `[targeting:tank]`
2. **Raid Frame Toggle**: Right-click a player in raid frames → "Toggle as Tank"
- Only works when raid frames are visible
**Example Macros:**
```lua
-- Pick up loose mobs (not targeting any tank)
/cast [multiscan:nearest,notargeting:tank,harm] Taunt
-- Only taunt if mob is targeting you (tank)
/cast [targeting:player] Sunder Armor
-- Assist tanks - attack what they're tanking
/cast [multiscan:nearest,targeting:tank,harm] Sunder Armor
-- Emergency taunt on loose mob hitting a healer
/cast [notargeting:tank,notargeting:player] Taunt
```
**Debug Command:**
```
/cleveroid tankdebug
```
Shows all marked tanks, current target info, and conditional results.
### Warrior Slam Conditionals
For optimizing Slam rotations without clipping auto-attacks:
| Conditional | Description |
|-------------|-------------|
| `noslamclip` | True if Slam NOW won't clip auto-attack |
| `slamclip` | True if Slam NOW WILL clip auto-attack |
| `nonextslamclip` | True if instant NOW won't cause NEXT Slam to clip |
| `nextslamclip` | True if instant NOW WILL cause NEXT Slam to clip |
```lua
/cast [noslamclip] Slam
/cast [slamclip] Heroic Strike -- Use HS when past slam window
/cast [nonextslamclip] Bloodthirst
```
### Auto-Attack Conditionals (Nampower v2.24+)
Track player melee swings and incoming attacks. Requires `NP_EnableAutoAttackEvents=1` CVar.
| Conditional | Example | Description |
|-------------|---------|-------------|
| `lastswing` | `[lastswing]` | Any melee swing in last 5 seconds |
| `lastswing:type` | `[lastswing:crit]` | Last swing was specific type |
| `lastswing:<N` | `[lastswing:<2]` | Last swing was < N seconds ago |
| `nolastswing:type` | `[nolastswing:miss/dodge]` | Last swing was NOT type (AND logic) |
| `incominghit` | `[incominghit]` | Any incoming hit in last 5 seconds |
| `incominghit:type` | `[incominghit:crushing]` | Last incoming hit was type |
| `noincominghit:type` | `[noincominghit:crushing]` | Last hit was NOT type |
**Swing/Hit Types:**
| Type | Description |
|------|-------------|
| `crit` | Critical hit |
@@ -301,165 +346,11 @@ Track player melee swings and incoming attacks. Requires `NP_EnableAutoAttackEve
| `mainhand` / `mh` | Main-hand swing (lastswing only) |
| `hit` | Successful hit (not miss/dodge/parry) |
```lua
/cast [lastswing:dodge] Overpower -- Overpower after enemy dodged
/use [incominghit:crushing] Last Stand -- Emergency CD after crushing blow
/cast [lastswing:crit] Execute -- Execute after crit proc
```
**Stat Types** (for `[stat]`): `str`, `agi`, `stam`, `int`, `spi`, `ap`, `rap`, `healing`, `armor`, `defense`, `arcane_power`, `fire_power`, `frost_power`, `nature_power`, `shadow_power`, `arcane_res`, `fire_res`, `frost_res`, `nature_res`, `shadow_res`
### Aura Cap Conditionals (Nampower v2.20+)
**Aura Capacity:** Player: 32 buffs, 16 debuffs. NPCs: 16 debuff slots + 32 overflow = 48 total.
Track buff/debuff bar capacity. Requires `NP_EnableAuraCastEvents=1` CVar for accurate tracking.
| Conditional | Example | Description |
|-------------|---------|-------------|
| `mybuffcapped` | `[mybuffcapped]` | Player has 32 buffs (full) |
| `nomybuffcapped` | `[nomybuffcapped]` | Player has buff room |
| `mydebuffcapped` | `[mydebuffcapped]` | Player has 16 debuffs (full) |
| `nomydebuffcapped` | `[nomydebuffcapped]` | Player has debuff room |
| `buffcapped` | `[buffcapped]` | Target buff bar is full (32) |
| `nobuffcapped` | `[nobuffcapped]` | Target has buff room |
| `debuffcapped` | `[debuffcapped]` | Target debuff bar is full |
| `nodebuffcapped` | `[nodebuffcapped]` | Target has debuff room |
**Aura Capacity:**
- **Player:** 32 buff slots, 16 debuff slots
- **NPCs:** 16 debuff slots + 32 overflow = 48 total visual debuff capacity
```lua
/cast [nodebuffcapped,nocursive:Rake] Rake -- Only DoT if room on target
/cast [nomybuffcapped] Mark of the Wild -- Only buff self if room
/cast [@focus,nodebuffcapped] Corruption -- Only DoT focus if room
```
### Multiscan (Target Scanning)
Scans enemies and soft-casts without changing your target. Requires UnitXP_SP3.
```lua
/cast [multiscan:nearest,nodebuff:Rake] Rake
/cast [multiscan:skull,harm] Eviscerate
/cast [multiscan:markorder] Sinister Strike
/cast [multiscan:highesthp,noimmune:stun] Cheap Shot
/cast [multiscan:nearest,notargeting:tank,harm] Taunt -- Pick up loose mobs (pfUI)
```
**Priorities:**
| Priority | Description |
|----------|-------------|
| `nearest` | Closest enemy |
| `farthest` | Farthest enemy |
| `highesthp` | Highest HP % |
| `lowesthp` | Lowest HP % |
| `highestrawhp` | Highest raw HP |
| `lowestrawhp` | Lowest raw HP |
| `markorder` | First mark in kill order (skull→cross→square→moon→triangle→diamond→circle→star) |
| `skull`, `cross`, `square`, `moon`, `triangle`, `diamond`, `circle`, `star` | Specific raid mark |
**Note:** Scanned targets must be in combat with player, except current target and `@unit` specified in macro.
---
## Slash Commands
### Commands with Conditional Support
These commands accept `[conditionals]` and use UnitXP 3D enemy scanning when applicable.
| Command | Conditionals | UnitXP Scan | Description |
|---------|:------------:|:-----------:|-------------|
| `/cast [cond] Spell` | ✅ | — | Cast spell with conditionals |
| `/castpet [cond] Spell` | ✅ | — | Cast pet spell |
| `/use [cond] Item` | ✅ | — | Use item by name/ID/slot |
| `/equip [cond] Item` | ✅ | — | Equip item (same as /use) |
| `/target [cond]` | ✅ | ✅ | Target with conditionals + enemy scan |
| `/startattack [cond]` | ✅ | — | Start auto-attack if conditions met |
| `/stopattack [cond]` | ✅ | — | Stop auto-attack if conditions met |
| `/stopcasting [cond]` | ✅ | — | Stop casting if conditions met |
| `/unqueue [cond]` | ✅ | — | Clear spell queue if conditions met |
| `/cleartarget [cond]` | ✅ | — | Clear target if conditions met |
| `/cancelaura [cond] Name` | ✅ | — | Cancel buff if conditions met |
| `/quickheal [cond]` | ✅ | — | Smart heal (requires QuickHeal) |
| `/stopmacro [cond]` | ✅ | — | Stop ALL macro execution (including parent macros) |
| `/skipmacro [cond]` | ✅ | — | Stop current submacro only, parent continues |
| `/firstaction [cond]` | ✅ | — | Stop on first successful `/cast` or `/use` (priority mode) |
| `/nofirstaction [cond]` | ✅ | — | Re-enable multi-queue after `/firstaction` |
| `/petattack [cond]` | ✅ | — | Pet attack with conditionals |
| `/petfollow [cond]` | ✅ | — | Pet follow with conditionals |
| `/petwait [cond]` | ✅ | — | Pet stay with conditionals |
| `/petpassive [cond]` | ✅ | — | Pet passive with conditionals |
| `/petdefensive [cond]` | ✅ | — | Pet defensive with conditionals |
| `/petaggressive [cond]` | ✅ | — | Pet aggressive with conditionals |
| `/castsequence` | ✅ | — | Sequence with reset conditionals |
| `/equipmh [cond] Item` | ✅ | — | Equip to main hand |
| `/equipoh [cond] Item` | ✅ | — | Equip to off hand |
| `/equip11` - `/equip14 [cond]` | ✅ | — | Equip to slot (rings/trinkets) |
| `/unshift [cond]` | ✅ | — | Cancel shapeshift if conditions met |
| `/applymain [cond] Item` | ✅ | — | Apply poison/oil to main hand |
| `/applyoff [cond] Item` | ✅ | — | Apply poison/oil to off hand |
#### Priority-Based Macro Evaluation (`/firstaction` & `/nofirstaction`)
By default, all `/cast` lines in a macro are evaluated and may queue spells with Nampower. Use `/firstaction` to enable "first successful cast wins" behavior - once a `/cast` or `/use` succeeds, the macro stops evaluating subsequent lines. Use `/nofirstaction` to re-enable normal multi-queue behavior.
```lua
-- WITHOUT /firstaction: Both Shred AND Claw may queue if conditions are met
/cast [myrawpower:>48] Shred
/cast [myrawpower:>40] Claw
-- WITH /firstaction: Only Shred casts if energy >= 48, Claw is skipped
/firstaction
/cast [myrawpower:>48] Shred
/cast [myrawpower:>40] Claw
-- Mixed mode: priority section + always-queue section
/firstaction
/cast [myrawpower:>48] Shred -- Priority: only one of these
/cast [myrawpower:>40] Claw
/nofirstaction
/cast Tiger's Fury -- Always evaluates (can queue alongside)
/startattack
-- With conditionals
/firstaction [group] -- Priority mode only in groups
/cast [myrawpower:>48] Shred
/cast [myrawpower:>40] Claw
```
**Child Macro Behavior:** `/firstaction` and `/nofirstaction` in child macros (called via `{MacroName}`) affect subsequent lines in the parent macro. Think of the child's lines as being "inlined" at the call site.
**Comparison:**
- `/stopmacro [cond]` - Stop if condition is true (regardless of cast success)
- `/skipmacro [cond]` - Stop current submacro only
- `/firstaction [cond]` - Stop on first **successful** cast/use (priority mode)
- `/nofirstaction [cond]` - Re-enable multi-queue behavior after `/firstaction`
### Commands without Conditional Support
| Command | Description |
|---------|-------------|
| `/retarget` | Clear invalid target, target nearest enemy |
| `/runmacro Name` | Execute macro by name (use `{MacroName}` in `/cast` for conditionals) |
| `/rl` | Reload UI |
### UnitXP 3D Enemy Scanning
`/target` with any conditionals automatically uses UnitXP 3D scanning to find enemies in line of sight, even without nameplates visible. The only exception is `[help]` without `[harm]` (friendly-only targeting).
```lua
/target [name:Onyxia] -- Scans for exact name match
/target [nodead,harm] -- Scans for living enemies
/target [hp:<30] -- Scans for low HP enemies
/target [cc:stun] -- Scans for stunned enemies
-- Kara 40 Mage Incantagos example --
/target [name:Red_Affinity]
/cast [name:Red_Affinity] Fireball
/target [name:Blue_Affinity]
/cast [name:Blue_Affinity] Frostbolt
/target [name:Mana_Affinity]
/cast [name:Mana_Affinity] Arcane Missiles
```
If no matching target is found, your original target is preserved.
**Speed Reference** (for `[moving:>N]`): 0 = still, 100 = run, 160 = mount, 200 = epic mount.
---
@@ -473,6 +364,12 @@ Auto-learns debuff durations from your casts. 335+ spells pre-configured.
/cast Wrath -- Filler
```
**Notes:**
- Existence checks (`[nodebuff]`, `[debuff]`) detect ANY debuff on target
- Time-remaining checks (`[debuff:X<5]`) use internal tracking from your casts
- Shared debuffs (Sunder, Faerie Fire) are detected from any source
- [Cursive](https://github.com/pepopo978/Cursive) provides GUID-based tracking with better accuracy (handles target switching, pending casts, debuff cap)
### Combo Point Tracking
Tracks finisher durations (Rip, Rupture, Kidney Shot) accounting for combo points spent.
@@ -487,9 +384,9 @@ Automatically adjusts tracked durations for talents (Imp. Gouge, Taste for Blood
---
## Immunity Tracking Guide
## Immunity Tracking
The addon auto-learns NPC immunities from combat. When a spell fails with "immune", the addon remembers it for that NPC.
Auto-learns NPC immunities from combat. When a spell fails with "immune", the addon remembers it for that NPC.
### Using `[noimmune]`
@@ -500,172 +397,21 @@ The addon auto-learns NPC immunities from combat. When a spell fails with "immun
| `[noimmune:bleed]` | Bleed immunity specifically |
| `[noimmune:stun]` | Stun CC immunity |
### Common Examples
**Split damage spells** (Rake, Pounce, Garrote) have initial physical hit + bleed DoT. `[noimmune]` automatically checks **both** components.
```lua
-- Fire Mage: Skip immune targets
/cast [noimmune] Fireball
-- Warlock: Check shadow immunity
/cast [noimmune:shadow] Corruption
-- Rogue: Skip stun-immune targets
/cast [noimmune:stun] Cheap Shot
/cast Sinister Strike
-- Druid: Check fear immunity for Hibernate
/cast [noimmune:sleep,type:Beast/Dragonkin] Hibernate
/cast [noimmune] Rake -- Checks physical AND bleed
/cast [noimmune:stun] Cheap Shot -- Checks stun immunity
/cast [noimmune:shadow] Corruption -- Checks shadow immunity
```
### Split Damage Spells (Rake, Pounce, Garrote)
These spells have an initial physical hit + a bleed DoT. The addon **automatically checks BOTH immunities**:
### Manual Immunity Commands
```lua
-- Checks both physical AND bleed immunity
/cast [noimmune] Rake
-- With time check using Cursive
/cast [noimmune, cursive:Rake<1.5] Rake
-- Multi-DoT rotation
/cast [noimmune, nocursive:Rake] Rake
/cast [noimmune, nocursive:Rip, combo:>=4] Rip
```
`[noimmune]` for these spells returns false (skip cast) if target is immune to **either** component. You can also check specific schools:
- `[noimmune:physical]` - only checks physical immunity
- `[noimmune:bleed]` - only checks bleed immunity
### Pure Bleed Spells (Rip, Rupture, Rend)
These are pure bleeds with no initial hit:
```lua
-- Rip (pure bleed)
/cast [noimmune, combo:>=4] Rip
-- Rupture (pure bleed)
/cast [noimmune, combo:>=4] Rupture
```
### Manually Adding Immunities
If you know an NPC is immune before fighting:
```lua
/cleveroid addimmune "Boss Name" bleed -- Permanent bleed immunity
/cleveroid addimmune "Boss Name" fire "Shield" -- Fire immune only when buffed
/cleveroid addccimmune "Boss Name" stun -- Permanent stun immunity
```
### Viewing Learned Immunities
```lua
/cleveroid listimmune -- All school immunities
/cleveroid listimmune bleed -- Only bleed immunities
/cleveroid listccimmune -- All CC immunities
/cleveroid listccimmune stun -- Only stun immunities
```
---
## Debuff Tracking Guide
### Built-in Tracking (`[debuff]`)
The addon checks debuffs on the target:
```lua
-- Basic: Only cast if target doesn't have Moonfire
/cast [nodebuff:Moonfire] Moonfire
-- Time check: Refresh when < 4 seconds left
/cast [debuff:Moonfire<4] Moonfire
```
**Notes:**
- Existence checks (`[nodebuff]`, `[debuff]`) detect ANY debuff on target
- Time-remaining checks (`[debuff:X<5]`) use internal tracking from your casts
- Shared debuffs (Sunder, Faerie Fire) are detected from any source
### Cursive Integration (`[cursive]`)
[Cursive](https://github.com/avitasia/Cursive) provides more accurate GUID-based tracking:
```lua
-- Check if target has Rake (any time remaining)
/cast [nocursive:Rake] Rake
-- Refresh when < 3 seconds remaining
/cast [cursive:Rake<3] Rake
-- Complex: Only if missing OR about to expire
/cast [nocursive:Rake>1.5] Rake
```
**Advantages of Cursive:**
- GUID-based (survives target switching)
- Tracks pending casts
- Works at debuff cap
- More accurate timing
### MonkeySpeed Integration (`[moving]`)
[MonkeySpeed](https://github.com/jrc13245/MonkeySpeed) provides accurate movement speed detection via SuperWoW's `UnitPosition` API:
```lua
-- Only cast if standing still
/cast [nomoving] Aimed Shot
-- Cast instant ability while moving
/cast [moving] Arcane Shot
-- Check speed percentage (100 = normal run speed)
/cast [moving:>100] Sprint -- Already faster than normal
/cast [moving:<50] Escape Artist -- Currently slowed
-- Combined with other conditionals
/cast [nomoving, harm] Aimed Shot
/cast [moving, harm] Arcane Shot
```
**Speed Reference:**
| Speed | Description |
|-------|-------------|
| `0` | Standing still |
| `100` | Normal run speed |
| `160` | Level 40 mount |
| `200` | Epic mount |
**Notes:**
- Basic `[moving]`/`[nomoving]` works without MonkeySpeed (position fallback)
- Speed comparisons (`[moving:>100]`) require MonkeySpeed addon
- Speed values vary with buffs/debuffs affecting movement
### Recommended DoT Macro Patterns
**Druid Feral (with Cursive):**
```lua
#showtooltip
/cast [noimmune, nocursive:Rake] Rake
```
**Druid Feral (without Cursive):**
```lua
#showtooltip
/cast [noimmune, nodebuff] Rake
```
**Warlock Multi-DoT:**
```lua
#showtooltip
/cast [noimmune:shadow, nodebuff] Corruption
```
**Rogue Rupture:**
```lua
#showtooltip
/cast [noimmune, nodebuff, combo:>=4] Rupture
/cleveroid addimmune "Boss Name" bleed -- Permanent immunity
/cleveroid addimmune "Boss Name" fire "Shield" -- Conditional (while buffed)
/cleveroid addccimmune "Boss Name" stun -- CC immunity
/cleveroid listimmune [school] -- View school immunities
/cleveroid listccimmune [type] -- View CC immunities
```
---
@@ -673,25 +419,15 @@ The addon checks debuffs on the target:
## Settings
```lua
/cleveroid -- View settings
/cleveroid realtime 0|1 -- Instant updates (more CPU)
/cleveroid refresh 1-10 -- Update rate in Hz
/cleveroid debug 0|1 -- Debug messages
/cleveroid learn <id> <dur> -- Manually set spell duration
/cleveroid forget <id|all> -- Forget duration(s)
```
### Immunity Commands
```lua
/cleveroid listimmune [school]
/cleveroid addimmune "NPC" school [buff]
/cleveroid listccimmune [type]
/cleveroid addccimmune "NPC" type [buff]
```
### Combo Tracking
```lua
/combotrack show|clear|debug
/cleveroid -- View settings
/cleveroid realtime 0|1 -- Instant tooltip updates (more CPU)
/cleveroid refresh 1-10 -- Tooltip update rate in Hz
/cleveroid debug 0|1 -- Debug messages
/cleveroid learn <spellID> <dur> -- Manually set spell duration
/cleveroid forget <spellID|all> -- Forget learned duration(s)
/cleveroid debuffdebug [spell] -- Debug debuff tracking on target
/cleveroid tankdebug -- Debug pfUI tank conditionals
/combotrack show|clear|debug -- Combo point tracking
```
---
@@ -702,22 +438,14 @@ The addon checks debuffs on the target:
**Action Bars:** Blizzard, [pfUI](https://github.com/jrc13245/pfUI), Bongos, Discord Action Bars
**Integrations:** SP_SwingTimer, TWThreat, TimeToKill, QuickHeal, Cursive (with mouseover support for DoT timer bars), ClassicFocus, SuperMacro, MonkeySpeed
**Integrations:** [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer), [TWThreat](https://github.com/MarcelineVQ/TWThreat), [TimeToKill](https://github.com/jrc13245/TimeToKill), [QuickHeal](https://github.com/jrc13245/QuickHeal), [Cursive](https://github.com/pepopo978/Cursive), [ClassicFocus](https://github.com/wtfcolt/Addons-for-Vanilla-1.12.1-CFM/tree/master/ClassicFocus), [SuperMacro](https://github.com/jrc13245/SuperMacro-turtle-SuperWoW), [MonkeySpeed](https://github.com/jrc13245/MonkeySpeed)
> **Note:** For pfUI users, the [jrc13245/pfUI fork](https://github.com/jrc13245/pfUI) includes native SuperCleveRoidMacros integration for proper cooldown, icon, and tooltip display on conditional macros.
---
## Known Issues
- Unique macro names required (no blanks, duplicates, or spell names)
- Reactive abilities must be on action bars for detection
- HealComm requires MarcelineVQ's [LunaUnitFrames](https://github.com/MarcelineVQ/LunaUnitFrames) for SuperWoW compatibility
---
## Credits
Based on [CleverMacro](https://github.com/DanielAdolfsson/CleverMacro) and [Roid-Macros](https://github.com/DennisWG/Roid-Macros). Cursive integration by Avitasia.
Based on [CleverMacro](https://github.com/DanielAdolfsson/CleverMacro) and [Roid-Macros](https://github.com/DennisWG/Roid-Macros).
MIT License
+290 -58
View File
@@ -1972,7 +1972,7 @@ function lib:ShouldApplyDebuffRank(targetGUID, newSpellID)
end
-- Also clean up pfUI's tracking to prevent it from showing old ranks
if pfUI and pfUI.api and pfUI.api.libdebuff and targetName then
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff and targetName then
local pflib = pfUI.api.libdebuff
if pflib.objects and pflib.objects[targetName] then
@@ -2084,10 +2084,9 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster)
lib.objects[guid][spellID] = rec
-- PFUI INTEGRATION: Inject all tracked debuffs into pfUI's libdebuff
-- This allows pfUI to show accurate timers for ALL tracked debuffs (player + other casters)
-- For shared debuffs like Sunder Armor, we want pfUI to show timers regardless of who cast it
if pfUI and pfUI.api and pfUI.api.libdebuff and unitName then
-- PFUI INTEGRATION: Inject all tracked debuffs into pfUI's libdebuff (pre-7.6 only)
-- pfUI 7.6+ handles all duration tracking internally via GetUnitField
if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and not CleveRoids.hasPfUI76 then
local pflib = pfUI.api.libdebuff
local spellName = SpellInfo(spellID)
@@ -2351,8 +2350,8 @@ local function SeedUnit(unit)
existing.start = GetTime()
existing.duration = duration
-- PFUI INTEGRATION: Inject refreshed timer into pfUI
if pfUI and pfUI.api and pfUI.api.libdebuff and unitName then
-- PFUI INTEGRATION: Inject refreshed timer into pfUI (pre-7.6 only)
if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and not CleveRoids.hasPfUI76 then
local pflib = pfUI.api.libdebuff
local spellName = SpellInfo(spellID)
if spellName and pflib.AddEffect then
@@ -2440,8 +2439,8 @@ local function SeedUnit(unit)
existing.start = GetTime()
existing.duration = duration
-- PFUI INTEGRATION: Inject refreshed timer into pfUI
if pfUI and pfUI.api and pfUI.api.libdebuff and unitName then
-- PFUI INTEGRATION: Inject refreshed timer into pfUI (pre-7.6 only)
if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and not CleveRoids.hasPfUI76 then
local pflib = pfUI.api.libdebuff
local spellName = SpellInfo(spellID)
if spellName and pflib.AddEffect then
@@ -2714,7 +2713,7 @@ function lib.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID)
-- DON'T call pfUI's AddEffect - just update the existing entry directly
-- pfUI will pick up the new duration through our GetDuration/UnitDebuff hooks
if pfUI and pfUI.api and pfUI.api.libdebuff then
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
local pflib = pfUI.api.libdebuff
local ripSpellName = SpellInfo(ripSpellID)
local baseName = ripSpellName and string.gsub(ripSpellName, "%s*%(Rank %d+%)", "") or "Rip"
@@ -2827,7 +2826,7 @@ function lib.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID)
-- DON'T call pfUI's AddEffect - just update the existing entry directly
-- pfUI will pick up the new duration through our GetDuration/UnitDebuff hooks
if pfUI and pfUI.api and pfUI.api.libdebuff then
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
local pflib = pfUI.api.libdebuff
local rakeSpellName = SpellInfo(rakeSpellID)
local baseName = rakeSpellName and string.gsub(rakeSpellName, "%s*%(Rank %d+%)", "") or "Rake"
@@ -3280,7 +3279,17 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
-- Check after delay: 0.4s for hidden CC (no visible debuff), 0.2s for normal CC
-- Hidden CC (e.g., Pounce stun) needs longer delay to wait for "afflicted by" messages
local verifyDelay = pending.isHiddenCC and 0.4 or 0.2
if elapsed >= verifyDelay then
-- Drop stale entries (>2s) without recording immunity - inconclusive due to severe lag
if elapsed > 2.0 then
if debug then
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cffff6600[CC Stale]|r Dropping stale CC entry for %s on %s (%.1fs old)",
pending.ccType or "CC", pending.targetName or "Unknown", elapsed)
)
end
-- Don't add to newPendingList - silently discard
elseif elapsed >= verifyDelay then
local ccVerified = false
local totalDebuffs = 0
@@ -3290,10 +3299,24 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
ccVerified = true
end
-- SPELL_GO early-exit: Use definitive hit/miss data when available
-- This is faster and more reliable than debuff scanning, and handles debuff cap correctly
if not ccVerified and pending.spellGoHit then
ccVerified = true
if debug then
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cff00ff00[CC Verified via SPELL_GO]|r %s landed on %s - skipping debuff scan",
pending.ccType or "CC", pending.targetName or "Unknown")
)
end
end
-- CC IMMUNITY VERIFICATION: Check if CC effect actually landed
-- Uses hybrid approach: direct spell ID match OR mechanic-based validation
-- (CC debuff IDs often differ from cast IDs, e.g., Pounce cast ≠ Pounce Stun debuff)
if not ccVerified and hasSuperwow and pending.targetGUID then
-- Guard: skip debuff scanning if SPELL_GO already determined outcome
if not ccVerified and not pending.spellGoHit and not pending.spellGoMissed
and hasSuperwow and pending.targetGUID then
-- Skip verification if target is dead (debuffs are removed on death)
if _UnitIsDead(pending.targetGUID) then
ccVerified = true -- Assume CC landed, can't verify on dead target
@@ -3350,6 +3373,13 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
end
end
if not ccVerified and pending.spellGoMissed and debug then
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cffff6600[CC Missed via SPELL_GO]|r %s missed on %s - checking immunity",
pending.ccType or "CC", pending.targetName or "Unknown")
)
end
-- If CC didn't land, check if it's immunity or debuff cap
if not ccVerified and not _UnitIsDead(pending.targetGUID) then
-- totalDebuffs already counted above, reuse it
@@ -3392,7 +3422,8 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
-- SPLIT CC SPELLS: Skip immunity recording for spells with physical damage + resistable CC
-- (e.g., Master Strike) - physical damage lands but CC can be resisted independently
local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID]
-- spellGoHit: SPELL_GO confirmed hit, so CC was resisted independently (not true immunity)
local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] or pending.spellGoHit
if isSplitCCSpell then
if debug then
local spellNameDebug = pending.spellName or (_SpellInfo(pending.spellID) or "Unknown")
@@ -3485,13 +3516,36 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
)
end
-- Check after 0.2s delay (enough time for server sync)
if elapsed >= 0.2 then
-- Drop stale entries (>2s) without recording immunity - inconclusive due to severe lag
if elapsed > 2.0 then
if debug then
local spellNameDebug = pending.spellID and _SpellInfo(pending.spellID) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cffff6600[Shared Stale]|r Dropping stale shared entry for %s on %s (%.1fs old)",
spellNameDebug, pending.targetName or "Unknown", elapsed)
)
end
-- Don't add to newPendingList - silently discard
elseif elapsed >= 0.2 then
local debuffVerified = false
local totalDebuffs = 0
-- SPELL_GO early-exit: Use definitive hit/miss data when available
if pending.spellGoHit then
debuffVerified = true
if debug then
local spellNameDebug = _SpellInfo(pending.spellID) or "Shared Debuff"
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cff00ff00[Shared Verified via SPELL_GO]|r %s landed on %s - skipping debuff scan",
spellNameDebug, pending.targetName or "Unknown")
)
end
end
-- Skip verification if target is dead (debuffs are removed on death)
if hasSuperwow and pending.targetGUID then
-- Guard: skip debuff scanning if SPELL_GO already determined outcome
if not debuffVerified and not pending.spellGoHit and not pending.spellGoMissed
and hasSuperwow and pending.targetGUID then
if _UnitIsDead(pending.targetGUID) then
-- Target died - can't verify immunity, assume debuff landed
debuffVerified = true
@@ -3545,7 +3599,8 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
-- SPLIT CC SPELLS: Skip immunity recording for spells with physical damage + resistable CC
-- (e.g., Master Strike) - the physical damage lands but CC can be resisted independently
local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] or SPLIT_CC_SPELLS[pending.castSpellID]
-- spellGoHit: SPELL_GO confirmed hit, so debuff was resisted independently (not true immunity)
local isSplitCCSpell = SPLIT_CC_SPELLS[pending.spellID] or SPLIT_CC_SPELLS[pending.castSpellID] or pending.spellGoHit
if isSplitCCSpell then
if debug then
local spellNameDebug = _SpellInfo(pending.castSpellID or pending.spellID) or "Unknown"
@@ -3954,9 +4009,9 @@ ev:SetScript("OnEvent", function()
)
end
-- CRITICAL: Update pfUI's duration database directly
if pfUI and pfUI.api and pfUI.api.libdebuff and pfUI.api.libdebuff.debuffs then
-- pfUI stores durations by spell name in its debuffs table
-- Update pfUI's duration database directly (pre-7.6 only)
-- pfUI 7.6+ handles combo durations internally via GetStoredComboPoints()
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff and pfUI.api.libdebuff.debuffs then
pfUI.api.libdebuff.debuffs[baseName] = duration
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
@@ -4389,6 +4444,34 @@ ev:SetScript("OnEvent", function()
local _, playerGUID = UnitExists("player")
local isOurs = (casterGuid == playerGUID)
-- Annotate pending CC/shared debuffs with SPELL_GO hit/miss outcome
-- This data is consumed by the OnUpdate verification loop for early-exit paths
if isOurs then
-- Annotate pendingCCDebuffs
for _, pending in ipairs(lib.pendingCCDebuffs) do
if pending and pending.spellID == spellId and pending.targetGUID == targetGuid then
if numHit > 0 then
pending.spellGoHit = true
elseif numMissed > 0 then
pending.spellGoMissed = true
end
break
end
end
-- Annotate pendingSharedDebuffs (match against both castSpellID and spellID)
for _, pending in ipairs(lib.pendingSharedDebuffs) do
if pending and pending.targetGUID == targetGuid
and (pending.castSpellID == spellId or pending.spellID == spellId) then
if numHit > 0 then
pending.spellGoHit = true
elseif numMissed > 0 then
pending.spellGoMissed = true
end
break
end
end
end
-- Spell missed - clear pending, mark as failed, track for immunity detection
if numHit == 0 and numMissed > 0 then
if lib.pendingCasts[targetGuid] then
@@ -4482,14 +4565,26 @@ ev:SetScript("OnEvent", function()
-- time AURA_CAST fires. Store in pendingCasts for AURA_CAST to use.
if isOurs and lib.combopointAbilities and lib.combopointAbilities[spellName] then
-- Capture combo points from multiple sources
local comboPoints = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0
-- Priority: SPELL_CAST_EVENT (client-side, pre-server) > GetComboPoints > lastComboPoints > ComboPointTracking
local comboPoints = 0
-- If already consumed, use lastComboPoints fallback
-- PRIMARY: Check SPELL_CAST_EVENT capture (most reliable - captured before server consumed CP)
local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId]
if pending and pending.comboPoints and pending.comboPoints > 0 then
comboPoints = pending.comboPoints
end
-- Fallback: Try GetComboPoints (may still be available)
if comboPoints == 0 then
comboPoints = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0
end
-- Fallback: lastComboPoints (from pre-cast hooks or UNIT_CASTEVENT START)
if comboPoints == 0 and CleveRoids.lastComboPoints and CleveRoids.lastComboPoints > 0 then
comboPoints = CleveRoids.lastComboPoints
end
-- Also check ComboPointTracking for recent data (from /cast hook pre-population)
-- Fallback: ComboPointTracking for recent data (from /cast hook pre-population)
if comboPoints == 0 and CleveRoids.ComboPointTracking then
local tracking = CleveRoids.ComboPointTracking[spellName]
if tracking and tracking.combo_points and tracking.combo_points > 0 then
@@ -4551,24 +4646,35 @@ ev:SetScript("OnEvent", function()
-- Handle combo point abilities for our casts
if isOurs and lib.combopointAbilities[spellName] then
-- Priority order for combo points:
-- 1. pendingCasts (captured by SPELL_GO - most reliable)
-- 2. CleveRoids.lastComboPoints (from /cast hook pre-population)
-- 3. GetComboPoints() (might be 0 if already consumed)
-- 1. SPELL_CAST_EVENT capture (client-side, before server consumes CP)
-- 2. pendingCasts from SPELL_GO (captured on spell landing)
-- 3. CleveRoids.lastComboPoints (from pre-cast hooks)
-- 4. GetComboPoints() (might be 0 if already consumed)
local comboPoints = 0
local comboSource = "none"
-- Check pendingCasts first (populated by SPELL_GO)
if lib.pendingCasts[targetGuid] and lib.pendingCasts[targetGuid][spellName] then
-- PRIMARY: Check SPELL_CAST_EVENT capture (most reliable - client-side pre-server)
local castPending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId]
if castPending and castPending.comboPoints and castPending.comboPoints > 0 then
comboPoints = castPending.comboPoints
comboSource = "SPELL_CAST_EVENT"
end
-- Fallback: Check lib.pendingCasts (populated by SPELL_GO)
if comboPoints == 0 and lib.pendingCasts[targetGuid] and lib.pendingCasts[targetGuid][spellName] then
local pending = lib.pendingCasts[targetGuid][spellName]
local age = GetTime() - (pending.capturedAt or 0)
if pending.comboPoints and pending.comboPoints > 0 and age < 2.0 then
comboPoints = pending.comboPoints
comboSource = "SPELL_GO"
-- Clean up after use
lib.pendingCasts[targetGuid][spellName] = nil
end
end
-- Clean up lib.pendingCasts after reading (regardless of which source won)
if lib.pendingCasts[targetGuid] and lib.pendingCasts[targetGuid][spellName] then
lib.pendingCasts[targetGuid][spellName] = nil
end
-- Fallback to lastComboPoints
if comboPoints == 0 and CleveRoids.lastComboPoints and CleveRoids.lastComboPoints > 0 then
comboPoints = CleveRoids.lastComboPoints
@@ -5096,7 +5202,7 @@ evLearn:SetScript("OnEvent", function()
-- Sync refresh to pfUI
local targetName = lib.guidToName[targetGUID]
if pfUI and pfUI.api and pfUI.api.libdebuff and targetName then
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff and targetName then
local pflib = pfUI.api.libdebuff
local spellName = SpellInfo(flameShockID)
if spellName and pflib.AddEffect then
@@ -5262,17 +5368,14 @@ evJudgement:SetScript("OnEvent", function()
)
end
-- Also sync to pfUI if it's loaded
if pfUI and pfUI.api and pfUI.api.libdebuff then
-- Also sync to pfUI if it's loaded (pre-7.6 only)
if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then
local targetName = lib.guidToName[targetGUID] or UnitName("target")
local targetLevel = UnitLevel("target") or 0
local spellName = SpellInfo(spellID)
if spellName and targetName then
-- Remove rank from spell name to match pfUI's format
local effectName = string.gsub(spellName, "%s*%(Rank %d+%)", "")
-- Refresh in pfUI's tracking
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, effectName, rec.duration, "player")
if CleveRoids.debug then
@@ -7896,9 +7999,10 @@ function CleveRoids.ParseReactiveCombatLog()
local _, targetGUID = UnitExists("target")
-- Check each reactive ability's trigger patterns
-- NOTE: Overpower (enemy_dodge) ALWAYS uses combat log text parsing, even when SPELL_GO
-- events are available. SPELL_GO only provides hit/miss COUNTS, not miss TYPES, so it
-- cannot distinguish dodge (Overpower proc) from parry/block/resist (no Overpower proc).
for spellName, config in pairs(reactivePatterns) do
-- Check if this is a known reactive spell (skip hasSpell check - spellbook may not be indexed yet)
-- If player uses [reactive:SpellName], they have the ability
if CleveRoids.reactiveSpells and CleveRoids.reactiveSpells[spellName] then
for _, pattern in ipairs(config.patterns) do
if strfind(message, pattern) then
@@ -7908,7 +8012,6 @@ function CleveRoids.ParseReactiveCombatLog()
CleveRoids.SetReactiveProc(spellName, duration, guid)
-- For Overpower (enemy dodge), also update LastSwing so [lastswing:dodge] works
-- This handles ability dodges that AUTO_ATTACK events don't capture
if config.type == "enemy_dodge" and CleveRoids.LastSwing then
CleveRoids.LastSwing.timestamp = GetTime()
CleveRoids.LastSwing.victimState = VICTIMSTATE_DODGE_REACTIVE
@@ -7964,10 +8067,19 @@ end
-- Uses native events for dodge/parry/block detection when available.
-- Falls back to combat log parsing for older Nampower versions.
-- VictimState constants from NampowerAPI (copied for performance)
-- VictimState constants (from AUTO_ATTACK / SPELL_GO events)
local VICTIMSTATE_UNAFFECTED = 0 -- Generic miss (seen with HITINFO_MISS)
local VICTIMSTATE_NORMAL = 1 -- Hit landed
local VICTIMSTATE_DODGE = 2
local VICTIMSTATE_PARRY = 3
local VICTIMSTATE_INTERRUPT = 4
local VICTIMSTATE_BLOCKS = 5
local VICTIMSTATE_EVADES = 6
local VICTIMSTATE_IS_IMMUNE = 7
local VICTIMSTATE_DEFLECTS = 8
-- HitInfo constant for SPELL_GO miss tracking
local HITINFO_MISS = 16 -- 0x10
-- Track if we're using Nampower events (set during initialization)
CleveRoids.usingNampowerAutoAttack = false
@@ -8081,12 +8193,44 @@ if GetNampowerVersion then
end
end
-- Always register combat log events as fallback (or primary if no Nampower)
reactiveFrame:RegisterEvent("RAW_COMBATLOG")
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES")
reactiveFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES")
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS")
-- Check for Nampower v2.25+ SPELL_GO events (LastSwing tracking for yellow attack misses)
-- NOTE: SPELL_GO only provides hit/miss counts, not miss types, so Overpower still needs
-- combat log text parsing. SPELL_GO is used as a gate to avoid parsing combat log on every
-- successful spell hit - only parse when SPELL_GO reports a miss.
local hasSpellGoEvents = false
if GetNampowerVersion then
local npMajor, npMinor = GetNampowerVersion()
if npMajor > 2 or (npMajor == 2 and npMinor >= 25) then
hasSpellGoEvents = true
reactiveFrame:RegisterEvent("SPELL_GO_SELF")
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Using SPELL_GO events for yellow attack LastSwing tracking (v2.25+)")
end
end
end
-- Register combat log events for reactive abilities
-- Outgoing dodge detection (Overpower) uses combat log text parsing because SPELL_GO only
-- provides hit/miss counts, not miss types (dodge vs parry vs block vs resist). However,
-- with SPELL_GO available, CHAT_MSG_SPELL_SELF_DAMAGE parsing is gated behind a miss window
-- so we skip parsing on every successful spell hit (see OnEvent handler below).
-- AUTO_ATTACK events handle white swing avoidance; combat log handles yellow ability dodges.
if hasAutoAttackEvents then
-- Incoming avoidance (Riposte/Revenge)
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES")
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS")
-- Outgoing dodge detection (Overpower) - gated by SPELL_GO miss window when available
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES")
reactiveFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
else
-- Full combat log fallback
reactiveFrame:RegisterEvent("RAW_COMBATLOG")
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_MISSES")
reactiveFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES")
reactiveFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS")
end
reactiveFrame:SetScript("OnEvent", function()
-- ========================================================================
@@ -8122,18 +8266,106 @@ reactiveFrame:SetScript("OnEvent", function()
end
-- ========================================================================
-- COMBAT LOG PARSING FOR REACTIVE ABILITIES
-- NAMPOWER v2.25+ SPELL_GO EVENTS (yellow attack LastSwing tracking)
-- ========================================================================
-- NOTE: This must ALWAYS run, even when Nampower AUTO_ATTACK events are available.
-- AUTO_ATTACK events only fire for white (auto-attack) swings, NOT for yellow
-- (ability) damage like Mortal Strike, Heroic Strike, etc. being dodged/parried.
-- Combat log parsing catches ability dodges that AUTO_ATTACK misses.
-- The AUTO_ATTACK handlers above already `return` early, so no double-processing.
if event == "RAW_COMBATLOG" or
event == "CHAT_MSG_COMBAT_SELF_MISSES" or
event == "CHAT_MSG_SPELL_SELF_DAMAGE" or
event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES" or
event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS" then
-- Two-step: SPELL_CAST_EVENT (success=1) → SPELL_GO_SELF (hit/miss outcome)
-- Updates LastSwing state for yellow ability misses. Overpower detection still uses
-- combat log text parsing (SPELL_GO only has hit/miss counts, not types like dodge).
-- SPELL_GO_SELF params: itemId, spellId, casterGuid, targetGuid, castFlags, numTargetsHit, numTargetsMissed
if event == "SPELL_GO_SELF" then
local spellId = arg2
local targetGuid = arg4
local numHit = arg6 or 0
local numMissed = arg7 or 0
-- Step 1: Validate against SPELL_CAST_EVENT (confirms player-initiated, successful cast)
local pending = CleveRoids.pendingCasts and CleveRoids.pendingCasts[spellId]
if not pending or pending.consumed then
return -- No matching SPELL_CAST_EVENT with success=1, or already processed
end
-- Mark pending cast as consumed with timestamp instead of deleting immediately.
-- Other handlers (libdebuff SPELL_GO, AURA_CAST, ComboPointTracker) on different
-- frames may still need to read this data. Stale entries are cleaned up on next
-- SPELL_CAST_EVENT for the same spellId (overwritten) or expire naturally.
pending.consumed = true
pending.consumedAt = GetTime()
-- Skip channels and targeting spells (not melee/ranged attacks)
-- CastType: NORMAL=1, NON_GCD=2, ON_SWING=3, CHANNEL=4, TARGETING=5, TARGETING_NON_GCD=6
if pending.castType and pending.castType >= 4 then
return
end
-- Step 2: Check hit/miss outcome for LastSwing tracking
-- NOTE: SPELL_GO only provides hit/miss COUNTS, not miss TYPES (dodge vs parry vs
-- block vs resist). Overpower detection is handled by combat log text parsing which
-- can distinguish "dodged" from other avoidance types. We only update LastSwing here
-- as a generic miss indicator (combat log overwrites with specific type if dodge).
if numMissed >= 1 and numHit == 0 then
-- Use current target GUID if SPELL_GO targetGuid is empty
local procTarget = targetGuid
if not procTarget or procTarget == "0x0000000000000000" then
procTarget = pending.targetGuid
if not procTarget or procTarget == "0x0000000000000000" then
local _, currentTargetGUID = UnitExists("target")
procTarget = currentTargetGUID
end
end
-- Update LastSwing for yellow miss (victimState unknown from SPELL_GO, use UNAFFECTED)
-- Don't overwrite AUTO_ATTACK data from the same frame (on-swing abilities fire both)
-- Combat log handler will overwrite with specific type (dodge/parry/etc.) if available
if CleveRoids.LastSwing and CleveRoids.LastSwing.timestamp ~= GetTime() then
CleveRoids.LastSwing.timestamp = GetTime()
CleveRoids.LastSwing.hitInfo = HITINFO_MISS
CleveRoids.LastSwing.victimState = VICTIMSTATE_UNAFFECTED
CleveRoids.LastSwing.damage = 0
CleveRoids.LastSwing.blockedAmount = 0
CleveRoids.LastSwing.absorbAmount = 0
CleveRoids.LastSwing.resistAmount = 0
CleveRoids.LastSwing.targetGuid = procTarget
end
-- Gate combat log parsing: set miss window so CHAT_MSG_SPELL_SELF_DAMAGE
-- only calls ParseReactiveCombatLog when we know a yellow miss occurred.
-- This avoids parsing every successful spell hit message.
CleveRoids._yellowMissWindow = GetTime()
if CleveRoids.debug then
local spellName = SpellInfo and SpellInfo(spellId) or tostring(spellId)
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff00[SPELL_GO]|r Yellow miss detected - %s (hit=%d, miss=%d, castType=%s) - deferring to combat log for type",
spellName, numHit, numMissed, tostring(pending.castType))
)
end
end
return
end
-- ========================================================================
-- COMBAT LOG TEXT PARSING FOR REACTIVE ABILITIES
-- ========================================================================
-- Handles: Overpower (outgoing dodge), Riposte (incoming parry), Revenge (incoming avoid).
-- SPELL_GO provides hit/miss counts but not types, so combat log is still needed to
-- distinguish dodge (Overpower) from parry/block/resist. But we only parse when SPELL_GO
-- told us a miss occurred, avoiding overhead on every successful spell hit.
if event == "CHAT_MSG_SPELL_SELF_DAMAGE" then
-- Yellow ability avoidance (e.g., "Your Mortal Strike was dodged by Target")
-- With SPELL_GO events: only parse when a yellow miss was recently detected.
-- Without SPELL_GO: always parse (fallback for older Nampower).
if hasSpellGoEvents then
local missWindow = CleveRoids._yellowMissWindow
if not missWindow or (GetTime() - missWindow) > 1.0 then
return -- No recent yellow miss from SPELL_GO, skip parsing
end
end
CleveRoids.ParseReactiveCombatLog()
elseif event == "RAW_COMBATLOG" or
event == "CHAT_MSG_COMBAT_SELF_MISSES" or
event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES" or
event == "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS" then
CleveRoids.ParseReactiveCombatLog()
end
end)