diff --git a/Conditionals.lua b/Conditionals.lua index 6ce8683..4f1b2aa 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -360,7 +360,41 @@ local function Multi(t, func, conditionals, condition) return func(t) and true or false end - -- Check operator type from metadata + -- Check for grouped structure (multiple instances of same conditional) + -- Groups are AND'd together, values within each group use group's operator + if conditionals and conditionals._groups and conditionals._groups[condition] then + local groups = conditionals._groups[condition] + -- All groups must pass (AND between groups) + for _, group in ipairs(groups) do + local groupPassed = false + local groupOp = group.operator or "OR" + + if groupOp == "AND" then + -- AND within group: ALL values must match + groupPassed = true + for _, v in ipairs(group.values) do + if not func(v) then + groupPassed = false + break + end + end + else + -- OR within group: ANY value can match + for _, v in ipairs(group.values) do + if func(v) then + groupPassed = true + break + end + end + end + + -- If any group fails, the whole conditional fails (AND between groups) + if not groupPassed then return false end + end + return true + end + + -- Fallback: Check operator type from metadata (single group / backwards compat) local operatorType = "OR" -- default if conditionals and conditionals._operators and conditionals._operators[condition] then operatorType = conditionals._operators[condition] @@ -394,7 +428,42 @@ local function NegatedMulti(t, func, conditionals, condition) return func(t) and true or false end - -- Check operator type from metadata + -- Check for grouped structure (multiple instances of same conditional) + -- Groups are AND'd together, values within each group use FLIPPED group operator (De Morgan) + if conditionals and conditionals._groups and conditionals._groups[condition] then + local groups = conditionals._groups[condition] + -- All groups must pass (AND between groups) + for _, group in ipairs(groups) do + local groupPassed = false + local groupOp = group.operator or "OR" + + -- FLIPPED from positive conditionals (De Morgan's law for intuitive behavior) + if groupOp == "AND" then + -- AND separator (&) FLIPPED: ANY negation can pass (missing at least one) + for _, v in ipairs(group.values) do + if func(v) then + groupPassed = true + break + end + end + else + -- OR separator (/) FLIPPED: ALL negations must pass (missing all) + groupPassed = true + for _, v in ipairs(group.values) do + if not func(v) then + groupPassed = false + break + end + end + end + + -- If any group fails, the whole conditional fails (AND between groups) + if not groupPassed then return false end + end + return true + end + + -- Fallback: Check operator type from metadata (single group / backwards compat) local operatorType = "OR" -- default if conditionals and conditionals._operators and conditionals._operators[condition] then operatorType = conditionals._operators[condition] @@ -506,10 +575,14 @@ local function _get_debuff_timeleft(unitToken, auraName) local _, guid = UnitExists(unitToken) if guid and CleveRoids.libdebuff and CleveRoids.libdebuff.objects[guid] then -- Check 1-48: debuff slots 1-16 + overflow debuffs in buff slots 1-32 + -- NOTE: Slots 1-16 are dense (break on nil), slots 17-48 are sparse (continue on nil) + -- Overflow debuffs in buff slots are mixed with regular buffs, so we can't break early for i = 1, 48 do local effect, _, _, _, _, duration, timeleft = CleveRoids.libdebuff:UnitDebuff(unitToken, i) - if not effect then break end - if effect == auraName and timeleft and timeleft >= 0 then + -- 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 return timeleft, duration end end @@ -520,8 +593,10 @@ local function _get_debuff_timeleft(unitToken, auraName) if CleveRoids.libdebuff and CleveRoids.libdebuff.UnitDebuff then for idx = 1, 48 do local effect, _, _, _, _, duration, timeleft = CleveRoids.libdebuff:UnitDebuff(unitToken, idx) - if not effect then break end - if effect == auraName and timeleft and timeleft >= 0 then + -- 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 return timeleft, duration end end @@ -1863,8 +1938,10 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- Check 1-48: debuff slots 1-16 + overflow debuffs in buff slots 1-32 for idx = 1, 48 do local effect, _, _, _, _, duration, timeleft, effectCaster = CleveRoids.libdebuff:UnitDebuff(unit, idx, filterCaster) - if not effect then break end - if effect == args.name then + -- 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 local shouldSkip = false -- Auto-detect: If args.mine not specified and this is a personal debuff, only match player casts @@ -1902,7 +1979,33 @@ function CleveRoids.ValidateUnitDebuff(unit, args) end function CleveRoids.ValidatePlayerBuff(args) - return CleveRoids.ValidateAura("player", args, true) + -- First check regular buffs + local found = CleveRoids.ValidateAura("player", args, true) + if found then return true end + + -- Also check shapeshift forms (Cat Form, Bear Form, etc. are not regular buffs) + -- This is needed for !Cat Form syntax to work correctly + local searchName = type(args) == "table" and args.name or args + if searchName then + searchName = string.lower(string.gsub(searchName, "_", " ")) + local numForms = GetNumShapeshiftForms() + for i = 1, numForms do + local icon, name, isActive, isCastable = GetShapeshiftFormInfo(i) + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cff00ff00[ValidatePlayerBuff]|r Form %d: name=%s, isActive=%s, searching=%s", + i, tostring(name), tostring(isActive), searchName)) + end + if name and isActive and string.lower(name) == searchName then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[ValidatePlayerBuff]|r MATCH FOUND - returning true") + end + return true + end + end + end + + return false end function CleveRoids.ValidatePlayerDebuff(args) @@ -2717,9 +2820,24 @@ CleveRoids.Keywords = { end, nomybuff = function(conditionals) - return NegatedMulti(conditionals.nomybuff, function(v) - return not CleveRoids.ValidatePlayerBuff(v) + if CleveRoids.debug then + local vals = conditionals.nomybuff + local valStr = type(vals) == "table" and table.concat(vals, ", ") or tostring(vals) + DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[nomybuff]|r Checking: " .. valStr) + end + local result = NegatedMulti(conditionals.nomybuff, function(v) + local hasBuff = CleveRoids.ValidatePlayerBuff(v) + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cffff00ff[nomybuff]|r ValidatePlayerBuff(%s) = %s, returning %s", + tostring(v), tostring(hasBuff), tostring(not hasBuff))) + end + return not hasBuff end, conditionals, "nomybuff") + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[nomybuff]|r Final result: " .. tostring(result)) + end + return result end, mydebuff = function(conditionals) diff --git a/Core.lua b/Core.lua index 4246838..0d28968 100644 --- a/Core.lua +++ b/Core.lua @@ -1703,6 +1703,11 @@ function CleveRoids.ParseMsg(msg) conditionals[condition] = true else conditionals[condition] = conditionals.action + -- Create first group for non-boolean conditionals + if not conditionals._groups then + conditionals._groups = {} + end + conditionals._groups[condition] = { { values = { conditionals.action }, operator = "OR" } } end else -- existing code for when conditionals[condition] already exists @@ -1716,6 +1721,15 @@ function CleveRoids.ParseMsg(msg) conditionals._operators = {} end conditionals._operators[condition] = "AND" + + -- Add new group for this instance + if not conditionals._groups then + conditionals._groups = {} + end + if not conditionals._groups[condition] then + conditionals._groups[condition] = {} + end + table.insert(conditionals._groups[condition], { values = { conditionals.action }, operator = "OR" }) end else -- Has args. Ensure the key's value is a table and add new arguments. @@ -1729,10 +1743,13 @@ function CleveRoids.ParseMsg(msg) end -- Detect which separator is used: / (OR) or & (AND) - -- Initialize metadata table if needed + -- Initialize metadata tables if needed if not conditionals._operators then conditionals._operators = {} end + if not conditionals._groups then + conditionals._groups = {} + end -- Check which separator is present in the CURRENT args local hasSlash = string.find(args, "/") @@ -1756,14 +1773,18 @@ function CleveRoids.ParseMsg(msg) operatorType = "OR" end - -- Multiple instances of same conditional (comma-separated) = AND logic - if conditionAlreadyExists then - operatorType = "AND" - end - - -- Store the operator type for this conditional + -- Store the operator type for this conditional (for backwards compat) + -- Note: when groups exist, Multi/NegatedMulti will use per-group operators conditionals._operators[condition] = operatorType + -- Create a new group for this conditional instance + -- Structure: { values = { ... }, operator = "OR" or "AND" } + if not conditionals._groups[condition] then + conditionals._groups[condition] = {} + end + local currentGroup = { values = {}, operator = operatorType } + table.insert(conditionals._groups[condition], currentGroup) + -- Split args by the determined separator for _, arg_item in CleveRoids.splitString(args, separator) do local processed_arg = CleveRoids.Trim(arg_item) @@ -1783,6 +1804,7 @@ function CleveRoids.ParseMsg(msg) if not operator or not amount then -- No operator found, treat as simple string argument table.insert(conditionals[condition], processed_arg) + table.insert(currentGroup.values, processed_arg) else local name_to_use = (name and name ~= "") and name or conditionals.action local final_amount_str, num_replacements = string.gsub(amount, "#", "") @@ -1812,27 +1834,33 @@ function CleveRoids.ParseMsg(msg) end if table.getn(comparisons) > 0 then - table.insert(conditionals[condition], { + local entry = { name = CleveRoids.Trim(stat_name), comparisons = comparisons -- Store all comparisons - }) + } + table.insert(conditionals[condition], entry) + table.insert(currentGroup.values, entry) else -- Fallback to single comparison if parsing failed - table.insert(conditionals[condition], { + local entry = { name = CleveRoids.Trim(name_to_use), operator = operator, amount = tonumber(final_amount_str), checkStacks = should_check_stacks - }) + } + table.insert(conditionals[condition], entry) + table.insert(currentGroup.values, entry) end else -- Normal single-comparison conditional (existing behavior) - table.insert(conditionals[condition], { + local entry = { name = CleveRoids.Trim(name_to_use), operator = operator, amount = tonumber(final_amount_str), checkStacks = should_check_stacks - }) + } + table.insert(conditionals[condition], entry) + table.insert(currentGroup.values, entry) end end end @@ -1985,6 +2013,11 @@ end function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBeforeAction, action) local msg, conditionals = CleveRoids.GetParsedMsg(msg) + -- Debug: Log parsed msg and action type + if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then + CleveRoids.Print("|cff888888[EquipLog] DoWithConditionals: parsed msg='" .. tostring(msg) .. "' action=" .. tostring(action) .. "|r") + end + -- No conditionals. Just exit. if not conditionals then if not msg then -- if not even an empty string @@ -2116,6 +2149,9 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo elseif msg == "" or msg == nil then -- Empty action (conditionals passed but no spell to cast) -- For non-spell actions (pet commands, etc.), still execute the action + if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then + CleveRoids.Print("|cffff8800[EquipLog] Empty msg branch - calling action() with no args|r") + end if action and action ~= CastSpellByName then action() result = true @@ -2141,6 +2177,9 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo end else -- For other actions like UseContainerItem etc. + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ff00[EquipLog] Calling action('" .. tostring(msg) .. "')|r") + end action(msg) end end @@ -2692,7 +2731,14 @@ function CleveRoids.DoUse(msg) end function CleveRoids.EquipBagItem(msg, offhand) + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' offhand=" .. tostring(offhand) .. "|r") + end + if CleveRoids.equipInProgress then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cffff0000[EquipLog] Equip already in progress, skipping|r") + end return false end @@ -2704,13 +2750,27 @@ function CleveRoids.EquipBagItem(msg, offhand) local searchTerm = msg local itemId = tonumber(msg) + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff888888[EquipLog] Searching for '" .. tostring(searchTerm) .. "' (slot " .. invslot .. ")|r") + end + -- Check if already equipped in target slot if API.IsItemInSlot(searchTerm, invslot) then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ff00[EquipLog] '" .. tostring(searchTerm) .. "' already in slot " .. invslot .. "|r") + end return true end -- Find the item (works with both ID and name) local itemInfo = API.FindItemFast(searchTerm) + if CleveRoids.equipDebugLog then + if itemInfo then + CleveRoids.Print("|cff888888[EquipLog] FindItemFast found: invID=" .. tostring(itemInfo.inventoryID) .. " bag=" .. tostring(itemInfo.bagID) .. " slot=" .. tostring(itemInfo.slot) .. "|r") + else + CleveRoids.Print("|cffff8800[EquipLog] FindItemFast returned nil|r") + end + end if itemInfo then -- Already equipped in different slot - need to swap if itemInfo.inventoryID then @@ -2732,12 +2792,23 @@ function CleveRoids.EquipBagItem(msg, offhand) if CursorHasItem and CursorHasItem() then EquipCursorItem(invslot) ClearCursor() + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ff00[EquipLog] Equipped '" .. tostring(msg) .. "' from bag " .. itemInfo.bagID .. " slot " .. itemInfo.slot .. "|r") + end return true end end end -- Item not found via v2.18 lookup + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot|r") + -- Debug: Try direct FindPlayerItemSlot call + if FindPlayerItemSlot then + local bag, slot = FindPlayerItemSlot(msg) + CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r") + end + end return false end @@ -2755,6 +2826,9 @@ function CleveRoids.EquipBagItem(msg, offhand) if itemName then msg = itemName else + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cffff8800[EquipLog] Can't resolve item ID " .. tostring(itemId) .. "|r") + end return false -- Can't resolve item ID end end @@ -2808,6 +2882,9 @@ function CleveRoids.EquipBagItem(msg, offhand) end if not item or not item.name then + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found in bags or equipped|r") + end return false end @@ -4597,6 +4674,8 @@ SlashCmdList["CLEVEROID"] = function(msg) DEFAULT_CHAT_FRAME:AddMessage('/cleveroid slotdebug - Debug action slot state (tooltip/range/mana)') DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Slam Rotation (Warrior):|r") DEFAULT_CHAT_FRAME:AddMessage('/cleveroid slamdebug - Show Slam cast time and clip window calculations') + DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Debuff Tracking Debug:|r") + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid debuffdebug [spell] - Debug debuff tracking on target') return end @@ -5569,6 +5648,191 @@ SlashCmdList["CLEVEROID"] = function(msg) return end + -- formdebug (debug shapeshift form detection) + if cmd == "formdebug" or cmd == "form" or cmd == "shapeshiftdebug" then + CleveRoids.Print("|cff88ff88=== Shapeshift Form Debug ===|r") + CleveRoids.Print("Player class: " .. tostring(CleveRoids.playerClass)) + + local numForms = GetNumShapeshiftForms() + CleveRoids.Print("Number of forms: " .. tostring(numForms)) + + local currentIndex = CleveRoids.GetCurrentShapeshiftIndex() + CleveRoids.Print("Current form index: " .. tostring(currentIndex)) + + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== Form Details ===|r") + for i = 1, numForms do + local icon, name, isActive, isCastable = GetShapeshiftFormInfo(i) + local activeStr = isActive and "|cff00ff00ACTIVE|r" or "|cff888888inactive|r" + local castableStr = isCastable and "castable" or "not castable" + CleveRoids.Print(string.format("Form %d: %s - %s (%s)", i, tostring(name), activeStr, castableStr)) + end + + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== ValidatePlayerBuff Tests ===|r") + local testForms = {"Cat Form", "Bear Form", "Dire Bear Form", "Travel Form", "Aquatic Form", "Moonkin Form"} + for _, formName in ipairs(testForms) do + local result = CleveRoids.ValidatePlayerBuff(formName) + local resultStr = result and "|cff00ff00true|r" or "|cffff0000false|r" + CleveRoids.Print(" ValidatePlayerBuff('" .. formName .. "') = " .. resultStr) + end + return + end + + -- debuffdebug (debug debuff tracking on target) + if cmd == "debuffdebug" or cmd == "debuff" or cmd == "trackdebug" then + local searchName = val + if val2 and val2 ~= "" then + searchName = val .. " " .. val2 + end + -- Strip underscores and quotes + if searchName and searchName ~= "" then + searchName = string.gsub(searchName, "_", " ") + searchName = string.gsub(searchName, '"', "") + end + + CleveRoids.Print("|cff88ff88=== Debuff Tracking Debug ===|r") + + local _, guid = UnitExists("target") + if not guid then + CleveRoids.Print("|cffff0000No target selected!|r") + return + end + + local targetName = UnitName("target") or "Unknown" + guid = CleveRoids.NormalizeGUID(guid) + CleveRoids.Print("Target: " .. targetName .. " (GUID: " .. tostring(guid) .. ")") + + -- Show tracking table for this target + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== Tracked Debuffs (libdebuff.objects) ===|r") + local lib = CleveRoids.libdebuff + if lib and lib.objects and lib.objects[guid] then + local count = 0 + for spellID, rec in pairs(lib.objects[guid]) do + if rec and rec.start and rec.duration then + local timeRemaining = rec.duration + rec.start - GetTime() + local spellName = SpellInfo and SpellInfo(spellID) or "Unknown" + local caster = rec.caster or "unknown" + local stacks = rec.stacks or 0 + if timeRemaining > 0 then + CleveRoids.Print(string.format(" |cff00ff00[%d]|r %s: %.1fs left (caster: %s, stacks: %d)", + spellID, spellName, timeRemaining, caster, stacks)) + count = count + 1 + else + CleveRoids.Print(string.format(" |cffff0000[%d]|r %s: EXPIRED %.1fs ago (caster: %s)", + spellID, spellName, -timeRemaining, caster)) + end + end + end + if count == 0 then + CleveRoids.Print(" (no active tracked debuffs)") + end + else + CleveRoids.Print(" (no tracking data for this target)") + end + + -- Show actual debuff slots (1-16 via UnitDebuff, 17-48 via overflow) + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== Debuff Slots (UnitDebuff 1-16) ===|r") + local debuffCount = 0 + for i = 1, 16 do + local texture, stacks, debuffType, spellID = UnitDebuff("target", i) + if texture then + local spellName = SpellInfo and SpellInfo(spellID) or "slot" .. i + CleveRoids.Print(string.format(" Slot %d: [%d] %s (stacks: %d)", + i, spellID or 0, spellName, stacks or 0)) + debuffCount = debuffCount + 1 + end + end + if debuffCount == 0 then + CleveRoids.Print(" (no debuffs in slots 1-16)") + end + + -- Show overflow debuffs in buff slots (17-48) + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== Overflow Debuffs (UnitBuff 1-32 as debuffs 17-48) ===|r") + local overflowCount = 0 + for i = 1, 32 do + local texture, stacks, spellID = UnitBuff("target", i) + if texture and spellID then + -- Check if this might be an overflow debuff by checking libdebuff durations + local isDebuff = lib and lib.durations and lib.durations[spellID] + if isDebuff then + local spellName = SpellInfo and SpellInfo(spellID) or "slot" .. i + CleveRoids.Print(string.format(" Buff Slot %d (=Debuff %d): [%d] %s (stacks: %d) |cffff8800OVERFLOW|r", + i, i + 16, spellID, spellName, stacks or 0)) + overflowCount = overflowCount + 1 + end + end + end + if overflowCount == 0 then + CleveRoids.Print(" (no overflow debuffs detected)") + end + + -- If a specific debuff name was provided, test the conditional + if searchName and searchName ~= "" then + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== Testing [debuff:\"" .. searchName .. "\"] ===|r") + + -- Test ValidateUnitDebuff + local result = CleveRoids.ValidateUnitDebuff("target", { name = searchName }) + CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "'}): " .. + (result and "|cff00ff00true|r" or "|cffff0000false|r")) + + -- Test with time conditional + local resultTime = CleveRoids.ValidateUnitDebuff("target", { name = searchName, operator = "<", amount = 99999 }) + CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "', operator='<', amount=99999}): " .. + (resultTime and "|cff00ff00true|r" or "|cffff0000false|r")) + + -- Look for spell IDs matching this name + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== Spell ID Lookup for \"" .. searchName .. "\" ===|r") + local foundIDs = {} + -- Check Spells table + if CleveRoids.Spells then + for id, name in pairs(CleveRoids.Spells) do + if type(name) == "string" and string.lower(name) == string.lower(searchName) then + table.insert(foundIDs, id) + end + end + end + -- Also check SpellInfo + if SpellInfo then + for id = 1, 30000 do + local name = SpellInfo(id) + if name and string.lower(name) == string.lower(searchName) then + local found = false + for _, existingID in ipairs(foundIDs) do + if existingID == id then found = true break end + end + if not found then + table.insert(foundIDs, id) + end + end + -- Stop early if we found some + if table.getn(foundIDs) > 10 then break end + end + end + + if table.getn(foundIDs) > 0 then + for _, id in ipairs(foundIDs) do + local tracked = lib and lib.objects and lib.objects[guid] and lib.objects[guid][id] + local trackedStr = tracked and "|cff00ff00TRACKED|r" or "|cff888888not tracked|r" + CleveRoids.Print(" SpellID " .. id .. ": " .. trackedStr) + if tracked then + local remaining = tracked.duration + tracked.start - GetTime() + CleveRoids.Print(" -> " .. string.format("%.1fs remaining (caster: %s)", remaining, tracked.caster or "?")) + end + end + else + CleveRoids.Print(" (no spell IDs found for this name)") + end + end + + return + end + -- tooltipdebug (debug spell tooltip scanning) if cmd == "tooltipdebug" or cmd == "ttdebug" or cmd == "spelltooltip" then local spellName = val diff --git a/Init.lua b/Init.lua index 1dd448e..0243a51 100644 --- a/Init.lua +++ b/Init.lua @@ -128,6 +128,7 @@ CleveRoids.ignoreKeywords = { ignoretooltip = true, cancelaura = true, _operators = true, -- Metadata for AND/OR operator tracking + _groups = true, -- Grouped conditional values for AND/OR evaluation } -- TODO: Localize?