fix debuff timer checks, nodebuff existence checks with ranked spells, and split type spells having both types immune checked for cast, improve readme

This commit is contained in:
Jrc13245
2026-01-09 08:00:08 -05:00
parent c98223b64a
commit 728cf92ed2
3 changed files with 279 additions and 12 deletions
+22 -3
View File
@@ -714,6 +714,11 @@ local function _get_debuff_timeleft(unitToken, auraName)
-- Defensive: verify libdebuff is a table before accessing properties
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
-- Strip rank suffix for consistent matching (e.g., "Rake(Rank 4)" -> "Rake")
if auraName then
auraName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "")
end
-- SuperWoW path: GUID-based lookup
-- SuperWoW debuff slots: 1-16 are regular debuffs, 17-48 overflow to buff slots 1-32
if CleveRoids.hasSuperwow and lib then
@@ -727,7 +732,9 @@ local function _get_debuff_timeleft(unitToken, auraName)
-- Only break for slots 1-16 (regular debuffs are dense)
-- For overflow slots 17-48, nil means "regular buff filtered out", not "end of list"
if not effect and i <= 16 then break end
if effect and effect == auraName and timeleft and timeleft >= 0 then
-- Strip rank from effect name for comparison
local effectBase = effect and string.gsub(effect, "%s*%(%s*Rank%s+%d+%s*%)", "")
if effectBase and effectBase == auraName and timeleft and timeleft >= 0 then
return timeleft, duration
end
end
@@ -741,7 +748,9 @@ local function _get_debuff_timeleft(unitToken, auraName)
-- Only break for slots 1-16 (regular debuffs are dense)
-- For overflow slots 17-48, nil means "regular buff filtered out", not "end of list"
if not effect and idx <= 16 then break end
if effect and effect == auraName and timeleft and timeleft >= 0 then
-- Strip rank from effect name for comparison
local effectBase = effect and string.gsub(effect, "%s*%(%s*Rank%s+%d+%s*%)", "")
if effectBase and effectBase == auraName and timeleft and timeleft >= 0 then
return timeleft, duration
end
end
@@ -1812,6 +1821,11 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
local stacks, remaining
local i = isPlayer and 0 or 1
-- Strip rank suffix for consistent matching (e.g., "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)")
if args.name then
args.name = string.gsub(args.name, "%s*%(%s*Rank%s+%d+%s*%)", "")
end
-- PERFORMANCE: Cache lowercased search name to avoid repeated string.lower calls
local searchName = args.name and _string_lower(args.name)
@@ -1926,6 +1940,9 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
end
if not args.name then return false end
-- Strip rank suffix for consistent matching (e.g., "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)")
args.name = string.gsub(args.name, "%s*%(%s*Rank%s+%d+%s*%)", "")
local found = false
local texture, stacks, spellID, remaining
local i
@@ -2209,7 +2226,9 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
-- Only break for slots 1-16 (regular debuffs are dense)
-- For overflow slots 17-48, nil means "regular buff filtered out", not "end of list"
if not effect and idx <= 16 then break end
if effect and effect == args.name then
-- Strip rank from effect name for comparison
local effectBase = effect and string.gsub(effect, "%s*%(%s*Rank%s+%d+%s*%)", "")
if effectBase and effectBase == args.name then
local shouldSkip = false
-- Auto-detect: If args.mine not specified and this is a personal debuff, only match player casts
+154 -1
View File
@@ -162,13 +162,16 @@ All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff
|-------------|---------|-------------|
| `cc` | `[cc]` `[cc:stun/fear]` | Target has CC effect |
| `mycc` | `[mycc]` `[mycc:silence]` | Player has CC effect |
| `immune` | `[immune:fire]` `[immune:stun]` | School/CC immunity |
| `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:** stun, fear, root, snare/slow, sleep, charm, polymorph, banish, horror, disorient, silence, disarm, daze, freeze, shackle
**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
### Addon Integrations
| Conditional | Addon | Example | Description |
|-------------|-------|---------|-------------|
@@ -308,6 +311,156 @@ Automatically adjusts tracked durations for talents (Imp. Gouge, Taste for Blood
---
## Immunity Tracking Guide
The addon auto-learns NPC immunities from combat. When a spell fails with "immune", the addon remembers it for that NPC.
### Using `[noimmune]`
| Usage | What it checks |
|-------|----------------|
| `[noimmune]` | Auto-detects spell's school from action |
| `[noimmune:fire]` | Fire immunity specifically |
| `[noimmune:bleed]` | Bleed immunity specifically |
| `[noimmune:stun]` | Stun CC immunity |
### Common Examples
```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
```
### Split Damage Spells (Rake, Pounce, Garrote)
These spells have an initial physical hit + a bleed DoT. The addon **automatically checks BOTH immunities**:
```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
-- 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
### 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
```
---
## Settings
```lua
+103 -8
View File
@@ -4981,6 +4981,26 @@ local spellSchoolCache = {}
local function GetSpellSchool(spellName, spellID)
if not spellName and not spellID then return nil end
-- Get base name early for split damage spell check
local baseName = nil
if spellName then
baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
elseif spellID and SpellInfo then
local fullName = SpellInfo(spellID)
if fullName then
baseName = string.gsub(fullName, "%s*%(.-%)%s*$", "")
end
end
-- PRIORITY 0 (HIGHEST): Split damage spells ALWAYS use their debuff school for immunity checks
-- This prevents the initial physical hit from being learned and masking bleed immunity.
-- Example: Rake's initial hit is physical, but the DoT is bleed. On bleed-immune targets,
-- the DoT never ticks, so spellSchoolMapping stays "physical" - which would miss bleed immunity!
if baseName and SPLIT_DAMAGE_SPELLS[baseName] then
local school = SPLIT_DAMAGE_SPELLS[baseName].debuff
return school
end
-- PRIORITY 1: Use learned school from Nampower damage events (most accurate)
if spellID and CleveRoids.spellSchoolMapping[spellID] then
return CleveRoids.spellSchoolMapping[spellID]
@@ -5003,20 +5023,17 @@ local function GetSpellSchool(spellName, spellID)
if not spellName then return nil end
-- Remove rank information for cache consistency
local baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
-- Remove rank information for cache consistency (may already be set from above)
if not baseName then
baseName = string.gsub(spellName, "%s*%(.-%)%s*$", "")
end
-- PRIORITY 3: Check cache
if spellSchoolCache[baseName] then
return spellSchoolCache[baseName]
end
-- PRIORITY 4: Check if this is a split damage spell (return debuff school by default)
if SPLIT_DAMAGE_SPELLS[baseName] then
local school = SPLIT_DAMAGE_SPELLS[baseName].debuff
spellSchoolCache[baseName] = school
return school
end
-- PRIORITY 4: SPLIT_DAMAGE_SPELLS already checked at Priority 0
-- PRIORITY 5: Check known non-damaging spells
-- These won't be learned via damage events and need explicit mapping
@@ -5939,6 +5956,84 @@ function CleveRoids.CheckImmunity(unitId, spellOrSchool)
else
-- Input is a spell name, need to determine its school
checkSpellName = spellOrSchool -- Save the spell name for unknown school check
-- Get base name for split damage spell check
local baseName = string.gsub(spellOrSchool, "%s*%(.-%)%s*$", "")
-- SPLIT DAMAGE SPELLS: Check BOTH initial and debuff schools
-- If target is immune to EITHER component, skip the spell
if SPLIT_DAMAGE_SPELLS[baseName] then
local splitData = SPLIT_DAMAGE_SPELLS[baseName]
local initialSchool = splitData.initial
local debuffSchool = splitData.debuff
-- Check initial school immunity (e.g., physical for Rake's initial hit)
local initialImmune = false
if initialSchool and CleveRoids_ImmunityData[initialSchool] then
local initialImmunityData = CleveRoids_ImmunityData[initialSchool][targetName]
if initialImmunityData == true then
initialImmune = true
elseif type(initialImmunityData) == "table" and not initialImmunityData.buff then
initialImmune = true
elseif type(initialImmunityData) == "table" and initialImmunityData.buff then
-- Check if target has the immunity-granting buff
if CleveRoids.hasSuperwow then
for i = 1, 32 do
local texture, stacks, spellID = UnitBuff(unitId, i)
if not texture then break end
if spellID then
local buffName = SpellInfo(spellID)
if buffName and buffName == initialImmunityData.buff then
initialImmune = true
break
end
end
end
end
end
end
-- Check debuff school immunity (e.g., bleed for Rake's DoT)
local debuffImmune = false
if debuffSchool and CleveRoids_ImmunityData[debuffSchool] then
local debuffImmunityData = CleveRoids_ImmunityData[debuffSchool][targetName]
if debuffImmunityData == true then
debuffImmune = true
elseif type(debuffImmunityData) == "table" and not debuffImmunityData.buff then
debuffImmune = true
elseif type(debuffImmunityData) == "table" and debuffImmunityData.buff then
-- Check if target has the immunity-granting buff
if CleveRoids.hasSuperwow then
for i = 1, 32 do
local texture, stacks, spellID = UnitBuff(unitId, i)
if not texture then break end
if spellID then
local buffName = SpellInfo(spellID)
if buffName and buffName == debuffImmunityData.buff then
debuffImmune = true
break
end
end
end
end
end
end
-- Return true if immune to EITHER component
if initialImmune or debuffImmune then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff6600[Immunity Check]|r %s on %s: initial(%s)=%s, debuff(%s)=%s",
baseName, targetName,
initialSchool, initialImmune and "IMMUNE" or "ok",
debuffSchool, debuffImmune and "IMMUNE" or "ok")
)
end
return true
end
return false
end
school = GetSpellSchool(spellOrSchool)
if not school then
school = "unknown" -- If we can't determine school, check unknown category