From 728cf92ed2164012735896514641f6339da4345b Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Fri, 9 Jan 2026 08:00:08 -0500 Subject: [PATCH] fix debuff timer checks, nodebuff existence checks with ranked spells, and split type spells having both types immune checked for cast, improve readme --- Conditionals.lua | 25 +++++++- README.md | 155 ++++++++++++++++++++++++++++++++++++++++++++++- Utility.lua | 111 ++++++++++++++++++++++++++++++--- 3 files changed, 279 insertions(+), 12 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index 1dc488a..94e2a30 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -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 diff --git a/README.md b/README.md index 5a96858..69a5d2c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Utility.lua b/Utility.lua index 57f8557..dde549f 100644 --- a/Utility.lua +++ b/Utility.lua @@ -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