diff --git a/Conditionals.lua b/Conditionals.lua index bf8ec57..e1ca333 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -149,6 +149,33 @@ function CleveRoids.InvalidateSpellNameCache() _spellNameToIDsBuilt = false end +-- Check if a debuff is shared by spell ID or by name (for custom/Turtle WoW IDs). +-- Falls back to checking if the debuff NAME matches any known shared debuff name +-- in the spell name cache, even if the specific spell ID is unregistered. +local function IsSharedDebuffByIdOrName(lib, spellID, debuffName) + if not lib then return false end + -- Direct ID check (fast path) + if lib.IsPersonalDebuff and lib:IsPersonalDebuff(spellID) == false then + return true + end + -- Name-based fallback: if ANY spell ID for this name is in sharedDebuffs, treat as shared. + -- This handles custom server spell IDs (e.g., Turtle WoW Judgement variants) that have + -- the same debuff name but different IDs from the hardcoded vanilla entries. + if debuffName then + local knownIDs = GetSpellIDsForName(debuffName) + if knownIDs then + for _, kid in ipairs(knownIDs) do + if lib.sharedDebuffs and lib.sharedDebuffs[kid] then + -- Also register this new spell ID so future checks are fast + lib.sharedDebuffs[spellID] = lib.sharedDebuffs[kid] + return true + end + end + end + end + return false +end + -- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call) -- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache() -- Enhanced with Nampower v2.18+ GetEquippedItems when available @@ -3705,7 +3732,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if gufResult ~= nil then -- GetUnitField was available and searched if gufResult then - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(gufSpellId) == false + local isShared = IsSharedDebuffByIdOrName(lib, gufSpellId, args.name) if isShared then found = true stacks = gufStacks or 0 @@ -3739,7 +3766,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if matched then -- IMPORTANT: Only use fallback for SHARED debuffs -- Personal debuffs must come from tracking table (caster check) - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(debuffSpellID) == false + -- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements) + local isShared = IsSharedDebuffByIdOrName(lib, debuffSpellID, args.name) if isShared then found = true texture = tex @@ -3769,7 +3797,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args) end if matched then -- IMPORTANT: Only use fallback for SHARED debuffs - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false + -- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements) + local isShared = IsSharedDebuffByIdOrName(lib, buffSpellID, args.name) if isShared then found = true texture = tex diff --git a/Core.lua b/Core.lua index 6318b8b..d532238 100644 --- a/Core.lua +++ b/Core.lua @@ -1881,17 +1881,11 @@ function CleveRoids.ParseMsg(msg) -- With a condition block present, build out the conditionals table - -- optional spam/cancel flags (apply only when we actually have a [] block) + -- ! prefix with explicit conditionals: store as a flag for execution-time anti-toggle, + -- rather than injecting an implicit conditional that gets AND'd with user conditions. + -- The anti-toggle check happens in DoWithConditionals before CastSpellByName. if noSpam and noSpam ~= "" then - local spamCond = CleveRoids.GetSpammableConditional(action) - if spamCond then - conditionals[spamCond] = { action } - -- Also create _groups entry so Multi() finds it when combined with explicit conditionals - if not conditionals._groups then - conditionals._groups = {} - end - conditionals._groups[spamCond] = { { values = { action }, operator = "OR" } } - end + conditionals.noSpam = true end if cancelAura and cancelAura ~= "" then conditionals.cancelaura = action @@ -2481,8 +2475,30 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo end end if action == CastSpellByName then - -- Special case: !Attack should use AttackTarget() which doesn't toggle - -- This is more reliable than CastSpellByName("Attack") which toggles on/off + -- ! prefix anti-toggle: prevent CastSpellByName from toggling off active spells. + -- For Attack/AutoShot/Shoot, use the non-toggling AttackTarget() API. + -- For self-buffs/shapeshifts, skip if already active on the player. + if conditionals.noSpam then + local spamType = CleveRoids.spamConditions[msg] + if spamType == "checkchanneled" then + -- Attack/AutoShot/Shoot: use non-toggling API + if msg == CleveRoids.Localized.Attack then + AttackTarget() + if needRetarget then TargetLastTarget() end + conditionals.target = origTarget + return true + end + -- AutoShot/Shoot: checkchanneled prevents toggle (already handled below) + else + -- Self-buff/shapeshift: skip if already active on player + if CleveRoids.ValidatePlayerBuff(msg) then + if needRetarget then TargetLastTarget() end + conditionals.target = origTarget + return false + end + end + end + -- Legacy path: !Attack without explicit conditionals (checkchanneled injected) if msg == CleveRoids.Localized.Attack and conditionals.checkchanneled then AttackTarget() elseif CleveRoids.hasSuperwow and conditionals.target then diff --git a/Init.lua b/Init.lua index 93e0bb1..61e41e4 100644 --- a/Init.lua +++ b/Init.lua @@ -163,6 +163,7 @@ CleveRoids.ignoreKeywords = { action = true, ignoretooltip = true, cancelaura = true, + noSpam = true, -- ! prefix flag: prevent toggle-off at execution time _operators = true, -- Metadata for AND/OR operator tracking _groups = true, -- Grouped conditional values for AND/OR evaluation multiscan = true, -- Processed before Keywords loop (target resolution) diff --git a/Utility.lua b/Utility.lua index ef841ea..fc28e8e 100644 --- a/Utility.lua +++ b/Utility.lua @@ -2996,10 +2996,16 @@ delayedTrackingFrame:SetScript("OnUpdate", function() if not lib.detectedJudgementDebuffIDs[debuffSpellID] then lib.detectedJudgementDebuffIDs[debuffSpellID] = true lib.judgementSpells[debuffSpellID] = true -- Add to refresh list + -- Also register as shared debuff so [debuff]/[nodebuff] fallback detection works + -- for custom Turtle WoW spell IDs not in the hardcoded sharedDebuffs table + if not lib.sharedDebuffs[debuffSpellID] then + lib.sharedDebuffs[debuffSpellID] = 10 -- Standard judgement duration + CleveRoids.InvalidateSpellNameCache() + end if debug then DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh list", + _string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh + shared list", debuffName, debuffSpellID, pending.castSpellID) ) end @@ -4043,7 +4049,13 @@ ev:SetScript("OnEvent", function() }) -- If this is a Judgement spell cast by a Paladin, schedule a scan to find the actual debuff ID - if CleveRoids.playerClass == "PALADIN" and lib.judgementSpells[spellID] then + -- Check by ID (known judgement debuffs) OR by name (Turtle WoW custom cast spell IDs) + local isJudgementCast = lib.judgementSpells[spellID] + if not isJudgementCast and CleveRoids.playerClass == "PALADIN" then + local castName = _SpellInfo(spellID) + isJudgementCast = castName and _string_find(castName, "^Judgement") + end + if CleveRoids.playerClass == "PALADIN" and isJudgementCast then table.insert(lib.pendingJudgements, { timestamp = GetTime(), castSpellID = spellID,