mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
4017 lines
148 KiB
Lua
4017 lines
148 KiB
Lua
--[[
|
|
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()
|
|
-- Strip rank from input spell name to match cache keys
|
|
-- This handles cases like "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)"
|
|
if spellName then
|
|
spellName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "")
|
|
end
|
|
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
|
|
|
|
-- PERFORMANCE: Local function refs and reusable pattern for buff checking
|
|
local _string_lower = string.lower
|
|
local _string_gsub = string.gsub
|
|
local _RANK_PATTERN = "%s*%(%s*Rank%s+%d+%s*%)"
|
|
|
|
-- PERFORMANCE: Simple cache for lowercase spell names (cleared periodically)
|
|
local _spellNameCache = {}
|
|
local _spellNameCacheSize = 0
|
|
local _MAX_SPELL_CACHE = 200
|
|
|
|
-- PERFORMANCE: Cache for base spell names (rank stripped, not lowercased)
|
|
local _baseNameCache = {}
|
|
local _baseNameCacheSize = 0
|
|
|
|
local function GetLowercaseSpellName(spellID)
|
|
local cached = _spellNameCache[spellID]
|
|
if cached then return cached end
|
|
|
|
local name = SpellInfo(spellID)
|
|
if not name then return nil end
|
|
|
|
-- Strip rank and lowercase
|
|
local baseName = _string_gsub(name, _RANK_PATTERN, "")
|
|
local lowerName = _string_lower(baseName)
|
|
|
|
-- Cache if not too large
|
|
if _spellNameCacheSize < _MAX_SPELL_CACHE then
|
|
_spellNameCache[spellID] = lowerName
|
|
_spellNameCacheSize = _spellNameCacheSize + 1
|
|
end
|
|
|
|
return lowerName
|
|
end
|
|
|
|
-- PERFORMANCE: Get base spell name (rank stripped) and full name - cached
|
|
local function GetSpellNames(spellID)
|
|
local cached = _baseNameCache[spellID]
|
|
if cached then
|
|
return cached.base, cached.full
|
|
end
|
|
|
|
local fullName = SpellInfo(spellID)
|
|
if not fullName then return nil, nil end
|
|
|
|
local baseName = _string_gsub(fullName, _RANK_PATTERN, "")
|
|
|
|
-- Cache if not too large
|
|
if _baseNameCacheSize < _MAX_SPELL_CACHE then
|
|
_baseNameCache[spellID] = { base = baseName, full = fullName }
|
|
_baseNameCacheSize = _baseNameCacheSize + 1
|
|
end
|
|
|
|
return baseName, fullName
|
|
end
|
|
|
|
-- Clear spell name caches (called periodically from Core.lua cleanup)
|
|
function CleveRoids.ClearSpellNameCaches()
|
|
for k in pairs(_spellNameCache) do
|
|
_spellNameCache[k] = nil
|
|
end
|
|
_spellNameCacheSize = 0
|
|
|
|
for k in pairs(_baseNameCache) do
|
|
_baseNameCache[k] = nil
|
|
end
|
|
_baseNameCacheSize = 0
|
|
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
|
|
-- PERFORMANCE: Use cached lowercase spell name lookup
|
|
local lowerName = GetLowercaseSpellName(current_spellID)
|
|
if lowerName and lowerName == searchName then
|
|
found = true
|
|
break
|
|
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
|
|
-- PERFORMANCE: Use cached lowercase spell name lookup
|
|
local lowerName = GetLowercaseSpellName(current_spellID)
|
|
if lowerName and lowerName == searchName then
|
|
found = true
|
|
break
|
|
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
|
|
-- PERFORMANCE: Use cached spell name lookup
|
|
local baseName, fullName = GetSpellNames(debuffSpellID)
|
|
if baseName and (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
|
|
|
|
-- 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
|
|
-- PERFORMANCE: Use cached spell name lookup
|
|
local baseName, fullName = GetSpellNames(buffSpellID)
|
|
if baseName and (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
|
|
-- 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
|
|
-- PERFORMANCE: Use cached spell name lookup
|
|
local baseName, fullName = GetSpellNames(spellID)
|
|
if baseName and (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
|
|
-- PERFORMANCE: Use cached spell name lookup
|
|
local baseName, fullName = GetSpellNames(spellID)
|
|
if baseName and (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,
|
|
|
|
-- Check if player has NO current target (target frame is empty)
|
|
-- Different from noexists: notarget checks player's target, noexists checks @unit
|
|
-- Usage: /target [notarget,@mouseover] - target mouseover only if no current target
|
|
notarget = function(conditionals)
|
|
return not UnitExists("target")
|
|
end,
|
|
|
|
-- Check if player HAS a current target (target frame is occupied)
|
|
-- Usage: /cast [hastarget] Spell - only cast if player has a target selected
|
|
hastarget = function(conditionals)
|
|
return UnitExists("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 Multi(conditionals.combat, function(unit)
|
|
return UnitExists(unit) and UnitAffectingCombat(unit)
|
|
end, conditionals, "combat")
|
|
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 Multi(conditionals.reactive, function (v)
|
|
return CleveRoids.IsReactiveUsable(v)
|
|
end, conditionals, "reactive")
|
|
end,
|
|
|
|
noreactive = function(conditionals)
|
|
return NegatedMulti(conditionals.noreactive, function (v)
|
|
return not CleveRoids.IsReactiveUsable(v)
|
|
end, conditionals, "noreactive")
|
|
end,
|
|
|
|
usable = function(conditionals)
|
|
return Multi(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, conditionals, "usable")
|
|
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 Multi(conditionals.checkchanneled, function(channeledSpells)
|
|
return CleveRoids.CheckChanneled(channeledSpells)
|
|
end, conditionals, "checkchanneled")
|
|
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 Multi(conditionals.checkcasting, function(castingSpells)
|
|
return CleveRoids.CheckCasting(castingSpells)
|
|
end, conditionals, "checkcasting")
|
|
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 Multi(conditionals.mybuff, function(v)
|
|
return CleveRoids.ValidatePlayerBuff(v)
|
|
end, conditionals, "mybuff")
|
|
end,
|
|
|
|
nomybuff = function(conditionals)
|
|
return NegatedMulti(conditionals.nomybuff, function(v)
|
|
return not CleveRoids.ValidatePlayerBuff(v)
|
|
end, conditionals, "nomybuff")
|
|
end,
|
|
|
|
mydebuff = function(conditionals)
|
|
return Multi(conditionals.mydebuff, function(v)
|
|
return CleveRoids.ValidatePlayerDebuff(v)
|
|
end, conditionals, "mydebuff")
|
|
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 Multi(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, conditionals, "inrange")
|
|
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 Multi(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, conditionals, "outrange")
|
|
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
|
|
}
|