--[[ Author: Dennis Werner Garske (DWG) / brian / Mewtiny License: MIT License ]] local _G = _G or getfenv(0) local CleveRoids = _G.CleveRoids or {} -- Permanent cache for name normalization (underscores to spaces) local _normalizedNames = {} -- Cached name normalization function CleveRoids.NormalizeName(name) if not name then return name end local c = _normalizedNames[name] if c then return c end c = string.gsub(name, "_", " ") _normalizedNames[name] = c return c end -- Direct passthrough functions (no caching overhead for normal usage) function CleveRoids.GetCachedTime() return GetTime() end function CleveRoids.GetCachedPlayerHealthPercent() local max = UnitHealthMax("player") return max > 0 and (100 * UnitHealth("player") / max) or 0 end function CleveRoids.GetCachedPlayerPowerPercent() local max = UnitManaMax("player") return max > 0 and (100 * UnitMana("player") / max) or 0 end function CleveRoids.GetCachedPlayerPower() return UnitMana("player") end function CleveRoids.GetCachedTargetHealthPercent() if not UnitExists("target") then return 0 end local max = UnitHealthMax("target") return max > 0 and (100 * UnitHealth("target") / max) or 0 end -- Cooldown uses original function directly function CleveRoids.GetCachedCooldown(name, ignoreGCD) return CleveRoids._GetCooldownUncached(name, ignoreGCD) end -- No-ops for compatibility function CleveRoids.ClearFrameCache() end function CleveRoids.TrackButtonPress() end function CleveRoids.GetCacheStats() return nil end function CleveRoids.SetCacheTTL(ms) end function CleveRoids.SetMacroThrottle(ms) end function CleveRoids.GetMacroThrottle() return 0 end -- ============================================================================ -- PERFORMANCE: Cache spell name -> spell ID mappings for debuff lookups -- This avoids iterating personalDebuffs/sharedDebuffs and calling SpellInfo() repeatedly local _spellNameToIDs = {} -- [spellName] = { spellID1, spellID2, ... } local _spellNameToIDsBuilt = false -- Build the spell name to ID cache (called once on first debuff check) local function BuildSpellNameCache() if _spellNameToIDsBuilt then return end _spellNameToIDsBuilt = true local lib = CleveRoids.libdebuff if not lib then return end local gsub = string.gsub if lib.personalDebuffs then for sid, _ in pairs(lib.personalDebuffs) do local name = SpellInfo(sid) if name then name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") if not _spellNameToIDs[name] then _spellNameToIDs[name] = {} end table.insert(_spellNameToIDs[name], sid) end end end if lib.sharedDebuffs then for sid, _ in pairs(lib.sharedDebuffs) do local name = SpellInfo(sid) if name then name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") if not _spellNameToIDs[name] then _spellNameToIDs[name] = {} end -- Avoid duplicates local found = false for _, existingId in ipairs(_spellNameToIDs[name]) do if existingId == sid then found = true; break end end if not found then table.insert(_spellNameToIDs[name], sid) end end end end end -- Get cached spell IDs for a spell name local function GetSpellIDsForName(spellName) BuildSpellNameCache() return _spellNameToIDs[spellName] end -- Invalidate cache (call if debuff lists change) function CleveRoids.InvalidateSpellNameCache() _spellNameToIDs = {} _spellNameToIDsBuilt = false end -- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call) -- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache() local _equippedItemIDs = {} -- [slot] = itemID (number) local _equippedItemNames = {} -- [slot] = itemName (lowercase string) local _equipmentCacheValid = false local function BuildEquipmentCache() if _equipmentCacheValid then return end _equipmentCacheValid = true -- Clear old data for i = 1, 19 do _equippedItemIDs[i] = nil _equippedItemNames[i] = nil end local string_find = string.find local string_lower = string.lower for slot = 1, 19 do local link = GetInventoryItemLink("player", slot) if link then local _, _, id = string_find(link, "item:(%d+)") local _, _, nameInBrackets = string_find(link, "%[(.+)%]") if id then _equippedItemIDs[slot] = tonumber(id) end if nameInBrackets then _equippedItemNames[slot] = string_lower(nameInBrackets) elseif id then -- Fallback: resolve via GetItemInfo local itemName = GetItemInfo(tonumber(id)) if itemName then _equippedItemNames[slot] = string_lower(itemName) end end end end end -- Invalidate equipment cache (call on UNIT_INVENTORY_CHANGED) function CleveRoids.InvalidateEquipmentCache() _equipmentCacheValid = false end -- ============================================================================ -- PERFORMANCE: Unified item location lookup using CleveRoids.Items cache -- Returns: { type="inventory"|"bag", inventoryID=N } or { type="bag", bag=N, slot=N } -- Returns nil if item not found -- ============================================================================ local string_lower = string.lower local string_find = string.find -- Fast item lookup using cache - O(1) instead of O(n) scan -- @param item: item ID (number) or item name (string) -- @return table with location info, or nil if not found function CleveRoids.FindItemLocation(item) local Items = CleveRoids.Items if not Items then return nil end local itemData = nil -- Case 1: Numeric item ID local numericItem = tonumber(item) if numericItem then -- Check if it's an equipment slot (1-19) if numericItem >= 1 and numericItem <= 19 then local link = GetInventoryItemLink("player", numericItem) if link then return { type = "inventory", inventoryID = numericItem } end return nil end -- Look up by item ID in cache (Items[id] = name) local itemName = Items[numericItem] if itemName then itemData = Items[itemName] end else -- Case 2: String item name if type(item) == "string" and item ~= "" then -- Try exact match first, then lowercase itemData = Items[item] if not itemData or type(itemData) == "string" then local lowerItem = string_lower(item) local resolved = Items[lowerItem] if type(resolved) == "string" then itemData = Items[resolved] elseif type(resolved) == "table" then itemData = resolved end elseif type(itemData) == "string" then -- Resolve indirection (lowercase -> canonical name) itemData = Items[itemData] end end end if not itemData or type(itemData) ~= "table" then return nil end -- Return location info if itemData.inventoryID then return { type = "inventory", inventoryID = itemData.inventoryID, itemData = itemData } elseif itemData.bagID and itemData.slot then return { type = "bag", bag = itemData.bagID, slot = itemData.slot, itemData = itemData } end return nil end -- Fast item existence check using cache -- @param item: item ID (number) or item name (string) -- @return boolean function CleveRoids.HasItemCached(item) return CleveRoids.FindItemLocation(item) ~= nil end -- Fast item cooldown lookup using cache -- @param item: item ID (number) or item name (string) -- @return remainingSeconds, totalDuration, enabled function CleveRoids.GetItemCooldownCached(item) local location = CleveRoids.FindItemLocation(item) if not location then return 0, 0, 0 end local start, duration, enable if location.type == "inventory" then start, duration, enable = GetInventoryItemCooldown("player", location.inventoryID) else start, duration, enable = GetContainerItemCooldown(location.bag, location.slot) end -- Normalize cooldown values start = tonumber(start) or 0 duration = tonumber(duration) or 0 enable = tonumber(enable) or 0 if duration <= 0 or start <= 0 then return 0, 0, enable end local remaining = (start + duration) - GetTime() if remaining < 0 then remaining = 0 end return remaining, duration, enable end --This table maps stat keys to the functions that retrieve their values. local stat_checks = { -- Base Stats (Corrected to use the 'effective' stat with gear) str = function() local _, effective = UnitStat("player", 1); return effective end, strength = function() local _, effective = UnitStat("player", 1); return effective end, agi = function() local _, effective = UnitStat("player", 2); return effective end, agility = function() local _, effective = UnitStat("player", 2); return effective end, stam = function() local _, effective = UnitStat("player", 3); return effective end, stamina = function() local _, effective = UnitStat("player", 3); return effective end, int = function() local _, effective = UnitStat("player", 4); return effective end, intellect = function() local _, effective = UnitStat("player", 4); return effective end, spi = function() local _, effective = UnitStat("player", 5); return effective end, spirit = function() local _, effective = UnitStat("player", 5); return effective end, -- Combat Ratings (Corrected to use UnitAttackPower and UnitRangedAttackPower) ap = function() local base, pos, neg = UnitAttackPower("player"); return base + pos + neg end, attackpower = function() local base, pos, neg = UnitAttackPower("player"); return base + pos + neg end, rap = function() local base, pos, neg = UnitRangedAttackPower("player"); return base + pos + neg end, rangedattackpower = function() local base, pos, neg = UnitRangedAttackPower("player"); return base + pos + neg end, healing = function() return GetBonusHealing() end, healingpower = function() return GetBonusHealing() end, -- Bonus Spell Damage by School arcane_power = function() return GetSpellBonusDamage(6) end, fire_power = function() return GetSpellBonusDamage(3) end, frost_power = function() return GetSpellBonusDamage(4) end, nature_power = function() return GetSpellBonusDamage(2) end, shadow_power = function() return GetSpellBonusDamage(5) end, -- Defensive Stats armor = function() local _, effective = UnitArmor("player"); return effective end, defense = function() local base, modifier = UnitDefense("player") return (base or 0) + (modifier or 0) end, -- Resistances arcane_res = function() local _, val = UnitResistance("player", 7); return val end, fire_res = function() local _, val = UnitResistance("player", 3); return val end, frost_res = function() local _, val = UnitResistance("player", 5); return val end, nature_res = function() local _, val = UnitResistance("player", 4); return val end, shadow_res = function() local _, val = UnitResistance("player", 6); return val end } -- PERFORMANCE: Avoid creating wrapper tables for single values local function And(t, func) if type(func) ~= "function" then return false end -- PERFORMANCE: Handle non-table case without allocation if type(t) ~= "table" then return func(t) and true or false end for k, v in pairs(t) do if not func(v) then return false end end return true end local function Or(t, func) if type(func) ~= "function" then return false end -- PERFORMANCE: Handle non-table case without allocation if type(t) ~= "table" then return func(t) and true or false end for k, v in pairs(t) do if func(v) then return true end end return false end -- Helper to choose And() or Or() based on operator metadata -- For positive conditionals (hp, power, cooldown, etc.): -- - OR separator (/) uses Or() logic -> ANY value can match -- - AND separator (&) uses And() logic -> ALL values must match local function Multi(t, func, conditionals, condition) if type(func) ~= "function" then return false end -- PERFORMANCE: Handle non-table case without allocation if type(t) ~= "table" then return func(t) and true or false end -- Check operator type from metadata local operatorType = "OR" -- default if conditionals and conditionals._operators and conditionals._operators[condition] then operatorType = conditionals._operators[condition] end if operatorType == "AND" then -- AND separator (&): ALL must match for k, v in pairs(t) do if not func(v) then return false end end return true else -- OR separator (/) [default]: ANY can match for k, v in pairs(t) do if func(v) then return true end end return false end end -- Helper to choose And() or Or() based on operator metadata -- For negated conditionals (nomybuff, nozone, etc.), operators are FLIPPED (De Morgan's law): -- - OR separator (/) [default]: ALL must be missing (e.g., nobuff:X/Y = no X AND no Y) -- - AND separator (&): ANY can be missing (e.g., nobuff:X&Y = no X OR no Y) -- This matches natural language: "no X or Y" intuitively means "neither X nor Y" local function NegatedMulti(t, func, conditionals, condition) if type(func) ~= "function" then return false end -- PERFORMANCE: Handle non-table case without allocation if type(t) ~= "table" then return func(t) and true or false end -- Check operator type from metadata local operatorType = "OR" -- default if conditionals and conditionals._operators and conditionals._operators[condition] then operatorType = conditionals._operators[condition] end -- FLIPPED from positive conditionals (De Morgan's law for intuitive behavior) if operatorType == "AND" then -- AND separator (&): ANY negation can pass (missing at least one) for k, v in pairs(t) do if func(v) then return true end end return false else -- OR separator (/) [default]: ALL negations must pass (missing all) for k, v in pairs(t) do if not func(v) then return false end end return true end end -- ============================================================================ -- THREAT TRACKING (reads server data via CHAT_MSG_ADDON like TWThreat) -- ============================================================================ -- Storage for threat data CleveRoids.ThreatData = { playerName = UnitName("player"), threats = {}, -- [playerName] = { threat, perc, tank, melee } lastUpdate = 0, } -- Parse threat packet from server (same format as TWThreat) -- Format: TWTv4=player1:tank:threat:perc:melee;player2:tank:threat:perc:melee;... local function ParseThreatPacket(packet) local threatApi = "TWTv4=" local startPos = string.find(packet, threatApi, 1, true) if not startPos then return end local playersString = string.sub(packet, startPos + string.len(threatApi)) local playerName = CleveRoids.ThreatData.playerName -- Clear old data CleveRoids.ThreatData.threats = {} CleveRoids.ThreatData.lastUpdate = GetTime() -- Split by semicolon for playerData in string.gfind(playersString, "[^;]+") do -- Split by colon: player:tank:threat:perc:melee local parts = {} for part in string.gfind(playerData, "[^:]+") do table.insert(parts, part) end if parts[1] and parts[2] and parts[3] and parts[4] and parts[5] then local name = parts[1] local tank = parts[2] == "1" local threat = tonumber(parts[3]) or 0 local perc = tonumber(parts[4]) or 0 local melee = parts[5] == "1" CleveRoids.ThreatData.threats[name] = { threat = threat, perc = perc, tank = tank, melee = melee, } end end end -- Get player's threat percentage function CleveRoids.GetPlayerThreatPercent() local playerName = CleveRoids.ThreatData.playerName local data = CleveRoids.ThreatData.threats[playerName] if data then return data.perc end return nil end -- Create frame to listen for threat addon messages local threatFrame = CreateFrame("Frame", "CleveRoidsThreatFrame") threatFrame:RegisterEvent("CHAT_MSG_ADDON") threatFrame:SetScript("OnEvent", function() if event == "CHAT_MSG_ADDON" then -- arg1 = prefix, arg2 = message, arg3 = channel, arg4 = sender if arg2 and string.find(arg2, "TWTv4=", 1, true) then ParseThreatPacket(arg2) end end end) -- ============================================================================ -- pfUI debuff time helper (Vanilla 1.12.1 / Lua 5.0 safe) local function PFUI_HasLibDebuff() return type(pfUI) == "table" and type(pfUI.api) == "table" and type(pfUI.api.libdebuff) == "table" and type(pfUI.api.libdebuff.UnitDebuff) == "function" end -- Helper: Get debuff time-left (seconds) from CleveRoids.libdebuff only local function _get_debuff_timeleft(unitToken, auraName) -- SuperWoW path: GUID-based lookup -- SuperWoW debuff slots: 1-16 are regular debuffs, 17-48 overflow to buff slots 1-32 if CleveRoids.hasSuperwow then 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 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 return timeleft, duration end end end end -- Non-SuperWoW fallback 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 return timeleft, duration end end end return nil, nil end -- Validates that the given target is either friend (if [help]) or foe (if [harm]) -- target: The unit id to check -- help: Optional. If set to 1 then the target must be friendly. If set to 0 it must be an enemy. -- remarks: Will always return true if help is not given -- returns: Whether or not the given target can either be attacked or supported, depending on help function CleveRoids.CheckHelp(target, help) if help == nil then return true end if help then return UnitCanAssist("player", target) else return UnitCanAttack("player", target) end end -- Ensures the validity of the given target -- target: The unit id to check -- help: Optional. If set to 1 then the target must be friendly. If set to 0 it must be an enemy -- returns: Whether or not the target is a viable target function CleveRoids.IsValidTarget(target, help) -- If the conditional is not for @mouseover, use the existing logic. if target ~= "mouseover" then if not UnitExists(target) or not CleveRoids.CheckHelp(target, help) then return false end return true end -- --- START OF PATCH --- -- New logic to handle [@mouseover] with pfUI compatibility. local effectiveMouseoverUnit = "mouseover" -- Start with the default game token. -- Check if the default mouseover exists. If not, check pfUI's internal data, -- which is necessary because pfUI frames don't always update the default token. if not UnitExists(effectiveMouseoverUnit) then if pfUI and pfUI.uf and pfUI.uf.mouseover and pfUI.uf.mouseover.unit and UnitExists(pfUI.uf.mouseover.unit) then -- If pfUI has a valid mouseover unit recorded, use that instead. effectiveMouseoverUnit = pfUI.uf.mouseover.unit else -- If neither the default token nor the pfUI unit exists, there's no valid mouseover. return false end end -- --- END OF PATCH --- -- Finally, perform the help/harm check on the determined mouseover unit (either from the game or from pfUI). if not UnitExists(effectiveMouseoverUnit) or not CleveRoids.CheckHelp(effectiveMouseoverUnit, help) then return false end return true end -- Returns the current shapeshift / stance index -- returns: The index of the current shapeshift form / stance. 0 if in no shapeshift form / stance function CleveRoids.GetCurrentShapeshiftIndex() if CleveRoids.playerClass == "PRIEST" then return CleveRoids.ValidatePlayerBuff(CleveRoids.Localized.Spells["Shadowform"]) and 1 or 0 elseif CleveRoids.playerClass == "ROGUE" then return CleveRoids.ValidatePlayerBuff(CleveRoids.Localized.Spells["Stealth"]) and 1 or 0 end for i=1, GetNumShapeshiftForms() do _, _, active = GetShapeshiftFormInfo(i) if active then return i end end return 0 end function CleveRoids.CancelAura(auraName) local ix = 0 auraName = string.lower(string.gsub(auraName, "_"," ")) while true do local aura_ix = GetPlayerBuff(ix,"HELPFUL") ix = ix + 1 if aura_ix == -1 then break end if CleveRoids.hasSuperwow then local bid = GetPlayerBuffID(aura_ix) bid = (bid < -1) and (bid + 65536) or bid if string.lower(SpellInfo(bid)) == auraName then CancelPlayerBuff(aura_ix) return true end else AuraScanTooltip:SetPlayerBuff(aura_ix) local name = string.lower(getglobal("AuraScanTooltipTextLeft1"):GetText()) if name == auraName then CancelPlayerBuff(aura_ix) break end end end return false end function CleveRoids.HasGearEquipped(gearId) if not gearId then return false end -- PERFORMANCE: Build/refresh equipment cache if needed BuildEquipmentCache() -- Handle both numeric IDs and string IDs like "5196" local wantId = tonumber(gearId) local wantName = (type(gearId) == "string" and not wantId) and string.lower(gearId) or nil -- PERFORMANCE: Use cached data instead of scanning all slots for slot = 1, 19 do if wantId and _equippedItemIDs[slot] == wantId then return true end if wantName and _equippedItemNames[slot] == wantName then return true end end return false end -- Checks whether or not the given weaponType is currently equipped -- weaponType: The name of the weapon's type (e.g. Axe, Shield, etc.) -- returns: True when equipped, otherwhise false function CleveRoids.HasWeaponEquipped(weaponType) if not CleveRoids.WeaponTypeNames[weaponType] then return false end local slotName = CleveRoids.WeaponTypeNames[weaponType].slot local localizedName = CleveRoids.WeaponTypeNames[weaponType].name local slotId = GetInventorySlotInfo(slotName) local slotLink = GetInventoryItemLink("player",slotId) if not slotLink then return false end local _,_,itemId = string.find(slotLink,"item:(%d+)") if not itemId then -- Also good to check if itemId was found return false end local _name,_link,_,_lvl,_type,subtype = GetItemInfo(itemId) -- just had to be special huh? local fist = string.find(subtype,"^Fist") -- drops things like the One-Handed prefix local _,_,subtype = string.find(subtype,"%s?(%S+)$") if subtype == localizedName or (fist and (CleveRoids.WeaponTypeNames[weaponType].name == CleveRoids.Localized.FistWeapon)) then return true end return false end -- Checks whether or not the given UnitId is in your party or your raid -- target: The UnitId of the target to check -- groupType: The name of the group type your target has to be in ("party" or "raid") -- returns: True when the given target is in the given groupType, otherwhise false function CleveRoids.IsTargetInGroupType(target, groupType) local groupSize = (groupType == "raid") and 40 or 5 for i = 1, groupSize do if UnitIsUnit(groupType..i, target) then return true end end return false end function CleveRoids.GetSpammableConditional(name) return CleveRoids.spamConditions[name] or "nomybuff" end -- Checks whether or not we're currently casting a spell with cast time -- Returns TRUE if we should allow the cast (not casting, or not casting the specified spell) -- Returns FALSE if we should block the cast (currently casting) function CleveRoids.CheckCasting(castingSpell) -- No parameter: check if we're casting ANYTHING if not castingSpell or castingSpell == "" then -- Time-based prediction: if we know cast duration, check if it should be done if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then local elapsed = GetTime() - CleveRoids.castStartTime local remaining = CleveRoids.castDuration - elapsed -- If cast should be done (with 0.1s grace period), treat as not casting if remaining <= 0.1 then CleveRoids.CurrentSpell.type = "" return true end end return CleveRoids.CurrentSpell.type ~= "cast" end -- With parameter: check if we're casting a specific spell local spellName = string.gsub(CleveRoids.CurrentSpell.spellName or "", "%(.-%)%s*", "") local casting = string.gsub(castingSpell, "%(.-%)%s*", "") -- If we're casting this specific spell, block the recast if CleveRoids.CurrentSpell.type == "cast" and spellName == casting then return false end -- Not casting the specified spell, allow the cast return true end -- Checks whether or not we're currently casting a channeled spell -- Returns TRUE if we should allow the cast (not channeling, or not channeling the specified spell) -- Returns FALSE if we should block the cast (currently channeling) function CleveRoids.CheckChanneled(channeledSpell) -- No parameter: check if we're channeling ANYTHING if not channeledSpell or channeledSpell == "" then -- Time-based prediction: if we know channel duration, check if it should be done if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then local elapsed = GetTime() - CleveRoids.channelStartTime local remaining = CleveRoids.channelDuration - elapsed -- If channel should be done (with 0.1s grace period), treat as not channeling if remaining <= 0.1 then CleveRoids.CurrentSpell.type = "" return true end end return CleveRoids.CurrentSpell.type ~= "channeled" end -- Remove the "(Rank X)" part from the spells name in order to allow downranking local spellName = string.gsub(CleveRoids.CurrentSpell.spellName or "", "%(.-%)%s*", "") local channeled = string.gsub(channeledSpell, "%(.-%)%s*", "") -- If we're channeling this specific spell, block the recast if CleveRoids.CurrentSpell.type == "channeled" and spellName == channeled then return false end -- Special cases for auto-attacks if channeled == CleveRoids.Localized.Attack then return not CleveRoids.CurrentSpell.autoAttack end if channeled == CleveRoids.Localized.AutoShot then return not CleveRoids.CurrentSpell.autoShot end if channeled == CleveRoids.Localized.Shoot then return not CleveRoids.CurrentSpell.wand end -- If none of the special cases matched, allow the cast (not channeling the specified spell) return true end function CleveRoids.ValidateComboPoints(operator, amount) if not operator or not amount then return false end local points = GetComboPoints() if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](points, amount) end return false end -- Validates swing timer percentage for SP_SwingTimer addon integration -- operator: Comparison operator (>, <, =, >=, <=, ~=) -- amount: Percentage of swing time elapsed (e.g., 20 means 20% of swing has elapsed) -- returns: True if percentElapsed [operator] amount function CleveRoids.ValidateSwingTimer(operator, amount) if not operator or not amount then return false end -- Check if SP_SwingTimer is loaded by checking for st_timer global if st_timer == nil then -- Only show error once per session if not CleveRoids._swingTimerErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swingtimer] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) CleveRoids._swingTimerErrorShown = true end return false end -- Get player's attack speed (main hand) local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return false end -- Calculate percentage of swing elapsed -- st_timer counts down from attackSpeed to 0 (time remaining) -- So: timeElapsed = attackSpeed - st_timer local timeElapsed = attackSpeed - st_timer local percentElapsed = (timeElapsed / attackSpeed) * 100 -- Compare percent elapsed against threshold if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](percentElapsed, amount) end return false end -- Constants for Slam window calculations local GCD_DURATION = 1.5 -- Global cooldown in seconds local DEFAULT_SLAM_CAST = 2.5 -- Default Slam cast time in Turtle WoW -- Cache for Slam cast time from tooltip local cachedSlamCastTime = nil local slamCastTimeLastUpdate = 0 local SLAM_CACHE_DURATION = 2 -- Re-scan tooltip every 2 seconds -- Hidden tooltip for scanning spell info local SlamScanTooltip = nil -- Create hidden tooltip for scanning (once) local function GetSlamScanTooltip() if not SlamScanTooltip then SlamScanTooltip = CreateFrame("GameTooltip", "CleveRoidsSlamScanTooltip", nil, "GameTooltipTemplate") SlamScanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") end return SlamScanTooltip end -- Parse cast time from tooltip text (e.g., "1.5 sec cast" or "1.59 sec cast") local function ParseCastTimeFromText(text) if not text then return nil end -- Match patterns like "1.5 sec cast", "1.59 sec cast", "2 sec cast" local castTime = string.match(text, "(%d+%.?%d*) sec cast") if castTime then return tonumber(castTime) end return nil end -- Get Slam's spellbook slot local function GetSlamSpellSlot() -- Search through spellbook for Slam local i = 1 while true do local spellName, spellRank = GetSpellName(i, BOOKTYPE_SPELL) if not spellName then break end if spellName == "Slam" then return i, BOOKTYPE_SPELL end i = i + 1 end return nil, nil end -- Expose for debug command CleveRoids.GetSlamSpellSlot = GetSlamSpellSlot -- Scan Slam tooltip for cast time local function ScanSlamCastTime() local slot, bookType = GetSlamSpellSlot() if not slot then return nil end local tooltip = GetSlamScanTooltip() tooltip:ClearLines() tooltip:SetSpell(slot, bookType) -- Scan tooltip lines for cast time for i = 1, tooltip:NumLines() do local leftText = getglobal("CleveRoidsSlamScanTooltipTextLeft" .. i) if leftText then local text = leftText:GetText() local castTime = ParseCastTimeFromText(text) if castTime then return castTime end end local rightText = getglobal("CleveRoidsSlamScanTooltipTextRight" .. i) if rightText then local text = rightText:GetText() local castTime = ParseCastTimeFromText(text) if castTime then return castTime end end end return nil end -- Get Slam's cast time in seconds (with caching) -- Reads from spellbook tooltip to get accurate cast time with haste/talents function CleveRoids.GetSlamCastTime() local now = GetTime() -- Use cached value if still valid if cachedSlamCastTime and (now - slamCastTimeLastUpdate) < SLAM_CACHE_DURATION then return cachedSlamCastTime end -- Try to scan tooltip for cast time local castTime = ScanSlamCastTime() if castTime and castTime > 0 then cachedSlamCastTime = castTime slamCastTimeLastUpdate = now return castTime end -- Fall back to default return DEFAULT_SLAM_CAST end -- Force refresh of cached Slam cast time (call when buffs change) function CleveRoids.RefreshSlamCastTime() cachedSlamCastTime = nil slamCastTimeLastUpdate = 0 end -- Calculate maximum elapsed swing timer % to cast Slam without clipping auto-attack -- Formula: MaxSlamPercent = (SwingTimer - SlamCastTime) / SwingTimer * 100 function CleveRoids.GetSlamWindowPercent() local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return 0 end local slamCastTime = CleveRoids.GetSlamCastTime() local maxSlamStart = attackSpeed - slamCastTime if maxSlamStart <= 0 then return 0 end -- Slam cast time exceeds swing timer return (maxSlamStart / attackSpeed) * 100 end -- Calculate maximum elapsed swing timer % to cast instant without clipping NEXT Slam -- Scenario: No Slam this swing, cast instant, then Slam next swing without clipping -- Formula: MaxInstantPercent = (2 * SwingTimer - SlamCastTime - GCD) / SwingTimer * 100 function CleveRoids.GetInstantWindowPercent() local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return 0 end local slamCastTime = CleveRoids.GetSlamCastTime() local maxInstantStart = (2 * attackSpeed) - slamCastTime - GCD_DURATION if maxInstantStart <= 0 then return 0 end -- Window is impossible with current timings return (maxInstantStart / attackSpeed) * 100 end -- Validate if current swing timer is within the Slam window (no clip) -- Returns true if casting Slam NOW will NOT clip the auto-attack function CleveRoids.ValidateNoSlamClip() -- Check if SP_SwingTimer is loaded if st_timer == nil then if not CleveRoids._slamClipErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [noslamclip] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) CleveRoids._slamClipErrorShown = true end return false end local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return false end local timeElapsed = attackSpeed - st_timer local percentElapsed = (timeElapsed / attackSpeed) * 100 local maxPercent = CleveRoids.GetSlamWindowPercent() return percentElapsed <= maxPercent end -- Validate if current swing timer is within the instant window for next Slam -- Returns true if casting an instant NOW will NOT cause the NEXT Slam to clip function CleveRoids.ValidateNoNextSlamClip() -- Check if SP_SwingTimer is loaded if st_timer == nil then if not CleveRoids._slamClipErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [nonextslamclip] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) CleveRoids._slamClipErrorShown = true end return false end local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return false end local timeElapsed = attackSpeed - st_timer local percentElapsed = (timeElapsed / attackSpeed) * 100 local maxPercent = CleveRoids.GetInstantWindowPercent() return percentElapsed <= maxPercent end function CleveRoids.ValidateLevel(unit, operator, amount) if not unit or not operator or not amount then return false end local level = UnitLevel(unit) -- Treat skull/boss mobs (??) as level 63 if level == -1 then level = 63 end if level and CleveRoids.operators[operator] then return CleveRoids.comparators[operator](level, amount) end return false end --- Validates a threat percentage conditional using server threat data. --- Usage: [threat:>80] [threat:<50] [threat:=100] --- operator: Comparison operator (>, <, =, >=, <=, ~=) --- amount: Threat percentage (0-100+, where 100 = will pull aggro) --- returns: True if playerThreat [operator] amount --- Note: Requires TWThreat addon to be running (sends threat requests to server) function CleveRoids.ValidateThreat(operator, amount) if not operator or not amount then return false end -- Get threat percentage from parsed server data local threatpct = CleveRoids.GetPlayerThreatPercent() -- No threat data available if threatpct == nil then return false end -- Compare threat percentage against threshold if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](threatpct, amount) end return false end --- Validates a Time-To-Kill conditional using TimeToKill addon. --- Usage: [ttk:<10] [ttk:>30] [ttk:=5] --- operator: Comparison operator (>, <, =, >=, <=, ~=) --- amount: Time in seconds until target death --- returns: True if TTK [operator] amount function CleveRoids.ValidateTTK(operator, amount) if not operator or not amount then return false end -- Check if TimeToKill is loaded if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTK) ~= "function" then -- Only show error once per session if not CleveRoids._ttkErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [ttk] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) CleveRoids._ttkErrorShown = true end return false end local ttk = TimeToKill.GetTTK() if ttk == nil then return false -- Not tracking TTK end -- Compare TTK against threshold if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](ttk, amount) end return false end --- Validates a Time-To-Execute conditional using TimeToKill addon. --- Usage: [tte:<5] [tte:>10] --- operator: Comparison operator (>, <, =, >=, <=, ~=) --- amount: Time in seconds until target reaches 20% HP --- returns: True if TTE [operator] amount function CleveRoids.ValidateTTE(operator, amount) if not operator or not amount then return false end -- Check if TimeToKill is loaded if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTE) ~= "function" then if not CleveRoids._ttkErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [tte] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) CleveRoids._ttkErrorShown = true end return false end local tte = TimeToKill.GetTTE() if tte == nil then return false -- Not tracking or already in execute phase end if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](tte, amount) end return false end function CleveRoids.ValidateKnown(args) if not args then return false end if table.getn(CleveRoids.Talents) == 0 then CleveRoids.IndexTalents() end local effective_name_to_check local original_args_for_rank_check = args if type(args) ~= "table" then effective_name_to_check = args args = { name = args } else effective_name_to_check = args.name end local spell = CleveRoids.GetSpell(effective_name_to_check) local talent_points = nil if not spell then talent_points = CleveRoids.GetTalent(effective_name_to_check) end if not spell and talent_points == nil then return false end local arg_amount = nil local arg_operator = nil if type(original_args_for_rank_check) == "table" then arg_amount = original_args_for_rank_check.amount arg_operator = original_args_for_rank_check.operator end if spell then local spell_rank_str = spell.rank or (spell.highest and spell.highest.rank) or "" -- FLEXIBLY extract just the number from the rank string local _, _, spell_rank_num_str = string.find(spell_rank_str, "(%d+)") if not arg_amount and not arg_operator then return true elseif arg_amount and arg_operator and CleveRoids.operators[arg_operator] and spell_rank_num_str and spell_rank_num_str ~= "" then local numeric_rank = tonumber(spell_rank_num_str) if numeric_rank then return CleveRoids.comparators[arg_operator](numeric_rank, arg_amount) else return false end else return false end elseif talent_points ~= nil then if not arg_amount and not arg_operator then return talent_points > 0 elseif arg_amount and arg_operator and CleveRoids.operators[arg_operator] then return CleveRoids.comparators[arg_operator](talent_points, arg_amount) else return false end end return false end function CleveRoids.ValidateResting() return IsResting() end -- TODO: refactor numeric comparisons... -- Checks whether or not the given unit has power in percent vs the given amount -- unit: The unit we're checking -- operator: valid comparitive operator symbol -- amount: The required amount -- returns: True or false function CleveRoids.ValidatePower(unit, operator, amount) if not unit or not operator or not amount then return false end local powerPercent = 100 / UnitManaMax(unit) * UnitMana(unit) if powerPercent and CleveRoids.operators[operator] then return CleveRoids.comparators[operator](powerPercent, amount) end return false end -- Checks whether or not the given unit has current power vs the given amount -- unit: The unit we're checking -- operator: valid comparitive operator symbol -- amount: The required amount -- returns: True or false function CleveRoids.ValidateRawPower(unit, operator, amount) if not unit or not operator or not amount then return false end local power = UnitMana(unit) if power and CleveRoids.operators[operator] then return CleveRoids.comparators[operator](power, amount) end return false end -- Raw caster-form mana for druids (SuperWoW: 2nd return of UnitMana) function CleveRoids.ValidateDruidRawMana(unit, operator, amount) unit = unit or "player" if not operator or amount == nil then return false end if (CleveRoids.playerClass ~= "DRUID") then return false end -- SuperWoW returns: current-form power, caster-form mana local _, casterMana = UnitMana(unit) -- Fallback: if for some reason we didn't get a 2nd value and we're in caster form now if type(casterMana) ~= "number" then if UnitPowerType and UnitPowerType(unit) == 0 then casterMana = UnitMana(unit) else return false end end local cmp = CleveRoids.comparators and CleveRoids.comparators[operator] return cmp and cmp(casterMana, amount) or false end -- Checks whether or not the given unit has a power deficit vs the amount specified -- unit: The unit we're checking -- operator: valid comparitive operator symbol -- amount: The required amount -- returns: True or false function CleveRoids.ValidatePowerLost(unit, operator, amount) if not unit or not operator or not amount then return false end local powerLost = UnitManaMax(unit) - UnitMana(unit) if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](powerLost, amount) end return false end -- Checks whether or not the given unit has hp in percent vs the given amount -- unit: The unit we're checking -- operator: valid comparitive operator symbol -- amount: The required amount -- returns: True or false function CleveRoids.ValidateHp(unit, operator, amount) if not unit or not operator or not amount then return false end local hpPercent = 100 / UnitHealthMax(unit) * UnitHealth(unit) if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](hpPercent, amount) end return false end -- Checks whether or not the given unit has hp vs the given amount -- unit: The unit we're checking -- operator: valid comparitive operator symbol -- amount: The required amount -- returns: True or false function CleveRoids.ValidateRawHp(unit, operator, amount) if not unit or not operator or not amount then return false end local rawhp = UnitHealth(unit) if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](rawhp, amount) end return false end -- Checks whether or not the given unit has an hp deficit vs the amount specified -- unit: The unit we're checking -- operator: valid comparitive operator symbol -- amount: The required amount -- returns: True or false function CleveRoids.ValidateHpLost(unit, operator, amount) if not unit or not operator or not amount then return false end local hpLost = UnitHealthMax(unit) - UnitHealth(unit) if CleveRoids.operators[operator] then return CleveRoids.comparators[operator](hpLost, amount) end return false end -- Checks whether the given creatureType is the same as the target's creature type -- creatureType: The type to check -- target: The target's unitID -- returns: True or false -- remarks: Allows for both localized and unlocalized type names function CleveRoids.ValidateCreatureType(creatureType, target) if not target then return false end local targetType = UnitCreatureType(target) if not targetType then return false end -- ooze or silithid etc local ct = string.lower(creatureType) local cl = UnitClassification(target) if (ct == "boss" and "worldboss" or ct) == cl then return true end if string.lower(creatureType) == "boss" then creatureType = "worldboss" end local englishType = CleveRoids.Localized.CreatureTypes[targetType] return ct == string.lower(targetType) or creatureType == englishType end -- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed function CleveRoids.ValidateCooldown(args, ignoreGCD) if not args then return false end local name if type(args) ~= "table" then -- PERFORMANCE: Use cached normalization name = CleveRoids.NormalizeName(args) -- If this is a numeric slot (1-19), resolve to the equipped item's name local slotNum = tonumber(name) if slotNum and slotNum >= 1 and slotNum <= 19 then local link = GetInventoryItemLink("player", slotNum) if link then local _, _, itemName = string.find(link, "%[(.+)%]") if itemName then name = itemName end end end args = {name = name} else if args.name then -- PERFORMANCE: Use cached normalization name = CleveRoids.NormalizeName(args.name) -- If this is a numeric slot (1-19), resolve to the equipped item's name local slotNum = tonumber(name) if slotNum and slotNum >= 1 and slotNum <= 19 then local link = GetInventoryItemLink("player", slotNum) if link then local _, _, itemName = string.find(link, "%[(.+)%]") if itemName then name = itemName end end end args.name = name else name = args.name end end -- PERFORMANCE: GetCooldown is now cached per-frame local expires = CleveRoids.GetCooldown(args.name, ignoreGCD) local now = CleveRoids.GetCachedTime() if not args.operator and not args.amount then return expires > now elseif CleveRoids.operators[args.operator] then return CleveRoids.comparators[args.operator](expires - now, args.amount) end end function CleveRoids.GetPlayerAura(index, isbuff) if not index then return false end local buffType = isbuff and "HELPFUL" or "HARMFUL" local bid = GetPlayerBuff(index, buffType) if bid < 0 then return end local spellID = CleveRoids.hasSuperwow and GetPlayerBuffID(bid) return GetPlayerBuffTexture(bid), GetPlayerBuffApplications(bid), spellID, GetPlayerBuffTimeLeft(bid) end function CleveRoids.ValidateAura(unit, args, isbuff) if not args or not UnitExists(unit) then return false end if not CleveRoids.hasSuperwow then return false end if type(args) ~= "table" then args = {name = args} end local isPlayer = UnitIsUnit(unit, "player") local found = false local stacks, remaining local i = isPlayer and 0 or 1 -- PERFORMANCE: Cache lowercased search name to avoid repeated string.lower calls local searchName = args.name and string.lower(args.name) -- Primary search: BUFFS if isbuff==true, DEBUFFS if isbuff==false while true do local texture local current_spellID = nil if isPlayer then -- GetPlayerAura(index, isbuff) => texture, stacks, spellID, timeLeft texture, stacks, current_spellID, remaining = CleveRoids.GetPlayerAura(i, isbuff) else if isbuff then -- UnitBuff => texture, stacks, spellID texture, stacks, current_spellID = UnitBuff(unit, i) else -- UnitDebuff => texture, stacks, debuffType, spellID texture, stacks, _, current_spellID = UnitDebuff(unit, i) end remaining = nil end if not texture then break end if current_spellID and searchName then local auraName = SpellInfo(current_spellID) if auraName then -- Strip rank for comparison (handles "Moonfire (Rank 7)" vs "moonfire") local baseName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "") if string.lower(baseName) == searchName or string.lower(auraName) == searchName then found = true break end end end i = i + 1 end -- Overflow handling: when searching DEBUFFS on non-players, also scan BUFFS if not isbuff and not isPlayer and not found and searchName then i = 1 while true do local texture local current_spellID = nil -- UnitBuff => texture, stacks, spellID texture, stacks, current_spellID = UnitBuff(unit, i) if not texture then break end if current_spellID then local auraName = SpellInfo(current_spellID) if auraName then -- Strip rank for comparison local baseName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "") if string.lower(baseName) == searchName or string.lower(auraName) == searchName then found = true break end end end i = i + 1 end end local ops = CleveRoids.operators -- Handle multi-comparison (e.g., >0&<10) if args.comparisons and type(args.comparisons) == "table" then if not found then return false -- Aura doesn't exist, so all comparisons fail end -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not ops[comp.operator] then return false -- Invalid operator end local value_to_check if comp.checkStacks then value_to_check = stacks or -1 elseif isPlayer then value_to_check = remaining or -1 else -- Non-player units don't have remaining time, only check existence return found end if not CleveRoids.comparators[comp.operator](value_to_check, comp.amount) then return false -- One comparison failed end end return true -- All comparisons passed end -- Single comparison (backward compatibility) if not args.amount and not args.operator and not args.checkStacks then return found elseif isPlayer and not args.checkStacks and args.amount and ops[args.operator] then return CleveRoids.comparators[args.operator](remaining or -1, args.amount) elseif args.amount and args.checkStacks and ops[args.operator] then return CleveRoids.comparators[args.operator](stacks or -1, args.amount) else return false end end function CleveRoids.ValidateUnitBuff(unit, args) return CleveRoids.ValidateAura(unit, args, true) end function CleveRoids.ValidateUnitDebuff(unit, args) if not args or not UnitExists(unit) then return false end if type(args) ~= "table" then args = { name = args } end if not args.name then return false end 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 return false end -- For non-player units, check tracking table directly -- SIMPLE: Did the player cast this spell? Is the timer still valid? if unit ~= "player" and CleveRoids.libdebuff then local _, guid = UnitExists(unit) if not guid then return false end -- Normalize GUID to string for consistent table key lookups guid = CleveRoids.NormalizeGUID(guid) 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 {} -- Check tracking table for ANY rank of this spell: Did player cast this? Is timer valid? if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then for _, spellID in ipairs(matchingSpellIDs) do local rec = CleveRoids.libdebuff.objects[guid] and CleveRoids.libdebuff.objects[guid][spellID] -- For shared debuffs (Sunder, Faerie Fire, etc.), accept any caster (including nil) -- For personal debuffs (Rip, Rupture, etc.), only accept player casts local isSharedDebuff = CleveRoids.libdebuff:IsPersonalDebuff(spellID) == false if rec and rec.duration and rec.start and (isSharedDebuff or rec.caster == "player") then local timeRemaining = rec.duration + rec.start - GetTime() if timeRemaining > 0 then found = true remaining = timeRemaining stacks = rec.stacks or 0 if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( string.format("|cff00ff00[Tracking]|r %s (ID:%d): %.1fs left", args.name, spellID, timeRemaining) ) end -- Get texture (optional, just for display) for i = 1, 16 do local _, _, _, sid = UnitDebuff(unit, i) if sid == spellID then texture = UnitDebuff(unit, i) break end end if not texture then for i = 1, 32 do local _, _, sid = UnitBuff(unit, i) if sid == spellID then texture = UnitBuff(unit, i) break end end end -- Found active debuff, stop searching break elseif rec then -- Debuff expired - remove it from tracking to avoid spam local expiredTime = GetTime() - (rec.start + rec.duration) if CleveRoids.debug and expiredTime < 2.0 then DEFAULT_CHAT_FRAME:AddMessage( string.format("|cffff6600[Tracking]|r %s (ID:%d) expired %.1fs ago", args.name, spellID, expiredTime) ) end -- Remove expired debuff to prevent repeated "expired" messages CleveRoids.libdebuff.objects[guid][spellID] = nil end end end -- Only show "not in tracking" message if CleveRoids.debugVerbose is enabled -- (too spammy for normal debug mode) if not found and CleveRoids.debugVerbose then DEFAULT_CHAT_FRAME:AddMessage( string.format("|cffff0000[Tracking]|r %s not in tracking table (checked %d ranks)", args.name, table.getn(matchingSpellIDs)) ) end elseif CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( string.format("|cffff0000[Tracking]|r Unknown spell: %s", args.name) ) end -- FALLBACK: If not found in tracking table (e.g., shared debuff from another player), -- scan actual debuffs on target using UnitDebuff() -- IMPORTANT: Skip this fallback for personal debuffs - they should only match if player cast them local isPersonalDebuff = false if table.getn(matchingSpellIDs) > 0 then for _, spellID in ipairs(matchingSpellIDs) do if CleveRoids.libdebuff:IsPersonalDebuff(spellID) then isPersonalDebuff = true break end end end if not found and not isPersonalDebuff and CleveRoids.hasSuperwow then -- Scan debuff slots for i = 1, 16 do local tex, debuffStacks, _, debuffSpellID = UnitDebuff(unit, i) if not tex then break end if debuffSpellID then local fullName = SpellInfo(debuffSpellID) if fullName then local baseName = string.gsub(fullName, "%s*%(%s*Rank%s+%d+%s*%)", "") if baseName == args.name or fullName == args.name then found = true texture = tex stacks = debuffStacks or 0 spellID = debuffSpellID -- No duration tracking for shared debuffs remaining = nil break end end end end -- If still not found, check buff slots (overflow debuffs) if not found then for i = 1, 32 do local tex, buffStacks, buffSpellID = UnitBuff(unit, i) if not tex then break end if buffSpellID then local fullName = SpellInfo(buffSpellID) if fullName then local baseName = string.gsub(fullName, "%s*%(%s*Rank%s+%d+%s*%)", "") if baseName == args.name or fullName == args.name then found = true texture = tex stacks = buffStacks or 0 spellID = buffSpellID remaining = nil break end end end end end end -- For player unit, use standard search (player only sees own debuffs on self) elseif unit == "player" then -- Search DEBUFFS first i = 0 while true do texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, false) if not texture then break end if CleveRoids.hasSuperwow then local fullName = SpellInfo(spellID) -- Strip rank for comparison (handles "Moonfire (Rank 7)" vs "Moonfire") local baseName = string.gsub(fullName or "", "%s*%(%s*Rank%s+%d+%s*%)", "") if baseName == args.name or fullName == args.name then found = true break end elseif texture == CleveRoids.auraTextures[args.name] then found = true break end i = i + 1 end -- If not found, search BUFFS (overflow debuffs shown as buffs on some servers) if not found then i = 0 while true do texture, stacks, spellID, remaining = CleveRoids.GetPlayerAura(i, true) if not texture then break end if CleveRoids.hasSuperwow then local fullName = SpellInfo(spellID) -- Strip rank for comparison local baseName = string.gsub(fullName or "", "%s*%(%s*Rank%s+%d+%s*%)", "") if baseName == args.name or fullName == args.name then found = true break end elseif texture == CleveRoids.auraTextures[args.name] then found = true break end i = i + 1 end end end -- Step 3: Perform conditional validation local ops = CleveRoids.operators local cmp = CleveRoids.comparators -- Handle multi-comparison (e.g., >0&<10) if args.comparisons and type(args.comparisons) == "table" then if not found then return false -- Debuff doesn't exist, so all comparisons fail end -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not ops[comp.operator] then return false -- Invalid operator end local value_to_check if comp.checkStacks then value_to_check = stacks or 0 elseif unit == "player" then value_to_check = remaining or 0 else -- Non-player units don't have remaining time, only check existence return found end if not cmp[comp.operator](value_to_check, comp.amount) then return false -- One comparison failed end end return true -- All comparisons passed end local hasNumCheck = (args.amount ~= nil) and (args.operator ~= nil) and ops[args.operator] -- Case A: No numeric/stack condition, just check for existence. if not hasNumCheck and not args.checkStacks then return found end -- Case B: Numeric/stack condition exists. if hasNumCheck then -- Stacks compare path if args.checkStacks then if found then return cmp[args.operator](stacks or 0, args.amount) else return cmp[args.operator](0, args.amount) end end -- Time-left compare path if unit == "player" then if not found then return false -- debuff doesn't exist, fail the check end local tl = remaining or 0 return cmp[args.operator](tl, args.amount) else -- Non-player: try pfUI → internal libdebuff → 0s local tl = _get_debuff_timeleft(unit, args.name) if tl ~= nil then return cmp[args.operator](tl or 0, args.amount) end if CleveRoids.libdebuff and CleveRoids.libdebuff.UnitDebuff then local atl = nil local caster = nil -- Auto-detect if this is a personal debuff (unless explicitly overridden) local filterCaster = nil if args.mine == true then -- User explicitly requested player-only filtering filterCaster = "player" elseif args.mine == false then -- User explicitly requested no filtering filterCaster = nil else -- Auto-detect based on spell type (if available and personal, filter to player) -- We'll determine this during the search filterCaster = nil end -- 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 local shouldSkip = false -- Auto-detect: If args.mine not specified and this is a personal debuff, only match player casts if args.mine == nil and spellID and CleveRoids.libdebuff.IsPersonalDebuff then if CleveRoids.libdebuff:IsPersonalDebuff(spellID) and effectCaster ~= "player" then -- This is a personal debuff from another player, skip it shouldSkip = true end end if not shouldSkip then atl = (timeleft and timeleft >= 0) and timeleft or 0 caster = effectCaster break end end end if atl ~= nil then return cmp[args.operator](atl, args.amount) end end -- No timers at all: treat missing/unknown as 0s and compare if not found then -- If debuff doesn't exist, treat as 0 seconds and compare return cmp[args.operator](0, args.amount) end -- If we reach here with no timer data, treat as 0 return cmp[args.operator](0, args.amount) end end -- If we get here, nothing matched return false end function CleveRoids.ValidatePlayerBuff(args) return CleveRoids.ValidateAura("player", args, true) end function CleveRoids.ValidatePlayerDebuff(args) return CleveRoids.ValidateAura("player", args, false) end function CleveRoids.ValidateWeaponImbue(slot, imbueName) -- Check if weapon has enchant via API local hasMainEnchant, mainExpiration, mainCharges, hasOffEnchant, offExpiration, offCharges = GetWeaponEnchantInfo() local hasEnchant, expiration, charges if slot == "mh" then hasEnchant = hasMainEnchant expiration = mainExpiration charges = mainCharges else hasEnchant = hasOffEnchant expiration = offExpiration charges = offCharges end -- Only consider temporary enchants (with time or charges) -- This filters out permanent enchants like Crusader, Lifestealing, etc. local hasTemporaryEnchant = hasEnchant and (expiration and expiration > 0 or charges and charges > 0) -- If no specific imbue requested, return temporary enchant status if not imbueName or imbueName == "" then return hasTemporaryEnchant end -- If no temporary enchant, don't bother scanning if not hasTemporaryEnchant then return false end -- For specific imbue names, scan tooltip to match the name -- BUT only check lines that have time markers (temporary enchants) -- This prevents matching weapon stats like "Equip: ... critical strike ..." -- Create tooltip scanner if needed if not CleveRoidsTooltip then CreateFrame("GameTooltip", "CleveRoidsTooltip", nil, "GameTooltipTemplate") end -- Scan weapon tooltip CleveRoidsTooltip:SetOwner(UIParent, "ANCHOR_NONE") CleveRoidsTooltip:ClearLines() CleveRoidsTooltip:SetInventoryItem("player", slot == "mh" and 16 or 17) -- Normalize search term once local searchTerm = string.lower(string.gsub(imbueName, "_", " ")) -- Look for green text with time markers - check ALL green lines with time for i = 1, CleveRoidsTooltip:NumLines() do local text = _G["CleveRoidsTooltipTextLeft"..i] if text then local line = text:GetText() if line then local r, g, b = text:GetTextColor() -- Green text indicates enchant if g > 0.8 and r < 0.2 and b < 0.2 then local lowerLine = string.lower(line) -- Only check lines with time markers (temporary enchants) -- This skips permanent weapon stats like "Equip: ... critical strike ..." if string.find(lowerLine, "%(") and (string.find(lowerLine, " min%)") or string.find(lowerLine, " sec%)") or string.find(lowerLine, " charge")) then -- This is a temporary enchant line, check if it matches if string.find(lowerLine, searchTerm, 1, true) then return true -- Found it! end end end end end end -- Checked all lines, didn't find it return false end -- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed -- PERFORMANCE: Uncached version - called by GetCachedCooldown function CleveRoids._GetCooldownUncached(name, ignoreGCD) if not name then return 0 end -- Check if it's a spell first local spell = CleveRoids.GetSpell(name) if spell then local expires = CleveRoids.GetSpellCooldown(name, ignoreGCD) return expires -- GetSpellCooldown already returns absolute time end -- Not a spell, check if it's an item -- GetItemCooldown returns (remainingSeconds, totalDuration, enabled) local remaining, duration, enabled = CleveRoids.GetItemCooldown(name, ignoreGCD) -- Convert remaining seconds to absolute expiry time if remaining and remaining > 0 then return CleveRoids.GetCachedTime() + remaining end return 0 end -- PERFORMANCE: Cached wrapper - use this for conditional checks function CleveRoids.GetCooldown(name, ignoreGCD) return CleveRoids.GetCachedCooldown(name, ignoreGCD) end -- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed -- Returns the cooldown of the given spellName or nil if no such spell was found function CleveRoids.GetSpellCooldown(spellName, ignoreGCD) if not spellName then return 0 end local spell = CleveRoids.GetSpell(spellName) if not spell then return 0 end local start, cd = GetSpellCooldown(spell.spellSlot, spell.bookType) if ignoreGCD and cd and cd > 0 and cd <= 1.5 then return 0 else return (start + cd) end end -- Check if an item exists in bags or equipped -- Returns: true if found, false otherwise -- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup, with fallback for substring matches function CleveRoids.HasItem(item) -- Fast path: check cache first (O(1) lookup) if CleveRoids.HasItemCached(item) then return true end -- Slow path fallback: only needed for substring matching on strings -- The cache handles exact matches by ID and name, but not partial/substring matches if type(item) == "string" and item ~= "" then local itemLower = string.lower(item) -- Check equipped slots for substring match for slot = 0, 19 do local link = GetInventoryItemLink("player", slot) if link then if string.find(string.lower(link), itemLower, 1, true) then return true end end end -- Check bags for substring match for bag = 0, 4 do local size = GetContainerNumSlots(bag) if size and size > 0 then for slotIndex = 1, size do local link = GetContainerItemLink(bag, slotIndex) if link then if string.find(string.lower(link), itemLower, 1, true) then return true end end end end end end return false end -- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed -- Hardened item cooldown resolver (Vanilla 1.12.1 / Lua 5.0) -- Returns: remainingSeconds, totalDuration, enabled -- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup, with fallback for substring matches function CleveRoids.GetItemCooldown(item) -- Helper to normalize cooldown values local function _norm(s, d, e) s = tonumber(s) or 0 d = tonumber(d) or 0 e = tonumber(e) or 0 if d <= 0 or s <= 0 then return 0, 0, e end local rem = (s + d) - GetTime() if rem < 0 then rem = 0 end return rem, d, e end -- Fast path: check cache first (O(1) lookup) local remaining, duration, enable = CleveRoids.GetItemCooldownCached(item) if duration > 0 or remaining > 0 then return remaining, duration, enable end -- If cache found the item but cooldown is 0, that's a valid result local location = CleveRoids.FindItemLocation(item) if location then return 0, 0, enable or 0 end -- Slow path fallback: only needed for substring matching on strings if type(item) == "string" and item ~= "" then local itemLower = string.lower(item) local start, dur, en -- Check equipped slots for substring match for slot = 0, 19 do local link = GetInventoryItemLink("player", slot) if link then if string.find(string.lower(link), itemLower, 1, true) then start, dur, en = GetInventoryItemCooldown("player", slot) return _norm(start, dur, en) end end end -- Check bags for substring match for bag = 0, 4 do local size = GetContainerNumSlots(bag) if size and size > 0 then for slotIndex = 1, size do local link = GetContainerItemLink(bag, slotIndex) if link then if string.find(string.lower(link), itemLower, 1, true) then start, dur, en = GetContainerItemCooldown(bag, slotIndex) return _norm(start, dur, en) end end end end end end -- Fallback: unknown item → no cooldown return 0, 0, 0 end function CleveRoids.ValidatePlayerAuraCount(bigger, amount) local aura_ix = -1 local num = 0 while true do aura_ix = GetPlayerBuff(num,"HELPFUL|PASSIVE") if aura_ix == -1 then break end num = num + 1 end if bigger == 0 then return num < tonumber(amount) else return num > tonumber(amount) end end function CleveRoids.IsReactive(name) return CleveRoids.reactiveSpells[spellName] ~= nil end function CleveRoids.GetActionButtonInfo(slot) local macroName, actionType, id = GetActionText(slot) if actionType == "MACRO" then return actionType, id, macroName elseif actionType == "SPELL" and id then local spellName, rank = SpellInfo(id) return actionType, id, spellName, rank elseif actionType == "ITEM" and id then local item = CleveRoids.GetItem(id) return actionType, id, (item and item.name), (item and item.id) end end function CleveRoids.IsReactiveUsable(spellName) -- For Overpower, Revenge, and Riposte: ONLY use combat log tracking -- These spells have specific proc conditions tracked via combat log if spellName == "Overpower" or spellName == "Revenge" or spellName == "Riposte" then if CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(spellName) then return 1 else return nil end end -- For other reactive spells, use fallback methods -- Use Nampower's IsSpellUsable if available (more accurate) if IsSpellUsable then local usable, oom = IsSpellUsable(spellName) if usable == 1 and oom ~= 1 then return 1 else return nil, oom end end -- Fallback to action bar slot checking (requires correct stance) if not CleveRoids.reactiveSlots[spellName] then return false end local actionSlot = CleveRoids.reactiveSlots[spellName] local isUsable, oom = CleveRoids.Hooks.OriginalIsUsableAction(actionSlot) local start, duration = GetActionCooldown(actionSlot) if isUsable and (start == 0 or duration == 1.5) then -- 1.5 just means gcd is active return 1 else return nil, oom end end -- Check if any spell is usable (not just reactive) function CleveRoids.CheckSpellUsable(spellName) if not spellName then return false end -- Use Nampower's IsSpellUsable if available if IsSpellUsable then local usable, oom = IsSpellUsable(spellName) return (usable == 1 and oom ~= 1) end -- Fallback: check if spell exists and player has mana/rage/energy local spell = CleveRoids.GetSpell(spellName) if not spell then return false end -- Check mana cost local currentPower = UnitMana("player") if spell.cost and currentPower < spell.cost then return false end -- Check cooldown (ignore GCD) local start, duration = GetSpellCooldown(spell.spellSlot, spell.bookType) if start > 0 and duration > 1.5 then return false end return true end function CleveRoids.CheckSpellCast(unit, spell) local spell = spell or "" local _,guid = UnitExists(unit) if not guid then return false end -- BUGFIX: Special handling for player unit - check CurrentSpell.type -- This is event-driven and more reliable than spell_tracking for the player if unit == "player" then -- Check if player is casting or channeling if CleveRoids.CurrentSpell and CleveRoids.CurrentSpell.type ~= "" then -- If checking for any spell, return true if spell == "" then return true end -- If checking for specific spell, compare spell names -- Note: CurrentSpell.spellName may not be set, so also check spell_tracking as fallback if CleveRoids.CurrentSpell.spellName and CleveRoids.CurrentSpell.spellName == spell then return true end end -- Fallback to spell_tracking for player if CurrentSpell doesn't have the info end -- For non-player units or as fallback, use spell_tracking if not CleveRoids.hasSuperwow then return false end if not CleveRoids.spell_tracking[guid] then return false else -- are we casting a specific spell, or any spell if spell == SpellInfo(CleveRoids.spell_tracking[guid].spell_id) or (spell == "") then return true end return false end end -- A list of Conditionals and their functions to validate them CleveRoids.Keywords = { exists = function(conditionals) return UnitExists(conditionals.target) end, noexists = function(conditionals) return not UnitExists(conditionals.target) end, help = function(conditionals) return conditionals.help and conditionals.target and UnitExists(conditionals.target) and UnitCanAssist("player", conditionals.target) end, harm = function(conditionals) return conditionals.harm and conditionals.target and UnitExists(conditionals.target) and UnitCanAttack("player", conditionals.target) end, stance = function(conditionals) local i = CleveRoids.GetCurrentShapeshiftIndex() return Or(conditionals.stance, function (v) return (i == tonumber(v)) end) end, nostance = function(conditionals) local i = CleveRoids.GetCurrentShapeshiftIndex() local forbiddenStances = conditionals.nostance if type(forbiddenStances) ~= "table" then return i == 0 end return NegatedMulti(forbiddenStances, function (v) return (i ~= tonumber(v)) end, conditionals, "nostance") end, noform = function(conditionals) local i = CleveRoids.GetCurrentShapeshiftIndex() local forbiddenForms = conditionals.noform if type(forbiddenForms) ~= "table" then return i == 0 end return NegatedMulti(forbiddenForms, function (v) return (i ~= tonumber(v)) end, conditionals, "noform") end, form = function(conditionals) local i = CleveRoids.GetCurrentShapeshiftIndex() return Or(conditionals.form, function (v) return (i == tonumber(v)) end) end, mod = function(conditionals) if type(conditionals.mod) ~= "table" then return CleveRoids.kmods.mod() end return Multi(conditionals.mod, function(mod) return CleveRoids.kmods[mod]() end, conditionals, "mod") end, nomod = function(conditionals) if type(conditionals.nomod) ~= "table" then return CleveRoids.kmods.nomod() end return NegatedMulti(conditionals.nomod, function(mod) return not CleveRoids.kmods[mod]() end, conditionals, "nomod") end, target = function(conditionals) return CleveRoids.IsValidTarget(conditionals.target, conditionals.help) end, combat = function(conditionals) -- Check if an argument like :target or :focus was provided. The parser turns this into a table. if type(conditionals.combat) == "table" then -- If so, run the check on the provided unit(s). return Or(conditionals.combat, function(unit) return UnitExists(unit) and UnitAffectingCombat(unit) end) else -- Otherwise, this is a bare [combat]. The value might be 'true' or a spell name. -- In either case, it should safely default to checking the player. return UnitAffectingCombat("player") end end, nocombat = function(conditionals) -- Check if an argument like :target or :focus was provided. if type(conditionals.nocombat) == "table" then -- If so, run the check on the provided unit(s). return NegatedMulti(conditionals.nocombat, function(unit) if not UnitExists(unit) then return true end return not UnitAffectingCombat(unit) end, conditionals, "nocombat") else -- Otherwise, this is a bare [nocombat]. Default to checking the player. return not UnitAffectingCombat("player") end end, stealth = function(conditionals) return ( (CleveRoids.playerClass == "ROGUE" and CleveRoids.ValidatePlayerBuff(CleveRoids.Localized.Spells["Stealth"])) or (CleveRoids.playerClass == "DRUID" and CleveRoids.ValidatePlayerBuff(CleveRoids.Localized.Spells["Prowl"])) ) end, nostealth = function(conditionals) return ( (CleveRoids.playerClass == "ROGUE" and not CleveRoids.ValidatePlayerBuff(CleveRoids.Localized.Spells["Stealth"])) or (CleveRoids.playerClass == "DRUID" and not CleveRoids.ValidatePlayerBuff(CleveRoids.Localized.Spells["Prowl"])) ) end, casting = function(conditionals) if type(conditionals.casting) ~= "table" then return CleveRoids.CheckSpellCast(conditionals.target, "") end return Or(conditionals.casting, function (spell) return CleveRoids.CheckSpellCast(conditionals.target, spell) end) end, nocasting = function(conditionals) if type(conditionals.nocasting) ~= "table" then return not CleveRoids.CheckSpellCast(conditionals.target, "") end return NegatedMulti(conditionals.nocasting, function (spell) return not CleveRoids.CheckSpellCast(conditionals.target, spell) end, conditionals, "nocasting") end, -- NEW: Direct player casting check with time-based prediction -- Uses our accurate state tracking instead of GetCurrentCastingInfo polling selfcasting = function(conditionals) -- Check for cast with time-based prediction if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then local remaining = CleveRoids.castDuration - (GetTime() - CleveRoids.castStartTime) if remaining <= 0.1 then return false -- Cast is done end end -- Check for channel with time-based prediction if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) if remaining <= 0.1 then return false -- Channel is done end end return CleveRoids.CurrentSpell.type == "cast" or CleveRoids.CurrentSpell.type == "channeled" end, noselfcasting = function(conditionals) -- Inverse of selfcasting with same prediction logic if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then local remaining = CleveRoids.castDuration - (GetTime() - CleveRoids.castStartTime) if remaining <= 0.1 then return true -- Cast is done end end if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) if remaining <= 0.1 then return true -- Channel is done end end return CleveRoids.CurrentSpell.type ~= "cast" and CleveRoids.CurrentSpell.type ~= "channeled" end, zone = function(conditionals) local zone = GetRealZoneText() local sub_zone = GetSubZoneText() return Or(conditionals.zone, function (v) return (sub_zone ~= "" and (v == sub_zone) or (v == zone)) end) end, nozone = function(conditionals) local zone = GetRealZoneText() local sub_zone = GetSubZoneText() return NegatedMulti(conditionals.nozone, function (v) return not ((sub_zone ~= "" and v == sub_zone)) or (v == zone) end, conditionals, "nozone") end, equipped = function(conditionals) local itemsToCheck = {} -- Case 1: conditionals.equipped is a string (e.g., [equipped]ItemName) if type(conditionals.equipped) == "string" then table.insert(itemsToCheck, conditionals.equipped) -- Case 2: conditionals.equipped is a table (e.g., [equipped:Shields]) elseif type(conditionals.equipped) == "table" and table.getn(conditionals.equipped) > 0 then itemsToCheck = conditionals.equipped -- Case 3: No value provided, check the action elseif conditionals.action then table.insert(itemsToCheck, conditionals.action) else return false end -- Check all items return Or(itemsToCheck, function(v) return (CleveRoids.HasWeaponEquipped(v) or CleveRoids.HasGearEquipped(v)) end) end, noequipped = function(conditionals) local itemsToCheck = {} -- Case 1: conditionals.noequipped is a string (e.g., [noequipped]ItemName) if type(conditionals.noequipped) == "string" then table.insert(itemsToCheck, conditionals.noequipped) -- Case 2: conditionals.noequipped is a table (e.g., [noequipped:Shields]) elseif type(conditionals.noequipped) == "table" and table.getn(conditionals.noequipped) > 0 then itemsToCheck = conditionals.noequipped -- Case 3: No value provided, check the action elseif conditionals.action then table.insert(itemsToCheck, conditionals.action) else return false end -- Check all items - ALL must be NOT equipped for this to pass return And(itemsToCheck, function(v) return not (CleveRoids.HasWeaponEquipped(v) or CleveRoids.HasGearEquipped(v)) end) end, dead = function(conditionals) if not conditionals.target then return false end return UnitIsDeadOrGhost(conditionals.target) end, alive = function(conditionals) if not conditionals.target then return false end return not UnitIsDeadOrGhost(conditionals.target) end, noalive = function(conditionals) if not conditionals.target then return false end return UnitIsDeadOrGhost(conditionals.target) end, nodead = function(conditionals) if not conditionals.target then return false end return not UnitIsDeadOrGhost(conditionals.target) end, reactive = function(conditionals) return Or(conditionals.reactive, function (v) return CleveRoids.IsReactiveUsable(v) end) end, noreactive = function(conditionals) return NegatedMulti(conditionals.noreactive, function (v) return not CleveRoids.IsReactiveUsable(v) end, conditionals, "noreactive") end, usable = function(conditionals) return Or(conditionals.usable, function(name) -- If checking a reactive spell, use reactive logic if CleveRoids.reactiveSpells[name] then return CleveRoids.IsReactiveUsable(name) end -- Check if it's a spell first local spell = CleveRoids.GetSpell(name) if spell then return CleveRoids.CheckSpellUsable(name) end -- Not a spell - check if it's an item or slot number local itemName = name local slotNum = tonumber(name) if slotNum and slotNum >= 1 and slotNum <= 19 then -- Resolve slot number to item name local link = GetInventoryItemLink("player", slotNum) if link then local _, _, extractedName = string.find(link, "%[(.+)%]") if extractedName then itemName = extractedName end end end -- Check if item exists in bags/equipped first if not CleveRoids.HasItem(itemName) then return false end -- Check item cooldown (0 remaining = usable) local remaining = CleveRoids.GetItemCooldown(itemName) return remaining == 0 end) end, nousable = function(conditionals) return NegatedMulti(conditionals.nousable, function(name) -- If checking a reactive spell, use reactive logic if CleveRoids.reactiveSpells[name] then return not CleveRoids.IsReactiveUsable(name) end -- Check if it's a spell first local spell = CleveRoids.GetSpell(name) if spell then return not CleveRoids.CheckSpellUsable(name) end -- Not a spell - check if it's an item or slot number local itemName = name local slotNum = tonumber(name) if slotNum and slotNum >= 1 and slotNum <= 19 then -- Resolve slot number to item name local link = GetInventoryItemLink("player", slotNum) if link then local _, _, extractedName = string.find(link, "%[(.+)%]") if extractedName then itemName = extractedName end end end -- Item not existing counts as "not usable" if not CleveRoids.HasItem(itemName) then return true end -- Check item cooldown (>0 remaining = not usable) local remaining = CleveRoids.GetItemCooldown(itemName) return remaining > 0 end, conditionals, "nousable") end, member = function(conditionals) return Or(conditionals.member, function(v) return CleveRoids.IsTargetInGroupType(conditionals.target, "party") or CleveRoids.IsTargetInGroupType(conditionals.target, "raid") end) end, party = function(conditionals) return CleveRoids.IsTargetInGroupType(conditionals.target, "party") end, noparty = function(conditionals) return not CleveRoids.IsTargetInGroupType(conditionals.target, "party") end, raid = function(conditionals) return CleveRoids.IsTargetInGroupType(conditionals.target, "raid") end, noraid = function(conditionals) return not CleveRoids.IsTargetInGroupType(conditionals.target, "raid") end, group = function(conditionals) if type(conditionals.group) ~= "table" then conditionals.group = { "party", "raid" } end return Or(conditionals.group, function(groups) if groups == "party" then return GetNumPartyMembers() > 0 elseif groups == "raid" then return GetNumRaidMembers() > 0 end end) end, checkchanneled = function(conditionals) if conditionals.checkchanneled == true then -- Boolean form [checkchanneled] - check if NOT channeling anything return CleveRoids.CheckChanneled(nil) else -- String form [checkchanneled:SpellName] - check if NOT channeling that spell return Or(conditionals.checkchanneled, function(channeledSpells) return CleveRoids.CheckChanneled(channeledSpells) end) end end, checkcasting = function(conditionals) if conditionals.checkcasting == true then -- Boolean form [checkcasting] - check if NOT casting anything return CleveRoids.CheckCasting(nil) else -- String form [checkcasting:SpellName] - check if NOT casting that spell return Or(conditionals.checkcasting, function(castingSpells) return CleveRoids.CheckCasting(castingSpells) end) end end, buff = function(conditionals) return Multi(conditionals.buff, function(v) return CleveRoids.ValidateUnitBuff(conditionals.target, v) end, conditionals, "buff") end, nobuff = function(conditionals) return NegatedMulti(conditionals.nobuff, function(v) return not CleveRoids.ValidateUnitBuff(conditionals.target, v) end, conditionals, "nobuff") end, debuff = function(conditionals) return Multi(conditionals.debuff, function(v) return CleveRoids.ValidateUnitDebuff(conditionals.target, v) end, conditionals, "debuff") end, nodebuff = function(conditionals) return NegatedMulti(conditionals.nodebuff, function(v) return not CleveRoids.ValidateUnitDebuff(conditionals.target, v) end, conditionals, "nodebuff") end, mybuff = function(conditionals) return Or(conditionals.mybuff, function(v) return CleveRoids.ValidatePlayerBuff(v) end) end, nomybuff = function(conditionals) return NegatedMulti(conditionals.nomybuff, function(v) return not CleveRoids.ValidatePlayerBuff(v) end, conditionals, "nomybuff") end, mydebuff = function(conditionals) return Or(conditionals.mydebuff, function(v) return CleveRoids.ValidatePlayerDebuff(v) end) end, nomydebuff = function(conditionals) return NegatedMulti(conditionals.nomydebuff, function(v) return not CleveRoids.ValidatePlayerDebuff(v) end, conditionals, "nomydebuff") end, power = function(conditionals) return Multi(conditionals.power, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<80) if args.comparisons and type(args.comparisons) == "table" then local unit = conditionals.target or "target" if not UnitExists(unit) then return false end local powerPercent = 100 / UnitManaMax(unit) * UnitMana(unit) -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](powerPercent, comp.amount) then return false end end return true end return CleveRoids.ValidatePower(conditionals.target, args.operator, args.amount) end, conditionals, "power") end, mypower = function(conditionals) return Multi(conditionals.mypower, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<80) if args.comparisons and type(args.comparisons) == "table" then -- PERFORMANCE: Use cached player power local powerPercent = CleveRoids.GetCachedPlayerPowerPercent() -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](powerPercent, comp.amount) then return false end end return true end return CleveRoids.ValidatePower("player", args.operator, args.amount) end, conditionals, "mypower") end, rawpower = function(conditionals) return Multi(conditionals.rawpower, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >500&<1000) if args.comparisons and type(args.comparisons) == "table" then local unit = conditionals.target or "target" if not UnitExists(unit) then return false end local power = UnitMana(unit) -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](power, comp.amount) then return false end end return true end return CleveRoids.ValidateRawPower(conditionals.target, args.operator, args.amount) end, conditionals, "rawpower") end, myrawpower = function(conditionals) return Multi(conditionals.myrawpower, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >500&<1000) if args.comparisons and type(args.comparisons) == "table" then -- PERFORMANCE: Use cached player power local power = CleveRoids.GetCachedPlayerPower() -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](power, comp.amount) then return false end end return true end return CleveRoids.ValidateRawPower("player", args.operator, args.amount) end, conditionals, "myrawpower") end, druidmana = function(conditionals) return Multi(conditionals.druidmana, function(args) if type(args) ~= "table" then return false end return CleveRoids.ValidateDruidRawMana("player", args.operator, args.amount) end, conditionals, "druidmana") end, powerlost = function(conditionals) return Multi(conditionals.powerlost, function(args) if type(args) ~= "table" then return false end return CleveRoids.ValidatePowerLost(conditionals.target, args.operator, args.amount) end, conditionals, "powerlost") end, mypowerlost = function(conditionals) return Multi(conditionals.mypowerlost, function(args) if type(args) ~= "table" then return false end return CleveRoids.ValidatePowerLost("player", args.operator, args.amount) end, conditionals, "mypowerlost") end, hp = function(conditionals) return Multi(conditionals.hp, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<80) if args.comparisons and type(args.comparisons) == "table" then local unit = conditionals.target or "target" if not UnitExists(unit) then return false end -- PERFORMANCE: Use cached health for target local hp if unit == "target" then hp = CleveRoids.GetCachedTargetHealthPercent() else local maxHp = UnitHealthMax(unit) hp = maxHp > 0 and (100 * UnitHealth(unit) / maxHp) or 0 end -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](hp, comp.amount) then return false end end return true end return CleveRoids.ValidateHp(conditionals.target, args.operator, args.amount) end, conditionals, "hp") end, level = function(conditionals) return Multi(conditionals.level, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<60) if args.comparisons and type(args.comparisons) == "table" then local unit = conditionals.target or "target" if not UnitExists(unit) then return false end local level = UnitLevel(unit) -- Treat skull/boss mobs (??) as level 63 if level == -1 then level = 63 end -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](level, comp.amount) then return false end end return true end return CleveRoids.ValidateLevel(conditionals.target, args.operator, args.amount) end, conditionals, "level") end, mylevel = function(conditionals) return Multi(conditionals.mylevel, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<60) if args.comparisons and type(args.comparisons) == "table" then local level = UnitLevel("player") -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](level, comp.amount) then return false end end return true end return CleveRoids.ValidateLevel("player", args.operator, args.amount) end, conditionals, "mylevel") end, myhp = function(conditionals) return Multi(conditionals.myhp, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<80) if args.comparisons and type(args.comparisons) == "table" then -- PERFORMANCE: Use cached player health local hp = CleveRoids.GetCachedPlayerHealthPercent() -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](hp, comp.amount) then return false end end return true end return CleveRoids.ValidateHp("player", args.operator, args.amount) end, conditionals, "myhp") end, rawhp = function(conditionals) return Multi(conditionals.rawhp, function(args) if type(args) ~= "table" then return false end return CleveRoids.ValidateRawHp(conditionals.target, args.operator, args.amount) end, conditionals, "rawhp") end, myrawhp = function(conditionals) return Multi(conditionals.myrawhp, function(args) if type(args) ~= "table" then return false end return CleveRoids.ValidateRawHp("player", args.operator, args.amount) end, conditionals, "myrawhp") end, hplost = function(conditionals) return Multi(conditionals.hplost, function(args) if type(args) ~= "table" then return false end return CleveRoids.ValidateHpLost(conditionals.target, args.operator, args.amount) end, conditionals, "hplost") end, myhplost = function(conditionals) return Multi(conditionals.myhplost, function(args) if type(args) ~= "table" then return false end return CleveRoids.ValidateHpLost("player", args.operator, args.amount) end, conditionals, "myhplost") end, type = function(conditionals) return Or(conditionals.type, function(unittype) return CleveRoids.ValidateCreatureType(unittype, conditionals.target) end) end, notype = function(conditionals) return NegatedMulti(conditionals.notype, function(unittype) return not CleveRoids.ValidateCreatureType(unittype, conditionals.target) end, conditionals, "notype") end, cooldown = function(conditionals) return Multi(conditionals.cooldown,function (v) return CleveRoids.ValidateCooldown(v, true) end, conditionals, "cooldown") end, nocooldown = function(conditionals) return NegatedMulti(conditionals.nocooldown,function (v) return not CleveRoids.ValidateCooldown(v, true) end, conditionals, "nocooldown") end, cdgcd = function(conditionals) return Multi(conditionals.cdgcd,function (v) return CleveRoids.ValidateCooldown(v, false) end, conditionals, "cdgcd") end, nocdgcd = function(conditionals) return NegatedMulti(conditionals.nocdgcd,function (v) return not CleveRoids.ValidateCooldown(v, false) end, conditionals, "nocdgcd") end, channeled = function(conditionals) -- Use time-based prediction for accuracy if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) if remaining <= 0.1 then return false -- Channel is done end end return CleveRoids.CurrentSpell.type == "channeled" end, nochanneled = function(conditionals) -- Use time-based prediction for accuracy if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then local remaining = CleveRoids.channelDuration - (GetTime() - CleveRoids.channelStartTime) if remaining <= 0.1 then return true -- Channel is done end end return CleveRoids.CurrentSpell.type ~= "channeled" end, channeltime = function(conditionals) -- Calculate remaining time (0 if not channeling) local timeLeft = 0 if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then local elapsed = GetTime() - CleveRoids.channelStartTime timeLeft = CleveRoids.channelDuration - elapsed -- Don't allow negative time if timeLeft < 0 then timeLeft = 0 end end local check = conditionals.channeltime -- channeltime is stored as an array by the parser, get the first element if type(check) == "table" and type(check[1]) == "table" then check = check[1] end if type(check) == "table" and check.operator and check.amount then -- Now compare: if not channeling, timeLeft is 0, so [channeltime:<0.5] returns true return CleveRoids.comparators[check.operator](timeLeft, check.amount) end return false end, casttime = function(conditionals) -- Calculate remaining time (0 if not casting) local timeLeft = 0 if CleveRoids.CurrentSpell.type == "cast" and CleveRoids.castStartTime and CleveRoids.castDuration then local elapsed = GetTime() - CleveRoids.castStartTime timeLeft = CleveRoids.castDuration - elapsed -- Don't allow negative time if timeLeft < 0 then timeLeft = 0 end end local check = conditionals.casttime -- casttime is stored as an array by the parser, get the first element if type(check) == "table" and type(check[1]) == "table" then check = check[1] end if type(check) == "table" and check.operator and check.amount then -- Now compare: if not casting, timeLeft is 0, so [casttime:<0.5] returns true return CleveRoids.comparators[check.operator](timeLeft, check.amount) end return false end, targeting = function(conditionals) return Or(conditionals.targeting, function (unit) return (UnitIsUnit("targettarget", unit) == 1) end) end, notargeting = function(conditionals) return NegatedMulti(conditionals.notargeting, function (unit) return UnitIsUnit("targettarget", unit) ~= 1 end, conditionals, "notargeting") end, isplayer = function(conditionals) return UnitIsPlayer(conditionals.target) end, isnpc = function(conditionals) return not UnitIsPlayer(conditionals.target) end, inrange = function(conditionals) if not IsSpellInRange then return end return Or(conditionals.inrange, function(spellName) local target = conditionals.target or "target" local checkValue = spellName or conditionals.action -- Try to convert spell name to ID for better accuracy (Nampower) if type(checkValue) == "string" and GetSpellIdForName then local spellId = GetSpellIdForName(checkValue) if spellId and spellId > 0 then checkValue = spellId end end return IsSpellInRange(checkValue, target) == 1 end) end, noinrange = function(conditionals) if not IsSpellInRange then return end return NegatedMulti(conditionals.noinrange, function(spellName) local target = conditionals.target or "target" local checkValue = spellName or conditionals.action if type(checkValue) == "string" and GetSpellIdForName then local spellId = GetSpellIdForName(checkValue) if spellId and spellId > 0 then checkValue = spellId end end return IsSpellInRange(checkValue, target) == 0 end, conditionals, "noinrange") end, outrange = function(conditionals) if not IsSpellInRange then return end return Or(conditionals.outrange, function(spellName) local target = conditionals.target or "target" local checkValue = spellName or conditionals.action if type(checkValue) == "string" and GetSpellIdForName then local spellId = GetSpellIdForName(checkValue) if spellId and spellId > 0 then checkValue = spellId end end return IsSpellInRange(checkValue, target) == 0 end) end, combo = function(conditionals) return Multi(conditionals.combo, function(args) return CleveRoids.ValidateComboPoints(args.operator, args.amount) end, conditionals, "combo") end, nocombo = function(conditionals) return NegatedMulti(conditionals.nocombo, function(args) return not CleveRoids.ValidateComboPoints(args.operator, args.amount) end, conditionals, "nocombo") end, known = function(conditionals) return Multi(conditionals.known, function(args) return CleveRoids.ValidateKnown(args) end, conditionals, "known") end, noknown = function(conditionals) return NegatedMulti(conditionals.noknown, function(args) return not CleveRoids.ValidateKnown(args) end, conditionals, "noknown") end, resting = function() return IsResting() == 1 end, noresting = function() return IsResting() == nil end, stat = function(conditionals) return Multi(conditionals.stat, function(args) if type(args) ~= "table" or not args.name then return false -- Malformed arguments from the parser. end local stat_key = string.lower(args.name) local get_stat_func = stat_checks[stat_key] if not get_stat_func then return false -- The requested stat key is invalid. end local current_value = get_stat_func() if not current_value then return false end -- Check if this is a multi-comparison stat conditional -- args.comparisons will be a table of {operator=, amount=} if multiple if args.comparisons and type(args.comparisons) == "table" then -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.comparators[comp.operator] then return false -- Invalid operator end if not CleveRoids.comparators[comp.operator](current_value, comp.amount) then return false -- One comparison failed, so the whole conditional fails end end return true -- All comparisons passed else -- Single comparison (backward compatibility) if not args.operator or not args.amount then return false end return CleveRoids.comparators[args.operator](current_value, args.amount) end end, conditionals, "stat") end, class = function(conditionals) -- Determine which unit to check. Defaults to 'target' if no @unitid was specified. local unitToCheck = conditionals.target or "target" -- The conditional must fail if the unit doesn't exist OR is not a player. if not UnitExists(unitToCheck) or not UnitIsPlayer(unitToCheck) then return false end -- Get the player's class. local localizedClass, englishClass = UnitClass(unitToCheck) if not localizedClass then return false end -- Failsafe for unusual cases -- The "Or" helper handles multiple values like [class:Warrior/Druid]. return Or(conditionals.class, function(requiredClass) return string.lower(requiredClass) == string.lower(localizedClass) or string.lower(requiredClass) == string.lower(englishClass) end) end, noclass = function(conditionals) -- Determine which unit to check. Defaults to 'target' if no @unitid was specified. local unitToCheck = conditionals.target or "target" -- A unit that doesn't exist cannot have a specific player class. if not UnitExists(unitToCheck) then return true end -- An NPC cannot have a specific player class. if not UnitIsPlayer(unitToCheck) then return true end -- If we get here, the unit is a player. Now check their class. local localizedClass, englishClass = UnitClass(unitToCheck) -- A player should always have a class, but if not, this condition is still met. if not localizedClass then return true end -- The "NegatedMulti" helper ensures the player's class is not any of the forbidden classes. return NegatedMulti(conditionals.noclass, function(forbiddenClass) return string.lower(forbiddenClass) ~= string.lower(localizedClass) and string.lower(forbiddenClass) ~= string.lower(englishClass) end, conditionals, "noclass") end, pet = function(conditionals) if not UnitExists("pet") then return false end return Or(conditionals.pet, function(petType) local currentPet = UnitCreatureFamily("pet") if not currentPet then return false end return string.lower(currentPet) == string.lower(petType) end) end, nopet = function(conditionals) if not UnitExists("pet") then return true end return NegatedMulti(conditionals.nopet, function(petType) local currentPet = UnitCreatureFamily("pet") if not currentPet then return true end return string.lower(currentPet) ~= string.lower(petType) end, conditionals, "nopet") end, swimming = function(conditionals) -- Check if "Aquatic Form" is in the reactive list and usable return CleveRoids.IsReactiveUsable("Aquatic Form") end, noswimming = function(conditionals) -- Check if "Aquatic Form" is NOT usable return not CleveRoids.IsReactiveUsable("Aquatic Form") end, distance = function(conditionals) if not CleveRoids.hasUnitXP then return false end return Multi(conditionals.distance, function(args) if type(args) ~= "table" or not args.operator or not args.amount then return false end local unit = conditionals.target or "target" if not UnitExists(unit) then return false end local distance = UnitXP("distanceBetween", "player", unit) if not distance then return false end return CleveRoids.comparators[args.operator](distance, args.amount) end, conditionals, "distance") end, nodistance = function(conditionals) if not CleveRoids.hasUnitXP then return false end return NegatedMulti(conditionals.nodistance, function(args) if type(args) ~= "table" or not args.operator or not args.amount then return false end local unit = conditionals.target or "target" if not UnitExists(unit) then return false end local distance = UnitXP("distanceBetween", "player", unit) if not distance then return false end return not CleveRoids.comparators[args.operator](distance, args.amount) end, conditionals, "nodistance") end, behind = function(conditionals) if not CleveRoids.hasUnitXP then return false end local unit = conditionals.target or "target" if not UnitExists(unit) then return false end return UnitXP("behind", "player", unit) == true end, nobehind = function(conditionals) if not CleveRoids.hasUnitXP then return false end local unit = conditionals.target or "target" if not UnitExists(unit) then return false end return UnitXP("behind", "player", unit) ~= true end, insight = function(conditionals) if not CleveRoids.hasUnitXP then return false end local unit = conditionals.target or "target" if not UnitExists(unit) then return false end return UnitXP("inSight", "player", unit) == true end, noinsight = function(conditionals) if not CleveRoids.hasUnitXP then return false end local unit = conditionals.target or "target" if not UnitExists(unit) then return false end return UnitXP("inSight", "player", unit) ~= true end, meleerange = function(conditionals) local unit = conditionals.target or "target" if not UnitExists(unit) then return false end if CleveRoids.hasUnitXP then local distance = UnitXP("distanceBetween", "player", unit, "meleeAutoAttack") return distance and distance <= 5 else -- Fallback: use CheckInteractDistance (3 = melee range) return CheckInteractDistance(unit, 3) end end, nomeleerange = function(conditionals) local unit = conditionals.target or "target" if not UnitExists(unit) then return true end if CleveRoids.hasUnitXP then local distance = UnitXP("distanceBetween", "player", unit, "meleeAutoAttack") return not distance or distance > 5 else return not CheckInteractDistance(unit, 3) end end, queuedspell = function(conditionals) if not CleveRoids.hasNampower then return false end if not CleveRoids.queuedSpell then return false end -- If no specific spell name provided, check if ANY spell is queued if not conditionals.queuedspell or table.getn(conditionals.queuedspell) == 0 then return true end -- Check if specific spell is queued return Or(conditionals.queuedspell, function(spellName) if not CleveRoids.queuedSpell.spellName then return false end local queuedName = string.gsub(CleveRoids.queuedSpell.spellName, "%s*%(.-%)%s*$", "") local checkName = string.gsub(spellName, "%s*%(.-%)%s*$", "") return string.lower(queuedName) == string.lower(checkName) end) end, noqueuedspell = function(conditionals) if not CleveRoids.hasNampower then return false end -- If no specific spell name, check if NO spell is queued if not conditionals.noqueuedspell or table.getn(conditionals.noqueuedspell) == 0 then return CleveRoids.queuedSpell == nil end -- Check if specific spell is NOT queued if not CleveRoids.queuedSpell or not CleveRoids.queuedSpell.spellName then return true end return NegatedMulti(conditionals.noqueuedspell, function(spellName) local queuedName = string.gsub(CleveRoids.queuedSpell.spellName, "%s*%(.-%)%s*$", "") local checkName = string.gsub(spellName, "%s*%(.-%)%s*$", "") return string.lower(queuedName) ~= string.lower(checkName) end, conditionals, "noqueuedspell") end, onswingpending = function(conditionals) if not GetCurrentCastingInfo then return false end local _, _, _, _, _, onswing = GetCurrentCastingInfo() return onswing == 1 end, noonswingpending = function(conditionals) if not GetCurrentCastingInfo then return true end local _, _, _, _, _, onswing = GetCurrentCastingInfo() return onswing ~= 1 end, mybuffcount = function(conditionals) return Multi(conditionals.mybuffcount,function (v) return CleveRoids.ValidatePlayerAuraCount(v.bigger, v.amount) end, conditionals, "mybuffcount") end, mhimbue = function(conditionals) local imbueName = nil -- Case 1: conditionals.mhimbue is a string (e.g., [mhimbue]Frostbrand) if type(conditionals.mhimbue) == "string" then imbueName = conditionals.mhimbue -- Case 2: conditionals.mhimbue is a table (e.g., [mhimbue:Frostbrand]) elseif type(conditionals.mhimbue) == "table" and table.getn(conditionals.mhimbue) > 0 then imbueName = conditionals.mhimbue[1] -- Use first value -- Case 3: Boolean true means check for any imbue elseif conditionals.mhimbue == true then imbueName = nil -- Check for existence only end return CleveRoids.ValidateWeaponImbue("mh", imbueName) end, nomhimbue = function(conditionals) local imbueName = nil -- Case 1: conditionals.nomhimbue is a string if type(conditionals.nomhimbue) == "string" then imbueName = conditionals.nomhimbue -- Case 2: conditionals.nomhimbue is a table elseif type(conditionals.nomhimbue) == "table" and table.getn(conditionals.nomhimbue) > 0 then imbueName = conditionals.nomhimbue[1] -- Case 3: Boolean true elseif conditionals.nomhimbue == true then imbueName = nil end return not CleveRoids.ValidateWeaponImbue("mh", imbueName) end, ohimbue = function(conditionals) local imbueName = nil -- Case 1: conditionals.ohimbue is a string if type(conditionals.ohimbue) == "string" then imbueName = conditionals.ohimbue -- Case 2: conditionals.ohimbue is a table elseif type(conditionals.ohimbue) == "table" and table.getn(conditionals.ohimbue) > 0 then imbueName = conditionals.ohimbue[1] -- Case 3: Boolean true elseif conditionals.ohimbue == true then imbueName = nil end return CleveRoids.ValidateWeaponImbue("oh", imbueName) end, noohimbue = function(conditionals) local imbueName = nil -- Case 1: conditionals.noohimbue is a string if type(conditionals.noohimbue) == "string" then imbueName = conditionals.noohimbue -- Case 2: conditionals.noohimbue is a table elseif type(conditionals.noohimbue) == "table" and table.getn(conditionals.noohimbue) > 0 then imbueName = conditionals.noohimbue[1] -- Case 3: Boolean true elseif conditionals.noohimbue == true then imbueName = nil end return not CleveRoids.ValidateWeaponImbue("oh", imbueName) end, immune = function(conditionals) -- Check if target is immune to the spell being cast or damage school -- Usage: [immune] SpellName OR [immune:SpellName] OR [immune:fire] local checkValue = nil -- Case 1: [immune:SpellName] or [immune:fire] if type(conditionals.immune) == "table" and table.getn(conditionals.immune) > 0 then checkValue = conditionals.immune[1] elseif type(conditionals.immune) == "string" then checkValue = conditionals.immune -- Case 2: [immune] SpellName (check the action being cast) elseif conditionals.action then checkValue = conditionals.action end if not checkValue then return false end return CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) end, noimmune = function(conditionals) -- Check if target is NOT immune to the spell being cast or damage school -- Usage: [noimmune] SpellName OR [noimmune:SpellName] OR [noimmune:fire] local checkValue = nil -- Case 1: [noimmune:SpellName] or [noimmune:fire] if type(conditionals.noimmune) == "table" and table.getn(conditionals.noimmune) > 0 then checkValue = conditionals.noimmune[1] elseif type(conditionals.noimmune) == "string" then checkValue = conditionals.noimmune -- Case 2: [noimmune] SpellName (check the action being cast) elseif conditionals.action then checkValue = conditionals.action end if not checkValue then return true -- If we can't determine spell/school, assume not immune end return not CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) end, -- SP_SwingTimer integration conditionals -- Checks percentage of swing time that has elapsed -- Usage: [swingtimer:<15] = less than 15% of swing has elapsed (early in swing) -- [swingtimer:>80] = more than 80% of swing has elapsed (late in swing) swingtimer = function(conditionals) return Multi(conditionals.swingtimer, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<80) if args.comparisons and type(args.comparisons) == "table" then -- Check if SP_SwingTimer is loaded if st_timer == nil then if not CleveRoids._swingTimerErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swingtimer] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) CleveRoids._swingTimerErrorShown = true end return false end local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return false end local timeElapsed = attackSpeed - st_timer local percentElapsed = (timeElapsed / attackSpeed) * 100 -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](percentElapsed, comp.amount) then return false end end return true end return CleveRoids.ValidateSwingTimer(args.operator, args.amount) end, conditionals, "swingtimer") end, -- Alias for swingtimer stimer = function(conditionals) return Multi(conditionals.stimer, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<80) if args.comparisons and type(args.comparisons) == "table" then -- Check if SP_SwingTimer is loaded if st_timer == nil then if not CleveRoids._swingTimerErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swingtimer] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) CleveRoids._swingTimerErrorShown = true end return false end local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return false end local timeElapsed = attackSpeed - st_timer local percentElapsed = (timeElapsed / attackSpeed) * 100 -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](percentElapsed, comp.amount) then return false end end return true end return CleveRoids.ValidateSwingTimer(args.operator, args.amount) end, conditionals, "stimer") end, -- Negated swingtimer noswingtimer = function(conditionals) return NegatedMulti(conditionals.noswingtimer, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison by checking positive and negating if args.comparisons and type(args.comparisons) == "table" then -- Check if SP_SwingTimer is loaded if st_timer == nil then if not CleveRoids._swingTimerErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swingtimer] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) CleveRoids._swingTimerErrorShown = true end return true end local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return true end local timeElapsed = attackSpeed - st_timer local percentElapsed = (timeElapsed / attackSpeed) * 100 -- Check if ALL comparisons pass for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return true end if not CleveRoids.comparators[comp.operator](percentElapsed, comp.amount) then return true -- One failed, so positive=false, negated=true end end return false -- All passed, so positive=true, negated=false end return not CleveRoids.ValidateSwingTimer(args.operator, args.amount) end, conditionals, "noswingtimer") end, -- Alias for noswingtimer nostimer = function(conditionals) return NegatedMulti(conditionals.nostimer, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison by checking positive and negating if args.comparisons and type(args.comparisons) == "table" then -- Check if SP_SwingTimer is loaded if st_timer == nil then if not CleveRoids._swingTimerErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swingtimer] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) CleveRoids._swingTimerErrorShown = true end return true end local attackSpeed = UnitAttackSpeed("player") if not attackSpeed or attackSpeed <= 0 then return true end local timeElapsed = attackSpeed - st_timer local percentElapsed = (timeElapsed / attackSpeed) * 100 -- Check if ALL comparisons pass for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return true end if not CleveRoids.comparators[comp.operator](percentElapsed, comp.amount) then return true -- One failed, so positive=false, negated=true end end return false -- All passed, so positive=true, negated=false end return not CleveRoids.ValidateSwingTimer(args.operator, args.amount) end, conditionals, "nostimer") end, -- Threat percentage conditional (reads server data via CHAT_MSG_ADDON) -- Usage: [threat:>80] - true if threat is above 80% -- 100% = will pull aggro -- Note: Requires TWThreat addon to request threat data from server threat = function(conditionals) return Multi(conditionals.threat, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >50&<90) if args.comparisons and type(args.comparisons) == "table" then local threatpct = CleveRoids.GetPlayerThreatPercent() if threatpct == nil then return false end -- ALL comparisons must pass (AND logic) for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](threatpct, comp.amount) then return false end end return true end return CleveRoids.ValidateThreat(args.operator, args.amount) end, conditionals, "threat") end, -- Negated threat conditional nothreat = function(conditionals) return NegatedMulti(conditionals.nothreat, function(args) if type(args) ~= "table" then return false end if args.comparisons and type(args.comparisons) == "table" then local threatpct = CleveRoids.GetPlayerThreatPercent() if threatpct == nil then return true end for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return true end if not CleveRoids.comparators[comp.operator](threatpct, comp.amount) then return true end end return false end return not CleveRoids.ValidateThreat(args.operator, args.amount) end, conditionals, "nothreat") end, -- Time-To-Kill conditional (requires TimeToKill addon) -- Usage: [ttk:<10] - true if target will die in less than 10 seconds ttk = function(conditionals) return Multi(conditionals.ttk, function(args) if type(args) ~= "table" then return false end -- Handle multi-comparison (e.g., >5&<15) if args.comparisons and type(args.comparisons) == "table" then if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTK) ~= "function" then if not CleveRoids._ttkErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [ttk] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) CleveRoids._ttkErrorShown = true end return false end local ttk = TimeToKill.GetTTK() if ttk == nil then return false end for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](ttk, comp.amount) then return false end end return true end return CleveRoids.ValidateTTK(args.operator, args.amount) end, conditionals, "ttk") end, -- Negated TTK conditional nottk = function(conditionals) return NegatedMulti(conditionals.nottk, function(args) if type(args) ~= "table" then return false end if args.comparisons and type(args.comparisons) == "table" then if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTK) ~= "function" then if not CleveRoids._ttkErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [ttk] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) CleveRoids._ttkErrorShown = true end return true end local ttk = TimeToKill.GetTTK() if ttk == nil then return true end for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return true end if not CleveRoids.comparators[comp.operator](ttk, comp.amount) then return true end end return false end return not CleveRoids.ValidateTTK(args.operator, args.amount) end, conditionals, "nottk") end, -- Time-To-Execute conditional (requires TimeToKill addon) -- Usage: [tte:<5] - true if target will reach 20% HP in less than 5 seconds tte = function(conditionals) return Multi(conditionals.tte, function(args) if type(args) ~= "table" then return false end if args.comparisons and type(args.comparisons) == "table" then if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTE) ~= "function" then if not CleveRoids._ttkErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [tte] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) CleveRoids._ttkErrorShown = true end return false end local tte = TimeToKill.GetTTE() if tte == nil then return false end for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return false end if not CleveRoids.comparators[comp.operator](tte, comp.amount) then return false end end return true end return CleveRoids.ValidateTTE(args.operator, args.amount) end, conditionals, "tte") end, -- Negated TTE conditional notte = function(conditionals) return NegatedMulti(conditionals.notte, function(args) if type(args) ~= "table" then return false end if args.comparisons and type(args.comparisons) == "table" then if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTE) ~= "function" then if not CleveRoids._ttkErrorShown then DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [tte] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) CleveRoids._ttkErrorShown = true end return true end local tte = TimeToKill.GetTTE() if tte == nil then return true end for _, comp in ipairs(args.comparisons) do if not CleveRoids.operators[comp.operator] then return true end if not CleveRoids.comparators[comp.operator](tte, comp.amount) then return true end end return false end return not CleveRoids.ValidateTTE(args.operator, args.amount) end, conditionals, "notte") end, -- Slam clip window conditionals for Warrior Slam rotation optimization -- Based on math: MaxSlamPercent = (SwingTimer - SlamCastTime) / SwingTimer * 100 -- Requires SP_SwingTimer addon and Nampower for cast time lookup -- [noslamclip] - True if casting Slam NOW will NOT clip the auto-attack -- Usage: /cast [noslamclip] Slam noslamclip = function(conditionals) return CleveRoids.ValidateNoSlamClip() end, -- [slamclip] - True if casting Slam NOW WILL clip the auto-attack (negated) -- Usage: /cast [slamclip] Heroic Strike -- Use HS instead when past slam window slamclip = function(conditionals) return not CleveRoids.ValidateNoSlamClip() end, -- [nonextslamclip] - True if casting an instant NOW will NOT cause NEXT Slam to clip -- Scenario: Skip Slam this swing, cast instant, then Slam next swing without clipping -- Formula: MaxInstantPercent = (2 * SwingTimer - SlamCastTime - GCD) / SwingTimer * 100 -- Usage: /cast [nonextslamclip] Bloodthirst nonextslamclip = function(conditionals) return CleveRoids.ValidateNoNextSlamClip() end, -- [nextslamclip] - True if casting an instant NOW WILL cause NEXT Slam to clip (negated) -- Usage: Use this when you want to know you're past the instant window nextslamclip = function(conditionals) return not CleveRoids.ValidateNoNextSlamClip() end, -- Checks if the target uses a specific power type (mana, rage, energy) powertype = function(conditionals) local unit = conditionals.target or "target" if not UnitExists(unit) then return false end return Or(conditionals.powertype, function(powerTypeName) local powerType = UnitPowerType(unit) local powerTypeLower = string.lower(powerTypeName or "") if powerTypeLower == "mana" then return powerType == 0 elseif powerTypeLower == "rage" then return powerType == 1 elseif powerTypeLower == "focus" then return powerType == 2 elseif powerTypeLower == "energy" then return powerType == 3 end return false end) end, -- Checks if the target does NOT use a specific power type nopowertype = function(conditionals) local unit = conditionals.target or "target" if not UnitExists(unit) then return true end return NegatedMulti(conditionals.nopowertype, function(powerTypeName) local powerType = UnitPowerType(unit) local powerTypeLower = string.lower(powerTypeName or "") if powerTypeLower == "mana" then return powerType ~= 0 elseif powerTypeLower == "rage" then return powerType ~= 1 elseif powerTypeLower == "focus" then return powerType ~= 2 elseif powerTypeLower == "energy" then return powerType ~= 3 end return true end, conditionals, "nopowertype") end }