From 3a3719a37a1d1b9811ef501dfbf1b9fd04f8d5ba Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Thu, 12 Feb 2026 21:19:20 -0500 Subject: [PATCH 01/12] initial overflowtest --- Conditionals.lua | 196 +++++++++- Core.lua | 68 +++- Extensions/MacroErrorUI.lua | 50 ++- Extensions/OverflowBuffFrame.lua | 620 +++++++++++++++++++++++++++++++ NampowerAPI.lua | 41 +- SuperCleveRoidMacros.toc | 1 + Utility.lua | 19 + 7 files changed, 976 insertions(+), 19 deletions(-) create mode 100644 Extensions/OverflowBuffFrame.lua diff --git a/Conditionals.lua b/Conditionals.lua index 29f37bf..7d72baa 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -849,6 +849,14 @@ CleveRoids.AuraCapStatus = { lastCleanupTime = 0, } +-- Overflow buff tracking: buffs applied while buff-capped (v2.34+) +-- The server maintains up to 48 buff slots (32 visible + 16 invisible overflow). +-- Buffs in slots 33-48 are mechanically active but have no client aura slot, +-- so GetPlayerBuff/GetPlayerAuraDuration can't see them. +-- Populated from AURA_CAST_ON_SELF when auraCapStatus indicates buff bar full. +-- Format: [spellId] = { timestamp = GetTime(), durationSec = durationMs/1000 } +CleveRoids.OverflowBuffs = {} + -- All-caster aura duration tracking (from AURA_CAST events) -- Tracks buff/debuff durations from ANY caster, not just player -- Structure: [targetGuid][spellId] = { start, duration, casterGuid } @@ -994,10 +1002,68 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur local now = GetTime() CleveRoids.AuraCapStatus.playerLastUpdate = now + local buffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL) + -- Check buff bar full (bit 1) - CleveRoids.AuraCapStatus.playerBuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL) + CleveRoids.AuraCapStatus.playerBuffCapped = buffCapped -- Check debuff bar full (bit 2) CleveRoids.AuraCapStatus.playerDebuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL) + + -- Track overflow buffs: if buff bar is full, this aura may land in server slots 33-48 + -- (no client aura slot). Store spellId + duration so /cancelaura and [mybuff] can find it. + -- But first verify it's actually a buff — debuffs applied while buff-capped go into + -- debuff slots (32-47), not buff overflow. Player debuffs never overflow into buff slots. + if buffCapped and spellId and spellId > 0 and effectAuraName and effectAuraName > 0 then + -- Check debuff slots (32-47) — if the spellId is there, it's a debuff, not overflow + local isDebuff = false + if _G.GetPlayerAuraDuration then + for slot = 32, 47 do + local sid = _G.GetPlayerAuraDuration(slot) + if sid and sid == spellId then + isDebuff = true + break + end + end + end + + if not isDebuff then + local entry = CleveRoids.OverflowBuffs[spellId] + if not entry then + entry = {} + CleveRoids.OverflowBuffs[spellId] = entry + end + entry.timestamp = now + entry.durationSec = durationMs and (durationMs / 1000) or 0 + end + elseif not buffCapped and next(CleveRoids.OverflowBuffs) then + -- No longer buff-capped: some overflow buffs may have gotten real slots. + -- Only remove entries that now appear in a visible aura slot. + -- The server does NOT auto-migrate overflow buffs into freed slots, + -- so we can't blindly clear everything. + if _G.GetPlayerAuraDuration then + local visibleSpells = {} + for slot = 0, 31 do + local sid = _G.GetPlayerAuraDuration(slot) + if sid and sid > 0 then + visibleSpells[sid] = true + end + end + for k in pairs(CleveRoids.OverflowBuffs) do + if visibleSpells[k] then + CleveRoids.OverflowBuffs[k] = nil + end + end + end + -- Also prune expired entries + for k, entry in pairs(CleveRoids.OverflowBuffs) do + if entry.durationSec and entry.durationSec > 0 then + local elapsed = now - (entry.timestamp or 0) + if elapsed > entry.durationSec then + CleveRoids.OverflowBuffs[k] = nil + end + end + end + end end local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAuraName, @@ -1663,6 +1729,58 @@ end function CleveRoids.CancelAura(auraName) local ix = 0 auraName = string.lower(string.gsub(auraName, "_"," ")) + + -- v2.34+ path: cancel by spell ID (works for buff-capped overflow auras too) + local API = CleveRoids.NampowerAPI + if API and API.features.hasCancelPlayerAuraSpellId and CleveRoids.hasSuperwow then + -- First scan visible buffs via GetPlayerBuff (fast, covers normal case) + while true do + local aura_ix = GetPlayerBuff(ix, "HELPFUL") + ix = ix + 1 + if aura_ix == -1 then break end + local bid = GetPlayerBuffID(aura_ix) + bid = (bid < -1) and (bid + 65536) or bid + if string.lower(SpellInfo(bid)) == auraName then + _G.CancelPlayerAuraSpellId(bid, 1) + return true + end + end + + -- Not found in visible buffs - scan all 32 raw aura slots + -- GetPlayerAuraDuration reads unit data fields directly (same 32 slots but bypasses UI filtering) + if API.features.hasGetPlayerAuraDuration and _G.GetPlayerAuraDuration then + for slot = 0, 31 do + local spellId = _G.GetPlayerAuraDuration(slot) + if spellId and spellId > 0 then + local name = SpellInfo(spellId) + if name and string.lower(name) == auraName then + _G.CancelPlayerAuraSpellId(spellId, 1) + return true + end + end + end + end + + -- Final fallback: check overflow buff tracking (buffs applied while buff-capped + -- that have NO client aura slot - tracked via AURA_CAST_ON_SELF events) + for spellId, entry in pairs(CleveRoids.OverflowBuffs) do + -- Skip expired overflow entries + local elapsed = GetTime() - (entry.timestamp or 0) + if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then + CleveRoids.OverflowBuffs[spellId] = nil + else + local name = SpellInfo(spellId) + if name and string.lower(name) == auraName then + _G.CancelPlayerAuraSpellId(spellId, 1) + CleveRoids.OverflowBuffs[spellId] = nil + return true + end + end + end + return false + end + + -- Legacy path for older Nampower versions while true do local aura_ix = GetPlayerBuff(ix,"HELPFUL") ix = ix + 1 @@ -3281,6 +3399,82 @@ function CleveRoids.ValidateAura(unit, args, isbuff) end end + -- Overflow buff fallback: player buffs in server slots 33-48 (no client slot) + -- Check AURA_CAST_ON_SELF tracked overflow data for presence + duration + if not found and isPlayer and isbuff and next(CleveRoids.OverflowBuffs) then + local now = GetTime() + for spellId, entry in pairs(CleveRoids.OverflowBuffs) do + -- Prune expired entries + local elapsed = now - (entry.timestamp or 0) + if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then + CleveRoids.OverflowBuffs[spellId] = nil + else + local match = false + if searchID then + match = (spellId == searchID) + elseif searchName then + local lowerName = GetLowercaseSpellName(spellId) + match = (lowerName and lowerName == searchName) + end + if match then + found = true + stacks = 0 + -- Compute remaining time from apply timestamp + duration + if entry.durationSec and entry.durationSec > 0 then + remaining = entry.durationSec - elapsed + if remaining < 0 then remaining = 0 end + else + remaining = -1 -- Unknown duration (permanent buff) + end + break + end + end + end + end + + -- Non-player overflow fallback: buff exists in server slots 33-48 (no client slot) + -- AllCasterAuraTracking already has data from AURA_CAST_ON_OTHER for all aura applications. + -- If the normal scan didn't find the buff, check there for presence + duration. + -- Guard: verify the spell isn't a visible debuff on the target (AllCasterAuraTracking + -- stores both buffs and debuffs, so without this check [buff:DebuffName] could false-positive). + if not found and not isPlayer and isbuff and (searchID or searchName) then + local _, targetGuid = UnitExists(unit) + if targetGuid then + -- Check if the spell is in a visible debuff slot — if so, it's a debuff, not a buff + local isDebuff = false + local di = 1 + while true do + local dtex, _, _, dspellId = UnitDebuff(unit, di) + if not dtex then break end + if dspellId then + if searchID then + if dspellId == searchID then + isDebuff = true + break + end + elseif searchName then + local lowerName = GetLowercaseSpellName(dspellId) + if lowerName and lowerName == searchName then + isDebuff = true + break + end + end + end + di = di + 1 + end + + if not isDebuff then + local trackRemaining = CleveRoids.FindAllCasterAuraByName(targetGuid, + searchID and tostring(searchID) or args.name) + if trackRemaining then + found = true + stacks = 0 + remaining = trackRemaining + end + end + end + end + local ops = CleveRoids.operators local cmp = CleveRoids.comparators diff --git a/Core.lua b/Core.lua index 29b6d83..34b4118 100644 --- a/Core.lua +++ b/Core.lua @@ -3181,16 +3181,15 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) end end - -- Item not found via v2.18 lookup + -- Item not found via v2.18 lookup - fall through to legacy path if CleveRoids.equipDebugLog then - CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot|r") + CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot, trying legacy fallback|r") -- Debug: Try direct FindPlayerItemSlot call if FindPlayerItemSlot then local bag, slot = FindPlayerItemSlot(msg) CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r") end end - return false end -- Fallback for older Nampower versions @@ -3294,13 +3293,25 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) if EquipItemByName then local ok = pcall(EquipItemByName, msg, invslot) if ok then - -- Invalidate cache entry if it exists - if CleveRoids.Items then - CleveRoids.Items[msg] = nil - CleveRoids.Items[string_lower(msg)] = nil + -- Verify the item actually landed in the target slot + -- EquipItemByName may silently no-op for same-named items in paired slots + -- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot) + local newLink = GetInventoryItemLink("player", invslot) + if newLink then + local _, _, newName = string_find(newLink, "|h%[(.-)%]|h") + if newName and string_lower(newName) == string_lower(msg) then + if CleveRoids.Items then + CleveRoids.Items[msg] = nil + CleveRoids.Items[string_lower(msg)] = nil + end + InvalidateDisplacedItem() + return true + end + end + -- Verification failed - EquipItemByName didn't place item in target slot + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. msg .. "', " .. invslot .. ") succeeded but item not in target slot, trying fallback|r") end - InvalidateDisplacedItem() - return true end end @@ -3336,12 +3347,23 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) if item.name and EquipItemByName and item.name ~= msg then local ok = pcall(EquipItemByName, item.name, invslot) if ok then - if CleveRoids.Items then - CleveRoids.Items[item.name] = nil - CleveRoids.Items[string_lower(item.name)] = nil + -- Verify the item actually landed in the target slot (same guard as above) + local newLink = GetInventoryItemLink("player", invslot) + if newLink then + local _, _, newName = string_find(newLink, "|h%[(.-)%]|h") + if newName and string_lower(newName) == string_lower(item.name) then + if CleveRoids.Items then + CleveRoids.Items[item.name] = nil + CleveRoids.Items[string_lower(item.name)] = nil + end + InvalidateDisplacedItem() + return true + end + end + -- Verification failed - fall through to manual cursor-based equip + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. item.name .. "', " .. invslot .. ") succeeded but item not in target slot, trying manual fallback|r") end - InvalidateDisplacedItem() - return true end end @@ -5824,6 +5846,8 @@ SlashCmdList["CLEVEROID"] = function(msg) DEFAULT_CHAT_FRAME:AddMessage('/cleveroid tankdebug - Debug tank targeting conditionals') DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00All-Caster Aura Tracking:|r") DEFAULT_CHAT_FRAME:AddMessage('/cleveroid auradebug - Debug buff/debuff time tracking from all casters') + DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Overflow Buff Frame:|r") + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowtest - Toggle overflow buff frame test mode') return end @@ -6559,6 +6583,22 @@ SlashCmdList["CLEVEROID"] = function(msg) return end + -- overflowtest (toggle overflow buff frame test mode) + if cmd == "overflowtest" then + if CleveRoids.ToggleOverflowTest then + CleveRoids.ToggleOverflowTest() + else + CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r") + end + return + end + + -- Delegate to extension console handlers (chain pattern) + if CleveRoids.HandleConsoleCommand then + CleveRoids.HandleConsoleCommand(msg) + return + end + -- Unknown command fallback CleveRoids.Print("Usage:") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings") diff --git a/Extensions/MacroErrorUI.lua b/Extensions/MacroErrorUI.lua index b2304db..0d32087 100644 --- a/Extensions/MacroErrorUI.lua +++ b/Extensions/MacroErrorUI.lua @@ -29,6 +29,7 @@ local pendingValidation = false local lastSelectedMacro = nil local updateFrame = nil local hooked = false +local measureFs = nil -- hidden FontString for measuring text width (wrap detection) -- Cache for current errors (avoids re-validation on every frame) local currentErrors = nil @@ -186,18 +187,61 @@ local function UpdateLineHighlights(errors) local editWidth = MacroFrameText:GetWidth() if editWidth < 10 then editWidth = 260 end -- fallback + -- Create measurement FontString lazily (same font as EditBox, hidden) + if not measureFs then + measureFs = (MacroFrame or UIParent):CreateFontString(nil, "OVERLAY") + measureFs:Hide() + end + local fontPath, fontSize, fontFlags = MacroFrameText:GetFont() + if fontPath then + measureFs:SetFont(fontPath, fontSize, fontFlags) + end + + -- Split macro text into logical lines for wrap calculation + local text = MacroFrameText:GetText() or "" + local logicalLines = {} + local lineStart = 1 + while true do + local nlPos = string.find(text, "\n", lineStart, true) + if nlPos then + table.insert(logicalLines, string.sub(text, lineStart, nlPos - 1)) + lineStart = nlPos + 1 + else + table.insert(logicalLines, string.sub(text, lineStart)) + break + end + end + + -- Calculate visual line count for each logical line + local visualCounts = {} + for i, line in ipairs(logicalLines) do + if line == "" or editWidth <= 0 then + visualCounts[i] = 1 + else + measureFs:SetText(line) + local textWidth = measureFs:GetStringWidth() + visualCounts[i] = math.max(1, math.ceil(textWidth / editWidth)) + end + end + local highlightIdx = 1 for lineNum, _ in pairs(errorLines) do if highlightIdx > HIGHLIGHT_POOL_SIZE then break end local tex = lineHighlights[highlightIdx] - -- Position: top text inset + (lineNum-1) lines down - local yOffset = -topInset - ((lineNum - 1) * lineHeight) + + -- Y offset: sum visual lines of all preceding logical lines + local yLines = 0 + for i = 1, lineNum - 1 do + yLines = yLines + (visualCounts[i] or 1) + end + local yOffset = -topInset - (yLines * lineHeight) + local highlightHeight = (visualCounts[lineNum] or 1) * lineHeight tex:ClearAllPoints() tex:SetPoint("TOPLEFT", MacroFrameText, "TOPLEFT", -2, yOffset) tex:SetWidth(editWidth + 4) - tex:SetHeight(lineHeight) + tex:SetHeight(highlightHeight) tex:Show() highlightIdx = highlightIdx + 1 diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua new file mode 100644 index 0000000..c0d1e50 --- /dev/null +++ b/Extensions/OverflowBuffFrame.lua @@ -0,0 +1,620 @@ +--[[ + OverflowBuffFrame Extension + Two frames displaying overflow buffs (slots 33-48) for the player + and the current target (if a group/party member). + Each frame shows 2 rows of 8 icons = 16 slots. + + Author: Mewtiny + License: MIT License +]] +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids or {} + +-- ============================================================================ +-- Constants +-- ============================================================================ +local ICON_SIZE = 24 +local ICON_SPACING = 2 +local ICONS_PER_ROW = 8 +local NUM_ROWS = 2 +local ICONS_PER_FRAME = ICONS_PER_ROW * NUM_ROWS -- 16 +local ROW_SPACING = 16 -- vertical gap between rows (icon + duration text) +local DURATION_FONT_SIZE = 10 +local DURATION_OFFSET_Y = -3 +local UPDATE_INTERVAL = 0.2 +local LABEL_HEIGHT = 12 + +-- ============================================================================ +-- State +-- ============================================================================ +local playerFrame = nil +local targetFrame = nil +local playerIcons = {} -- [1..16] +local targetIcons = {} -- [1..16] +local playerLabelFs = nil +local targetLabelFs = nil +local updateFrame = nil +local updateElapsed = 0 +local testMode = false +local lastPlayerCount = 0 +local lastTargetCount = 0 + +-- ============================================================================ +-- Duration Formatting (matches pfUI api.lua:1324 style) +-- ============================================================================ +local function FormatDuration(seconds) + if not seconds or seconds <= 0 then return "" end + if seconds >= 100 then + return math.floor(seconds / 60 + 0.5) .. "m" + elseif seconds > 5 then + return math.floor(seconds + 0.5) .. "s" + else + return string.format("%.1f", seconds) + end +end + +-- Forward declarations (needed by OnClick closures in CreateIconButton) +local RebuildFrame + +-- ============================================================================ +-- Icon Button Creation +-- ============================================================================ +local function CreateIconButton(parent, index, iconTable) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(ICON_SIZE) + btn:SetHeight(ICON_SIZE) + btn:RegisterForClicks("RightButtonUp") + + -- Dark 1px border backdrop (pfUI style) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + btn:SetBackdropColor(0, 0, 0, 0.6) + btn:SetBackdropBorderColor(0.1, 0.1, 0.1, 0.9) + + -- Icon texture (inset by 1px for border) + local icon = btn:CreateTexture(nil, "ARTWORK") + icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -1) + icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1) + icon:SetTexCoord(0.07, 0.93, 0.07, 0.93) + icon:Hide() + + -- Duration text below icon + local dur = btn:CreateFontString(nil, "OVERLAY") + dur:SetFont("Fonts\\FRIZQT__.TTF", DURATION_FONT_SIZE, "OUTLINE") + dur:SetPoint("TOP", btn, "BOTTOM", 0, DURATION_OFFSET_Y) + dur:SetJustifyH("CENTER") + dur:SetTextColor(1, 1, 1, 1) + dur:SetText("") + + -- Right-click to cancel player overflow buffs + btn:SetScript("OnClick", function() + local data = iconTable[index] + if not data or not data.spellId then return end + if data.source ~= "player" then return end + + -- CancelPlayerAuraSpellId(spellId, ignoreMissing) + -- ignoreMissing=1 is required for overflow buffs in invisible slots 33-48 + if CancelPlayerAuraSpellId then + CancelPlayerAuraSpellId(data.spellId, 1) + end + + -- Remove from overflow tracking + if CleveRoids.OverflowBuffs then + CleveRoids.OverflowBuffs[data.spellId] = nil + end + + -- Hide tooltip and rebuild + GameTooltip:Hide() + RebuildFrame() + end) + + -- Tooltip + btn:SetScript("OnEnter", function() + local data = iconTable[index] + if not data or not data.spellId then return end + GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT") + local spellName = SpellInfo and SpellInfo(data.spellId) or ("Spell " .. data.spellId) + if spellName then + local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + GameTooltip:AddLine(baseName, 1, 1, 1) + end + if data.remaining and data.remaining > 0 then + GameTooltip:AddLine(FormatDuration(data.remaining) .. " remaining", 0.7, 0.7, 0.7) + end + if data.source == "player" then + GameTooltip:AddLine("Right-click to cancel", 0.8, 0.8, 0.5) + else + GameTooltip:AddLine("Overflow buff (target)", 0.5, 1.0, 0.8) + end + GameTooltip:Show() + end) + btn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + btn:Hide() + + return { + button = btn, + icon = icon, + duration = dur, + spellId = nil, + remaining = nil, + source = nil, + } +end + +-- ============================================================================ +-- Frame Creation +-- ============================================================================ + +-- Compute frame dimensions +local FRAME_WIDTH = ICONS_PER_ROW * (ICON_SIZE + ICON_SPACING) - ICON_SPACING +local FRAME_HEIGHT = LABEL_HEIGHT + NUM_ROWS * (ICON_SIZE + ROW_SPACING) + +local function CreateBuffFrame(name, savedPosKey, defaultY, labelColor, iconTable) + local frame = CreateFrame("Frame", name, UIParent) + frame:SetWidth(FRAME_WIDTH) + frame:SetHeight(FRAME_HEIGHT) + frame:SetPoint("TOP", UIParent, "TOP", 0, defaultY) + frame:SetFrameStrata("MEDIUM") + frame:SetMovable(true) + frame:EnableMouse(false) + frame:Hide() + + -- Section label + local label = frame:CreateFontString(nil, "OVERLAY") + label:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") + label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) + label:SetTextColor(labelColor[1], labelColor[2], labelColor[3], 0.8) + label:SetText("") + + -- Create 16 icon slots in 2 rows of 8 + for i = 1, ICONS_PER_FRAME do + iconTable[i] = CreateIconButton(frame, i, iconTable) + local row = math.floor((i - 1) / ICONS_PER_ROW) + local col = math.mod(i - 1, ICONS_PER_ROW) + local xOff = col * (ICON_SIZE + ICON_SPACING) + local yOff = -(LABEL_HEIGHT + row * (ICON_SIZE + ROW_SPACING)) + iconTable[i].button:SetPoint("TOPLEFT", frame, "TOPLEFT", xOff, yOff) + end + + -- Shift+drag to move + frame:SetScript("OnMouseDown", function() + if IsShiftKeyDown() then + frame:StartMoving() + end + end) + frame:SetScript("OnMouseUp", function() + frame:StopMovingOrSizing() + local _, _, _, x, y = frame:GetPoint(1) + CleveRoidMacros = CleveRoidMacros or {} + CleveRoidMacros[savedPosKey] = { x = x, y = y } + end) + + -- Restore saved position + CleveRoidMacros = CleveRoidMacros or {} + if CleveRoidMacros[savedPosKey] then + frame:ClearAllPoints() + frame:SetPoint("TOP", UIParent, "TOP", + CleveRoidMacros[savedPosKey].x or 0, + CleveRoidMacros[savedPosKey].y or defaultY) + end + + return frame, label +end + +-- ============================================================================ +-- Data Collection +-- ============================================================================ + +local function GetPlayerOverflowBuffs() + local results = {} + local overflowBuffs = CleveRoids.OverflowBuffs + if not overflowBuffs then return results end + + local now = GetTime() + for spellId, entry in pairs(overflowBuffs) do + if entry.timestamp and entry.durationSec then + local remaining = entry.durationSec - (now - entry.timestamp) + if remaining > 0 then + table.insert(results, { + spellId = spellId, + remaining = remaining, + source = "player", + }) + end + end + end + + table.sort(results, function(a, b) return a.remaining > b.remaining end) + return results +end + +local function GetTargetOverflowBuffs() + local results = {} + + if not UnitExists("target") then return results end + if not testMode and not (UnitInParty("target") or UnitInRaid("target")) then return results end + + local _, targetGuid = UnitExists("target") + if not targetGuid then return results end + + local trackingData = CleveRoids.AllCasterAuraTracking + if not trackingData or not trackingData[targetGuid] then return results end + + -- Build set of visible buff textures on target + local visibleBuffs = {} + for i = 1, 32 do + local texture = UnitBuff("target", i) + if not texture then break end + visibleBuffs[texture] = true + end + + local now = GetTime() + local lib = CleveRoids.libdebuff + + for spellId, auraData in pairs(trackingData[targetGuid]) do + if auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > 0 then + local isVisible = false + if not auraData._testEntry then + local spellIcon = lib and lib:GetCachedIcon(spellId) + isVisible = spellIcon and visibleBuffs[spellIcon] + end + if not isVisible then + table.insert(results, { + spellId = spellId, + remaining = remaining, + source = "target", + }) + end + end + end + end + + table.sort(results, function(a, b) return a.remaining > b.remaining end) + return results +end + +-- ============================================================================ +-- Layout +-- ============================================================================ + +local function PopulateFrame(frame, label, labelText, iconTable, buffs, countRef) + local count = table.getn(buffs) + if count > ICONS_PER_FRAME then count = ICONS_PER_FRAME end + + local lib = CleveRoids.libdebuff + + for i = 1, count do + local data = iconTable[i] + local buff = buffs[i] + + local texture = lib and lib:GetCachedIcon(buff.spellId) + if texture then + data.icon:SetTexture(texture) + else + data.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + end + data.icon:Show() + + data.spellId = buff.spellId + data.remaining = buff.remaining + data.source = buff.source + data.duration:SetText(FormatDuration(buff.remaining)) + data.button:EnableMouse(true) + data.button:Show() + end + + -- Hide unused + for i = count + 1, ICONS_PER_FRAME do + local data = iconTable[i] + data.button:Hide() + data.icon:Hide() + data.duration:SetText("") + data.spellId = nil + data.remaining = nil + data.source = nil + end + + if count > 0 then + label:SetText(labelText) + frame:EnableMouse(true) + frame:Show() + else + label:SetText("") + frame:EnableMouse(false) + frame:Hide() + end + + return count +end + +RebuildFrame = function() + if not playerFrame then return end + + local pBuffs = GetPlayerOverflowBuffs() + lastPlayerCount = PopulateFrame(playerFrame, playerLabelFs, "Overflow (You)", playerIcons, pBuffs) + + local tBuffs = GetTargetOverflowBuffs() + local targetName = UnitExists("target") and UnitName("target") or "Target" + lastTargetCount = PopulateFrame(targetFrame, targetLabelFs, "Overflow (" .. targetName .. ")", targetIcons, tBuffs) +end + +-- Refresh only duration text (no full rebuild) +local function RefreshDurations() + local anyExpired = false + local now = GetTime() + + -- Player durations + if playerFrame and playerFrame:IsVisible() then + for i = 1, lastPlayerCount do + local data = playerIcons[i] + if data.spellId then + local entry = CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[data.spellId] + if entry and entry.timestamp and entry.durationSec then + local remaining = entry.durationSec - (now - entry.timestamp) + if remaining > 0 then + data.remaining = remaining + data.duration:SetText(FormatDuration(remaining)) + else + anyExpired = true + end + else + anyExpired = true + end + end + end + end + + -- Target durations + if targetFrame and targetFrame:IsVisible() then + local targetGuid = nil + if UnitExists("target") then + local _, guid = UnitExists("target") + targetGuid = guid + end + for i = 1, lastTargetCount do + local data = targetIcons[i] + if data.spellId and targetGuid then + local remaining = CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, data.spellId) or 0 + if remaining > 0 then + data.remaining = remaining + data.duration:SetText(FormatDuration(remaining)) + else + anyExpired = true + end + else + anyExpired = true + end + end + end + + if anyExpired then + RebuildFrame() + end +end + +-- ============================================================================ +-- Test Mode +-- ============================================================================ + +local TEST_PLAYER_SPELL_IDS = { + 1126, -- Mark of the Wild + 1243, -- Power Word: Fortitude + 1461, -- Arcane Intellect + 14752, -- Divine Spirit + 976, -- Shadow Protection + 6307, -- Blood Pact + 20217, -- Blessing of Kings + 19740, -- Blessing of Might + 21849, -- Gift of the Wild + 25898, -- Greater Blessing of Kings + 25899, -- Greater Blessing of Sanctuary + 25890, -- Greater Blessing of Light + 10938, -- Greater Power Word: Fortitude + 10157, -- Greater Arcane Intellect + 20911, -- Blessing of Sanctuary + 25782, -- Greater Blessing of Might +} + +local TEST_TARGET_SPELL_IDS = { + 467, -- Thorns + 10060, -- Power Infusion + 17007, -- Leader of the Pack + 24932, -- Leader of the Pack aura + 19506, -- Trueshot Aura + 8936, -- Regrowth + 774, -- Rejuvenation + 139, -- Renew + 1022, -- Blessing of Protection + 6346, -- Fear Ward + 1044, -- Blessing of Freedom + 10958, -- Shadow Protection (rank) + 27681, -- Prayer of Spirit + 21562, -- Prayer of Fortitude + 20914, -- Blessing of Sanctuary (rank) + 25916, -- Greater Blessing of Wisdom +} + +local testTargetGuid = nil +local testStartTime = 0 -- when test mode was enabled (for duration calc) + +-- Remove test entries from a specific GUID in AllCasterAuraTracking +local function ClearTestTargetData(guid) + if not guid or not CleveRoids.AllCasterAuraTracking then return end + local targetData = CleveRoids.AllCasterAuraTracking[guid] + if not targetData then return end + for spellId, entry in pairs(targetData) do + if entry._testEntry then + targetData[spellId] = nil + end + end + if not next(targetData) then + CleveRoids.AllCasterAuraTracking[guid] = nil + end +end + +-- Inject test entries for the current target +local function InjectTestTargetData() + if not UnitExists("target") then return end + local _, targetGuid = UnitExists("target") + if not targetGuid then return end + + -- Clean up old test target if it changed + if testTargetGuid and testTargetGuid ~= targetGuid then + ClearTestTargetData(testTargetGuid) + end + + testTargetGuid = targetGuid + CleveRoids.AllCasterAuraTracking = CleveRoids.AllCasterAuraTracking or {} + if not CleveRoids.AllCasterAuraTracking[targetGuid] then + CleveRoids.AllCasterAuraTracking[targetGuid] = {} + end + + local targetDurations = { 8, 15, 25, 40, 55, 90, 150, 240, 400, 500, 700, 1000, 1300, 1600, 2000, 3000 } + for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do + local spellId = TEST_TARGET_SPELL_IDS[i] + local dur = targetDurations[i] or 60 + CleveRoids.AllCasterAuraTracking[targetGuid][spellId] = { + start = testStartTime, + duration = dur, + casterGuid = "test", + _testEntry = true, + } + end +end + +local function EnableTestMode() + testMode = true + testStartTime = GetTime() + CleveRoids.OverflowBuffs = CleveRoids.OverflowBuffs or {} + + local playerDurations = { 5, 10, 30, 45, 60, 120, 180, 300, 450, 600, 900, 1200, 1500, 1800, 2400, 3600 } + + for i = 1, table.getn(TEST_PLAYER_SPELL_IDS) do + local spellId = TEST_PLAYER_SPELL_IDS[i] + local dur = playerDurations[i] or 60 + CleveRoids.OverflowBuffs[spellId] = { + timestamp = testStartTime, + durationSec = dur, + _testEntry = true, + } + end + + -- Inject target test data if we have a target + InjectTestTargetData() + + CleveRoids.Print("|cff00ff00Overflow buff frame test mode enabled|r") + CleveRoids.Print(" Shift+drag to reposition the frames") + + RebuildFrame() +end + +local function DisableTestMode() + testMode = false + + if CleveRoids.OverflowBuffs then + for spellId, entry in pairs(CleveRoids.OverflowBuffs) do + if entry._testEntry then + CleveRoids.OverflowBuffs[spellId] = nil + end + end + end + + ClearTestTargetData(testTargetGuid) + testTargetGuid = nil + + CleveRoids.Print("|cffff9900Overflow buff frame test mode disabled|r") + + RebuildFrame() +end + +local function ToggleTestMode() + if testMode then + DisableTestMode() + else + EnableTestMode() + end +end + +CleveRoids.ToggleOverflowTest = ToggleTestMode + +-- ============================================================================ +-- OnUpdate Handler +-- ============================================================================ +local function OnUpdate() + updateElapsed = updateElapsed + arg1 + if updateElapsed < UPDATE_INTERVAL then return end + updateElapsed = 0 + RefreshDurations() +end + +-- ============================================================================ +-- Extension Registration +-- ============================================================================ +local ext = CleveRoids.RegisterExtension("OverflowBuffFrame") + +ext.OnTargetChanged = function() + -- Re-inject test data for the new target during test mode + if testMode then + InjectTestTargetData() + end + RebuildFrame() +end + +ext.OnAuraCast = function() + RebuildFrame() +end + +ext.OnGroupChanged = function() + RebuildFrame() +end + +ext.OnPlayerDead = function() + if CleveRoids.OverflowBuffs then + if testMode then + for spellId, entry in pairs(CleveRoids.OverflowBuffs) do + if not entry._testEntry then + CleveRoids.OverflowBuffs[spellId] = nil + end + end + else + CleveRoids.OverflowBuffs = {} + end + end + RebuildFrame() +end + +ext.OnLoad = function() + playerFrame, playerLabelFs = CreateBuffFrame( + "CleveRoidsOverflowPlayer", "overflowPlayerPos", -100, + { 0.4, 0.7, 1.0 }, playerIcons) + + targetFrame, targetLabelFs = CreateBuffFrame( + "CleveRoidsOverflowTarget", "overflowTargetPos", -170, + { 0.4, 1.0, 0.7 }, targetIcons) + + ext.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged") + ext.RegisterEvent("PARTY_MEMBERS_CHANGED", "OnGroupChanged") + ext.RegisterEvent("RAID_ROSTER_UPDATE", "OnGroupChanged") + ext.RegisterEvent("PLAYER_DEAD", "OnPlayerDead") + + if CleveRoids.hasNampower then + ext.RegisterEvent("AURA_CAST_ON_SELF", "OnAuraCast") + end + + -- Shared OnUpdate frame + updateFrame = CreateFrame("Frame") + updateFrame:SetScript("OnUpdate", OnUpdate) + + RebuildFrame() +end + +_G["CleveRoids"] = CleveRoids diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 1a8aca2..e28d2f3 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -85,7 +85,13 @@ - Reads from NampowerSettings addon when available - Falls back to CVars when addon not present - Current version: v2.33.0 + Aura Cancel Functions (v2.34+): + - CancelPlayerAuraSlot(auraSlot) - Cancel buff/debuff by raw 0-based aura slot + - CancelPlayerAuraSpellId(spellId, [ignoreMissing]) - Cancel buff/debuff by spell ID + Pass ignoreMissing=1 to skip aura-slot presence check (for overflow buffs in + server slots 33-48 that have no client aura slot) + + Current version: v2.34.0 ]] local _G = _G or getfenv(0) @@ -242,6 +248,10 @@ API.VERSION_REQUIREMENTS = { -- v2.33+ - UPDATE_DURATION spellId parameter and GetCastInfo guid fix ["DurationEventSpellId"] = { 2, 33, 0 }, ["GetCastInfoGuidFix"] = { 2, 33, 0 }, + + -- v2.34+ - Aura cancel functions + ["CancelPlayerAuraSlot"] = { 2, 34, 0, "CancelPlayerAuraSlot" }, + ["CancelPlayerAuraSpellId"] = { 2, 34, 0, "CancelPlayerAuraSpellId" }, } -- Check if a specific feature is available @@ -382,6 +392,10 @@ local function InitializeFeatures() f.hasDurationEventSpellId = API.HasFeature("DurationEventSpellId") f.hasGetCastInfoGuidFix = API.HasFeature("GetCastInfoGuidFix") + -- v2.34+ Aura cancel functions + f.hasCancelPlayerAuraSlot = API.HasFeature("CancelPlayerAuraSlot") + f.hasCancelPlayerAuraSpellId = API.HasFeature("CancelPlayerAuraSpellId") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() @@ -2982,5 +2996,30 @@ function API.GetSpellPower(mode) return _G.GetSpellPower(mode) end +-------------------------------------------------------------------------------- +-- AURA CANCEL FUNCTIONS (v2.34+) +-------------------------------------------------------------------------------- + +-- Cancel a player aura by raw 0-based aura slot (v2.34+) +-- auraSlot: 0-31 for buffs, 32-47 for debuffs +function API.CancelPlayerAuraSlot(auraSlot) + if not API.features.hasCancelPlayerAuraSlot or not _G.CancelPlayerAuraSlot then + return false + end + _G.CancelPlayerAuraSlot(auraSlot) + return true +end + +-- Cancel a player aura by spell ID (v2.34+) +-- spellId: the spell ID to cancel +-- ignoreMissing: pass 1 to skip aura-slot presence check (useful for buff-capped hidden auras) +function API.CancelPlayerAuraSpellId(spellId, ignoreMissing) + if not API.features.hasCancelPlayerAuraSpellId or not _G.CancelPlayerAuraSpellId then + return false + end + _G.CancelPlayerAuraSpellId(spellId, ignoreMissing) + return true +end + -- Expose API globally for other addons _G.CleveRoidsNampowerAPI = API diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 1d66ce0..afadf78 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -43,4 +43,5 @@ Extensions\Mouseover\Cursive.lua Extensions\Tooltip\Generic.lua Extensions\CursiveCustomSpells.lua +Extensions\OverflowBuffFrame.lua diff --git a/Utility.lua b/Utility.lua index 7f034e4..ef841ea 100644 --- a/Utility.lua +++ b/Utility.lua @@ -4913,6 +4913,25 @@ ev:SetScript("OnEvent", function() CleveRoids.castTracking[guid] = nil end + -- Clean up overflow buff tracking (death removes all buffs) + local _, playerGUID = UnitExists("player") + if playerGUID and guid == playerGUID then + -- Player died: clear all overflow buff entries and reset cap status + if CleveRoids.OverflowBuffs then + for k in pairs(CleveRoids.OverflowBuffs) do + CleveRoids.OverflowBuffs[k] = nil + end + end + if CleveRoids.AuraCapStatus then + CleveRoids.AuraCapStatus.playerBuffCapped = false + CleveRoids.AuraCapStatus.playerDebuffCapped = false + end + end + -- Clean up all-caster aura tracking for the dead unit + if CleveRoids.AllCasterAuraTracking and CleveRoids.AllCasterAuraTracking[guid] then + CleveRoids.AllCasterAuraTracking[guid] = nil + end + -- Clean up GUID to name mapping (after 5 seconds to allow final lookups) -- Actually, keep it for a bit in case we need it for immunity detection -- lib.guidToName[guid] = nil From dae56dbb16e01488ad26a06a8ada9fa44190bd88 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sat, 14 Feb 2026 10:38:03 -0500 Subject: [PATCH 02/12] slashcommand whitelist --- Extensions/MacroErrorUI.lua | 314 +++++++++++++++++++++++++++++++++++- Init.lua | 2 + MacroErrorChecker.lua | 55 ++++++- NampowerAPI.lua | 36 ++++- 4 files changed, 402 insertions(+), 5 deletions(-) diff --git a/Extensions/MacroErrorUI.lua b/Extensions/MacroErrorUI.lua index 0d32087..8418f58 100644 --- a/Extensions/MacroErrorUI.lua +++ b/Extensions/MacroErrorUI.lua @@ -35,6 +35,15 @@ local measureFs = nil -- hidden FontString for measuring text width (wrap detec local currentErrors = nil local nameHighlight = nil -- Yellow backdrop behind macro name when name has errors +-- Whitelist GUI state +local whitelistButton = nil +local whitelistPopup = nil +local WHITELIST_ROW_POOL_SIZE = 8 +local whitelistRows = {} -- pre-allocated label+remove rows + +-- Forward declarations (defined later, referenced by whitelist GUI) +local RequestValidation + -- ============================================================================ -- Error Panel (Option A) -- ============================================================================ @@ -46,7 +55,7 @@ local function CreateErrorPanel() -- Anchor to the scroll frame (text edit area) instead of the full MacroFrame, -- which can extend far below the visible UI on extended macro clients local scrollRef = MacroFrameScrollFrame or MacroFrame - panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", 0, -27) + panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", -20, -27) panel:SetPoint("TOPRIGHT", scrollRef, "BOTTOMRIGHT", 0, -27) panel:SetHeight(1) panel:SetFrameStrata("DIALOG") @@ -290,6 +299,300 @@ local function UpdateNameHighlight(hasNameErrors) end end +-- ============================================================================ +-- Command Whitelist GUI +-- ============================================================================ + +local function RefreshWhitelistDisplay() + if not whitelistPopup then return end + + local commands = CleveRoids.GetWhitelistedCommandsList() + local count = table.getn(commands) + + -- Update rows + for i = 1, WHITELIST_ROW_POOL_SIZE do + local row = whitelistRows[i] + if row then + if i <= count then + row.label:SetText(commands[i]) + row.frame:Show() + else + row.label:SetText("") + row.frame:Hide() + end + end + end + + -- Show overflow indicator + local overflowText = whitelistPopup.overflowText + if count > WHITELIST_ROW_POOL_SIZE then + overflowText:SetText("|cff888888... and " .. (count - WHITELIST_ROW_POOL_SIZE) .. " more|r") + overflowText:Show() + else + overflowText:SetText("") + overflowText:Hide() + end + + -- Dynamic height: title(20) + gap(8) + editbox(18) + rows(18 each) + padding + local rowsShown = count + if rowsShown > WHITELIST_ROW_POOL_SIZE then rowsShown = WHITELIST_ROW_POOL_SIZE end + local height = 8 + 20 + 8 + 18 + 2 + (rowsShown * 18) + if count > WHITELIST_ROW_POOL_SIZE then + height = height + 14 + end + height = height + 8 -- bottom padding + whitelistPopup:SetHeight(height) + + -- Re-trigger error checking so results update in real time + RequestValidation() +end + +local function CreateWhitelistPopup() + if whitelistPopup then return end + + local popup = CreateFrame("Frame", "CleveRoidsWhitelistPopup", UIParent) + popup:SetWidth(220) + popup:SetHeight(120) + popup:SetPoint("CENTER", UIParent, "CENTER", 0, 100) + popup:SetFrameStrata("FULLSCREEN_DIALOG") + popup:SetMovable(true) + popup:EnableMouse(true) + + -- pfUI pixel-perfect flat style + popup:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + popup:SetBackdropColor(0.08, 0.08, 0.1, 0.95) + popup:SetBackdropBorderColor(0.2, 0.2, 0.2, 1) + popup:Hide() + + -- Drag handling + popup:RegisterForDrag("LeftButton") + popup:SetScript("OnDragStart", function() + this:StartMoving() + end) + popup:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + end) + + -- Title + local title = popup:CreateFontString(nil, "OVERLAY") + title:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE") + title:SetPoint("TOPLEFT", popup, "TOPLEFT", 8, -8) + title:SetText("Whitelisted Commands") + title:SetTextColor(0.9, 0.8, 0.5, 1) + + -- Close button (flat X) + local closeBtn = CreateFrame("Button", nil, popup) + closeBtn:SetWidth(16) + closeBtn:SetHeight(16) + closeBtn:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -4, -4) + local closeLbl = closeBtn:CreateFontString(nil, "OVERLAY") + closeLbl:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE") + closeLbl:SetPoint("CENTER", 0, 0) + closeLbl:SetText("x") + closeLbl:SetTextColor(0.6, 0.6, 0.6, 1) + closeBtn:SetScript("OnEnter", function() + closeLbl:SetTextColor(1, 0.3, 0.3, 1) + end) + closeBtn:SetScript("OnLeave", function() + closeLbl:SetTextColor(0.6, 0.6, 0.6, 1) + end) + closeBtn:SetScript("OnClick", function() + popup:Hide() + end) + + -- EditBox (flat style) + local editBox = CreateFrame("EditBox", "CleveRoidsWhitelistEditBox", popup) + editBox:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8) + editBox:SetWidth(148) + editBox:SetHeight(18) + editBox:SetAutoFocus(false) + editBox:SetMaxLetters(60) + editBox:SetFont("Fonts\\FRIZQT__.TTF", 10, "") + editBox:SetTextColor(1, 1, 1, 1) + editBox:SetTextInsets(4, 4, 0, 0) + editBox:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + editBox:SetBackdropColor(0.05, 0.05, 0.07, 0.9) + editBox:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + + -- Add button (flat style) + local addBtn = CreateFrame("Button", nil, popup) + addBtn:SetPoint("LEFT", editBox, "RIGHT", 4, 0) + addBtn:SetWidth(40) + addBtn:SetHeight(18) + addBtn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + addBtn:SetBackdropColor(0.15, 0.15, 0.15, 0.9) + addBtn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + local addLbl = addBtn:CreateFontString(nil, "OVERLAY") + addLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + addLbl:SetPoint("CENTER", 0, 0) + addLbl:SetText("Add") + addLbl:SetTextColor(0.8, 0.8, 0.8, 1) + addBtn:SetScript("OnEnter", function() + this:SetBackdropColor(0.25, 0.25, 0.25, 0.95) + this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1) + addLbl:SetTextColor(1, 1, 1, 1) + end) + addBtn:SetScript("OnLeave", function() + this:SetBackdropColor(0.15, 0.15, 0.15, 0.9) + this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + addLbl:SetTextColor(0.8, 0.8, 0.8, 1) + end) + + local function AddCommand() + local text = editBox:GetText() + if not text or CleveRoids.Trim(text) == "" then return end + text = CleveRoids.Trim(text) + CleveRoids.AddWhitelistedCommand(text) + editBox:SetText("") + editBox:ClearFocus() + RefreshWhitelistDisplay() + end + + addBtn:SetScript("OnClick", AddCommand) + editBox:SetScript("OnEnterPressed", function() + AddCommand() + end) + editBox:SetScript("OnEscapePressed", function() + this:ClearFocus() + end) + + -- Anchor for rows: below the editbox row + local rowAnchor = editBox + + -- Pre-allocate row pool + for i = 1, WHITELIST_ROW_POOL_SIZE do + local rowFrame = CreateFrame("Frame", nil, popup) + rowFrame:SetHeight(16) + rowFrame:SetPoint("TOPLEFT", rowAnchor, "BOTTOMLEFT", 0, -2) + rowFrame:SetPoint("RIGHT", popup, "RIGHT", -8, 0) + rowFrame:Hide() + + local label = rowFrame:CreateFontString(nil, "OVERLAY") + label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + label:SetPoint("LEFT", rowFrame, "LEFT", 2, 0) + label:SetJustifyH("LEFT") + label:SetTextColor(0.7, 0.7, 0.7, 1) + + -- Flat X remove button + local removeBtn = CreateFrame("Button", nil, rowFrame) + removeBtn:SetWidth(14) + removeBtn:SetHeight(14) + removeBtn:SetPoint("RIGHT", rowFrame, "RIGHT", -1, 0) + local removeLbl = removeBtn:CreateFontString(nil, "OVERLAY") + removeLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + removeLbl:SetPoint("CENTER", 0, 0) + removeLbl:SetText("x") + removeLbl:SetTextColor(0.5, 0.5, 0.5, 1) + removeBtn.removeLbl = removeLbl + removeBtn.rowIndex = i + + removeBtn:SetScript("OnEnter", function() + this.removeLbl:SetTextColor(1, 0.3, 0.3, 1) + end) + removeBtn:SetScript("OnLeave", function() + this.removeLbl:SetTextColor(0.5, 0.5, 0.5, 1) + end) + removeBtn:SetScript("OnClick", function() + local idx = this.rowIndex + local row = whitelistRows[idx] + if row and row.label then + local cmd = row.label:GetText() + if cmd and cmd ~= "" then + CleveRoids.RemoveWhitelistedCommand(cmd) + RefreshWhitelistDisplay() + end + end + end) + + whitelistRows[i] = { frame = rowFrame, label = label, removeBtn = removeBtn } + rowAnchor = rowFrame + end + + -- Overflow text + local overflowText = popup:CreateFontString(nil, "OVERLAY") + overflowText:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") + overflowText:SetPoint("TOPLEFT", whitelistRows[WHITELIST_ROW_POOL_SIZE].frame, "BOTTOMLEFT", 2, -2) + overflowText:SetJustifyH("LEFT") + overflowText:SetTextColor(0.5, 0.5, 0.5, 1) + overflowText:Hide() + popup.overflowText = overflowText + + whitelistPopup = popup +end + +local function CreateWhitelistButton() + if whitelistButton then return end + if not MacroFrame then return end + + local btn = CreateFrame("Button", "CleveRoidsWhitelistButton", MacroFrame) + btn:SetWidth(80) + btn:SetHeight(18) + btn:SetPoint("BOTTOMRIGHT", MacroFrame, "BOTTOMRIGHT", 8, 52) + btn:SetFrameStrata("DIALOG") + + -- pfUI pixel-perfect flat style + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 0, right = 0, top = 0, bottom = 0 }, + }) + btn:SetBackdropColor(0.1, 0.1, 0.1, 0.85) + btn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1) + + local label = btn:CreateFontString(nil, "OVERLAY") + label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE") + label:SetPoint("CENTER", btn, "CENTER", 0, 0) + label:SetText("Whitelist") + label:SetTextColor(0.8, 0.8, 0.8, 1) + btn.label = label + + -- Hover highlight + btn:SetScript("OnEnter", function() + this:SetBackdropColor(0.2, 0.2, 0.2, 0.95) + this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1) + this.label:SetTextColor(1, 1, 1, 1) + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText("Command Whitelist") + GameTooltip:AddLine("Add third-party addon commands so the", 1, 1, 1, true) + GameTooltip:AddLine("error checker stops flagging them.", 1, 1, 1, true) + GameTooltip:Show() + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(0.1, 0.1, 0.1, 0.85) + this:SetBackdropBorderColor(0.3, 0.3, 0.3, 1) + this.label:SetTextColor(0.8, 0.8, 0.8, 1) + GameTooltip:Hide() + end) + + btn:SetScript("OnClick", function() + CreateWhitelistPopup() + if whitelistPopup:IsVisible() then + whitelistPopup:Hide() + else + RefreshWhitelistDisplay() + whitelistPopup:Show() + end + end) + + whitelistButton = btn +end + -- ============================================================================ -- Validation & Debounce -- ============================================================================ @@ -426,7 +729,7 @@ local function RunValidation() UpdateNameHighlight(hasNameErrors) end -local function RequestValidation() +RequestValidation = function() lastKeystroke = GetTime() pendingValidation = true end @@ -607,6 +910,10 @@ local function ClearAll() if nameHighlight then nameHighlight:Hide() end + + if whitelistPopup then + whitelistPopup:Hide() + end end -- ============================================================================ @@ -635,8 +942,9 @@ local function InstallHooks() if origOnShow then origOnShow() end - -- Create panel lazily on first show + -- Create UI lazily on first show CreateErrorPanel() + CreateWhitelistButton() -- Reset state and validate lastSelectedMacro = MacroFrame.selectedMacro RunValidation() diff --git a/Init.lua b/Init.lua index f6b2274..93e0bb1 100644 --- a/Init.lua +++ b/Init.lua @@ -123,6 +123,8 @@ CleveRoids.dynamicCmds = { ["/equip12"] = true, ["/equip13"] = true, ["/equip14"] = true, + ["/applymain"] = true, + ["/applyoff"] = true, } -- Equipment swap queue system diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index d37a146..acf5db9 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -422,6 +422,59 @@ local VALID_COMMANDS = { ["/yes"] = true, } +-- ============================================================================ +-- Command Whitelist API (for third-party addon commands) +-- ============================================================================ + +local function GetWhitelistedCommands() + if not CleveRoidMacros then CleveRoidMacros = {} end + if not CleveRoidMacros.whitelistedCommands then + CleveRoidMacros.whitelistedCommands = {} + end + return CleveRoidMacros.whitelistedCommands +end + +function CleveRoids.AddWhitelistedCommand(cmd) + if not cmd or cmd == "" then return end + cmd = string.lower(cmd) + -- Auto-prepend "/" if missing + if string.sub(cmd, 1, 1) ~= "/" then + cmd = "/" .. cmd + end + local whitelist = GetWhitelistedCommands() + whitelist[cmd] = true +end + +function CleveRoids.RemoveWhitelistedCommand(cmd) + if not cmd or cmd == "" then return end + cmd = string.lower(cmd) + if string.sub(cmd, 1, 1) ~= "/" then + cmd = "/" .. cmd + end + local whitelist = GetWhitelistedCommands() + whitelist[cmd] = nil +end + +function CleveRoids.IsWhitelistedCommand(cmd) + if not cmd or cmd == "" then return false end + cmd = string.lower(cmd) + if string.sub(cmd, 1, 1) ~= "/" then + cmd = "/" .. cmd + end + local whitelist = GetWhitelistedCommands() + return whitelist[cmd] == true +end + +function CleveRoids.GetWhitelistedCommandsList() + local whitelist = GetWhitelistedCommands() + local list = {} + for cmd, _ in pairs(whitelist) do + table.insert(list, cmd) + end + table.sort(list) + return list +end + -- Commands that can have conditionals without actions -- e.g., /petattack [harm] or /target [exists,hp:<=20] local COMMANDS_NO_ACTION_NEEDED = { @@ -679,7 +732,7 @@ local function validateLine(line, lineNum) local _, _, cmd = safeStringFind(line, "^(/[a-z]+%d*)") if cmd then local lowerCmd = string.lower(cmd) - if not VALID_COMMANDS[lowerCmd] then + if not VALID_COMMANDS[lowerCmd] and not CleveRoids.IsWhitelistedCommand(lowerCmd) then table.insert(localErrors, { type = ERROR_TYPES.INVALID_COMMAND, line = lineNum, diff --git a/NampowerAPI.lua b/NampowerAPI.lua index e28d2f3..90d24d4 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -91,7 +91,13 @@ Pass ignoreMissing=1 to skip aura-slot presence check (for overflow buffs in server slots 33-48 that have no client aura slot) - Current version: v2.34.0 + GUID String Fix & Talent Helper (v2.35+): + - GetUnitData/GetUnitField now return GUID-type fields (charm, summon, charmedBy, + summonedBy, createdBy, target, persuaded, channelObject) as hex strings + (e.g., "0x0600000012345678") instead of raw numbers to avoid Lua 64-bit precision loss + - LearnTalentRank(talentPage, talentIndex, rank) - Learn a specific talent rank directly + + Current version: v2.35.0 ]] local _G = _G or getfenv(0) @@ -252,6 +258,10 @@ API.VERSION_REQUIREMENTS = { -- v2.34+ - Aura cancel functions ["CancelPlayerAuraSlot"] = { 2, 34, 0, "CancelPlayerAuraSlot" }, ["CancelPlayerAuraSpellId"] = { 2, 34, 0, "CancelPlayerAuraSpellId" }, + + -- v2.35+ - GUID string fix and talent helper + ["GuidStringFormat"] = { 2, 35, 0 }, -- GetUnitData/GetUnitField return GUID fields as hex strings + ["LearnTalentRank"] = { 2, 35, 0, "LearnTalentRank" }, } -- Check if a specific feature is available @@ -396,6 +406,10 @@ local function InitializeFeatures() f.hasCancelPlayerAuraSlot = API.HasFeature("CancelPlayerAuraSlot") f.hasCancelPlayerAuraSpellId = API.HasFeature("CancelPlayerAuraSpellId") + -- v2.35+ GUID string format and talent helper + f.hasGuidStringFormat = API.HasFeature("GuidStringFormat") + f.hasLearnTalentRank = API.HasFeature("LearnTalentRank") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() @@ -3021,5 +3035,25 @@ function API.CancelPlayerAuraSpellId(spellId, ignoreMissing) return true end +-------------------------------------------------------------------------------- +-- TALENT HELPER (v2.35+) +-------------------------------------------------------------------------------- + +-- Learn a specific talent rank directly (v2.35+) +-- talentPage: 1-3 (talent tab) +-- talentIndex: 1-32 (talent position within tab) +-- rank: 1-5 (rank to learn) +function API.LearnTalentRank(talentPage, talentIndex, rank) + if not API.features.hasLearnTalentRank or not _G.LearnTalentRank then + return false + end + if not talentPage or not talentIndex or not rank then return false end + if talentPage < 1 or talentPage > 3 then return false end + if talentIndex < 1 or talentIndex > 32 then return false end + if rank < 1 or rank > 5 then return false end + _G.LearnTalentRank(talentPage, talentIndex, rank) + return true +end + -- Expose API globally for other addons _G.CleveRoidsNampowerAPI = API From 8b1aaf1f8737f3bf4287d55153db34d3dbf0cf7a Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sat, 14 Feb 2026 21:29:15 -0500 Subject: [PATCH 03/12] fix moving condition with no monkeyspeed available --- Conditionals.lua | 4 ++-- Core.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index 7d72baa..fb9a868 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -2590,8 +2590,8 @@ function CleveRoids.IsPlayerMoving() -- prevIdx and currIdx walk from oldest to newest local currOffset = samplesAvail - i -- e.g., 3,2,1,0 for count=4 local prevOffset = samplesAvail - i + 1 -- e.g., 4,3,2,1 - local currIdx = math.mod((headIdx - currOffset - 1), 4) + 1 - local prevIdx = math.mod((headIdx - prevOffset - 1), 4) + 1 + local currIdx = math.mod((headIdx - currOffset - 1 + 4), 4) + 1 + local prevIdx = math.mod((headIdx - prevOffset - 1 + 4), 4) + 1 local prev = history[prevIdx] local curr = history[currIdx] local dx = curr.x - prev.x diff --git a/Core.lua b/Core.lua index 34b4118..7659e1d 100644 --- a/Core.lua +++ b/Core.lua @@ -3862,7 +3862,7 @@ function CleveRoids.OnUpdate(self) -- Maintain legacy vars for any code that uses them directly if CleveRoids._posHistoryCount >= 2 then - local prevIdx = math.mod((idx - 2), 4) + 1 + local prevIdx = math.mod((idx - 2 + 4), 4) + 1 CleveRoids._previousPlayerPos = history[prevIdx] CleveRoids._currentPlayerPos = entry end From e3cfde05060a8a6837b5106d33d4ab574eb4b506 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sun, 15 Feb 2026 10:09:34 -0500 Subject: [PATCH 04/12] add full disable command to macrochecker and temporarily disable swimming conditional ddue to lack of dll support --- Conditionals.lua | 21 +++++++++++---------- Core.lua | 23 +++++++++++++++++++++++ Extensions/MacroErrorUI.lua | 3 +++ MacroErrorChecker.lua | 9 ++++++++- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index fb9a868..5ad84f2 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -6413,15 +6413,16 @@ CleveRoids.Keywords = { 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, + -- TODO: swimming/noswimming disabled - IsSpellUsable doesn't check environmental + -- requirements (swimming state is server-side only). Revisit when DLL mods expose + -- movement flags or IsSwimming() API. + -- swimming = function(conditionals) + -- return CleveRoids.IsReactiveUsable("Aquatic Form") + -- end, + -- + -- noswimming = function(conditionals) + -- return not CleveRoids.IsReactiveUsable("Aquatic Form") + -- end, distance = function(conditionals) if not CleveRoids.hasUnitXP then return false end @@ -7705,7 +7706,7 @@ CleveRoids.STATIC_CONDITIONALS = { form = true, noform = true, stance = true, nostance = true, equipped = true, noequipped = true, mod = true, nomod = true, - swimming = true, noswimming = true, + -- swimming = true, noswimming = true, -- disabled: no client-side swimming detection resting = true, noresting = true, } diff --git a/Core.lua b/Core.lua index 7659e1d..ed52c73 100644 --- a/Core.lua +++ b/Core.lua @@ -490,6 +490,10 @@ frame:SetScript("OnEvent", function() if type(CleveRoidMacros.refresh) ~= "number" then CleveRoidMacros.refresh = 5 end + + if type(CleveRoidMacros.macrocheck) ~= "number" then + CleveRoidMacros.macrocheck = 1 -- enabled by default + end end) -- Queues a full update of all action bars. @@ -5807,11 +5811,13 @@ SlashCmdList["CLEVEROID"] = function(msg) CleveRoids.Print("Current Settings:") DEFAULT_CHAT_FRAME:AddMessage("realtime (force fast updates, CPU intensive) = " .. CleveRoidMacros.realtime .. " (Default: 0)") DEFAULT_CHAT_FRAME:AddMessage("refresh (updates per second) = " .. CleveRoidMacros.refresh .. " (Default: 5)") + DEFAULT_CHAT_FRAME:AddMessage("macrocheck (syntax checker) = " .. CleveRoidMacros.macrocheck .. " (Default: 1)") DEFAULT_CHAT_FRAME:AddMessage("debug (show learning messages) = " .. (CleveRoids.debug and "1" or "0") .. " (Default: 0)") DEFAULT_CHAT_FRAME:AddMessage(" ") CleveRoids.Print("Available Commands:") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1-10 updates/sec)") + DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker") if CleveRoids.hasSuperwow then DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn - Manually set spell duration") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget - Forget learned duration(s)") @@ -5878,6 +5884,23 @@ SlashCmdList["CLEVEROID"] = function(msg) return end + -- macrocheck + if cmd == "macrocheck" then + local num = tonumber(val) + if num == 0 or num == 1 then + CleveRoidMacros.macrocheck = num + if num == 1 then + CleveRoids.Print("Macro syntax checker |cff00ff00enabled|r") + else + CleveRoids.Print("Macro syntax checker |cffff0000disabled|r (reload UI to take effect)") + end + else + CleveRoids.Print("Usage: /cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker (Default: 1)") + CleveRoids.Print("Current macrocheck = " .. tostring(CleveRoidMacros.macrocheck)) + end + return + end + -- learn (manual set duration) if cmd == "learn" then if not CleveRoids.hasSuperwow then diff --git a/Extensions/MacroErrorUI.lua b/Extensions/MacroErrorUI.lua index 8418f58..29b28ea 100644 --- a/Extensions/MacroErrorUI.lua +++ b/Extensions/MacroErrorUI.lua @@ -985,6 +985,9 @@ function Extension.OnAddonLoaded() end function Extension.OnLoad() + -- Skip if macro checker is disabled + if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then return end + -- Skip if SuperMacro is loaded (detected at load time) if SuperMacroFrame ~= nil then return end diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index acf5db9..fbcf78e 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -1155,6 +1155,11 @@ end -- Slash command SLASH_MACROCHECK1 = "/macrocheck" SlashCmdList.MACROCHECK = function(msg) + if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then + CleveRoids.Print("Macro syntax checker is disabled. Enable with /cleveroid macrocheck 1") + return + end + local success, result = pcall(function() msg = safeTrim(msg or "") @@ -1170,4 +1175,6 @@ SlashCmdList.MACROCHECK = function(msg) end end -CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all") +if not CleveRoidMacros or CleveRoidMacros.macrocheck ~= 0 then + CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all") +end From d5fefc3198571cb2a5ae5903c4f0093e1cbd5b70 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sun, 15 Feb 2026 14:38:58 -0500 Subject: [PATCH 05/12] use nampower for default moving conditional when available. add rooted to mycc/nomycc/cc/nocc. add swimming/noswimming support with nampower 2_36 --- Conditionals.lua | 43 +++++++++++++++++++++++++++++++------------ NampowerAPI.lua | 17 ++++++++++++++++- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index 5ad84f2..bf8ec57 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -2569,11 +2569,16 @@ end --- Falls back to basic position checking if MonkeySpeed isn't available --- @return boolean True if player is moving function CleveRoids.IsPlayerMoving() - -- If MonkeySpeed is available, use it for accurate detection + -- If MonkeySpeed is available, use it for accurate speed-based detection if CleveRoids.HasMonkeySpeed() then return (MonkeySpeed.m_fSpeed or 0) > 0 end + -- Nampower 2.36+ provides direct movement flag query (catches jumping, falling, pitching) + if CleveRoids.NampowerAPI.features.hasPlayerIsMoving then + return PlayerIsMoving() == 1 + end + -- Fallback: use continuously tracked position history from OnUpdate -- Core.lua tracks position at ~100 Hz with 4-sample circular buffer (matches MonkeySpeed's 0.01s). -- Movement is detected if ANY consecutive pair of samples shows movement. @@ -4541,6 +4546,7 @@ CleveRoids.CCTypesLossOfControl = { [1] = true, -- charm [2] = true, -- disoriented [5] = true, -- fear + [7] = true, -- root (Entangling Roots, Frost Nova, etc.) [10] = true, -- sleep [12] = true, -- stun (Cheap Shot, Kidney Shot, etc.) [13] = true, -- freeze @@ -6413,16 +6419,29 @@ CleveRoids.Keywords = { end, conditionals, "nopet") end, - -- TODO: swimming/noswimming disabled - IsSpellUsable doesn't check environmental - -- requirements (swimming state is server-side only). Revisit when DLL mods expose - -- movement flags or IsSwimming() API. - -- swimming = function(conditionals) - -- return CleveRoids.IsReactiveUsable("Aquatic Form") - -- end, - -- - -- noswimming = function(conditionals) - -- return not CleveRoids.IsReactiveUsable("Aquatic Form") - -- end, + -- [swimming] - Player is currently swimming (Nampower v2.36+) + swimming = function(conditionals) + if not CleveRoids.NampowerAPI.features.hasPlayerIsSwimming then + if not CleveRoids._swimmingErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swimming] conditional requires Nampower v2.36.0 or newer.", 1, 0.5, 0.5) + CleveRoids._swimmingErrorShown = true + end + return false + end + return PlayerIsSwimming() == 1 + end, + + -- [noswimming] - Player is NOT swimming (Nampower v2.36+) + noswimming = function(conditionals) + if not CleveRoids.NampowerAPI.features.hasPlayerIsSwimming then + if not CleveRoids._swimmingErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swimming] conditional requires Nampower v2.36.0 or newer.", 1, 0.5, 0.5) + CleveRoids._swimmingErrorShown = true + end + return false + end + return PlayerIsSwimming() ~= 1 + end, distance = function(conditionals) if not CleveRoids.hasUnitXP then return false end @@ -7706,7 +7725,7 @@ CleveRoids.STATIC_CONDITIONALS = { form = true, noform = true, stance = true, nostance = true, equipped = true, noequipped = true, mod = true, nomod = true, - -- swimming = true, noswimming = true, -- disabled: no client-side swimming detection + swimming = true, noswimming = true, resting = true, noresting = true, } diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 90d24d4..d993693 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -97,7 +97,12 @@ (e.g., "0x0600000012345678") instead of raw numbers to avoid Lua 64-bit precision loss - LearnTalentRank(talentPage, talentIndex, rank) - Learn a specific talent rank directly - Current version: v2.35.0 + Player State Functions (v2.36+): + - PlayerIsMoving() - Returns 1 if player is moving, nil if stationary + - PlayerIsRooted() - Returns 1 if player is rooted, nil if not + - PlayerIsSwimming() - Returns 1 if player is swimming, nil if not + + Current version: v2.36.0 ]] local _G = _G or getfenv(0) @@ -262,6 +267,11 @@ API.VERSION_REQUIREMENTS = { -- v2.35+ - GUID string fix and talent helper ["GuidStringFormat"] = { 2, 35, 0 }, -- GetUnitData/GetUnitField return GUID fields as hex strings ["LearnTalentRank"] = { 2, 35, 0, "LearnTalentRank" }, + + -- v2.36+ - Player state functions + ["PlayerIsMoving"] = { 2, 36, 0, "PlayerIsMoving" }, + ["PlayerIsRooted"] = { 2, 36, 0, "PlayerIsRooted" }, + ["PlayerIsSwimming"] = { 2, 36, 0, "PlayerIsSwimming" }, } -- Check if a specific feature is available @@ -410,6 +420,11 @@ local function InitializeFeatures() f.hasGuidStringFormat = API.HasFeature("GuidStringFormat") f.hasLearnTalentRank = API.HasFeature("LearnTalentRank") + -- v2.36+ Player state functions + f.hasPlayerIsMoving = API.HasFeature("PlayerIsMoving") + f.hasPlayerIsRooted = API.HasFeature("PlayerIsRooted") + f.hasPlayerIsSwimming = API.HasFeature("PlayerIsSwimming") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() From 860fca00019f6a184ef8514bc2fa5686951d912a Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Mon, 16 Feb 2026 15:26:55 -0500 Subject: [PATCH 06/12] target can use multiscan/fix overflowframe not showing more than 1 buff --- Core.lua | 17 +++++++++++++++++ Extensions/OverflowBuffFrame.lua | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Core.lua b/Core.lua index ed52c73..6318b8b 100644 --- a/Core.lua +++ b/Core.lua @@ -2585,6 +2585,23 @@ function CleveRoids.DoTarget(msg) -- Save original target GUID for potential restoration (SuperWoW returns GUID as 2nd value) local _, originalTargetGuid = UnitExists("target") + -- Handle [multiscan:priority] - use ResolveMultiscanTarget for enemy scanning + -- ResolveMultiscanTarget handles its own target save/restore internally + if conditionals.multiscan then + local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, conditionals.target) + if scanResult then + TargetUnit(scanResult) + return true + end + -- No match found - restore original target if scanning changed it + if originalTargetGuid and UnitExists(originalTargetGuid) then + TargetUnit(originalTargetGuid) + elseif not originalTargetGuid then + ClearTarget() + end + return false + end + -- Track if an explicit target was specified via @unit syntax local explicitTarget = conditionals.target diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua index c0d1e50..cd0418b 100644 --- a/Extensions/OverflowBuffFrame.lua +++ b/Extensions/OverflowBuffFrame.lua @@ -36,6 +36,7 @@ local targetLabelFs = nil local updateFrame = nil local updateElapsed = 0 local testMode = false +local dirty = false -- Flag for deferred rebuild (avoids event ordering race) local lastPlayerCount = 0 local lastTargetCount = 0 @@ -551,6 +552,14 @@ CleveRoids.ToggleOverflowTest = ToggleTestMode -- ============================================================================ local function OnUpdate() updateElapsed = updateElapsed + arg1 + -- Check dirty flag every frame for responsive rebuilds after AURA_CAST events. + -- By the time OnUpdate fires, the core handler has already updated OverflowBuffs. + if dirty then + dirty = false + updateElapsed = 0 + RebuildFrame() + return + end if updateElapsed < UPDATE_INTERVAL then return end updateElapsed = 0 RefreshDurations() @@ -570,7 +579,11 @@ ext.OnTargetChanged = function() end ext.OnAuraCast = function() - RebuildFrame() + -- Defer rebuild to OnUpdate: the core handler that populates OverflowBuffs + -- registers for AURA_CAST_ON_SELF later (PLAYER_ENTERING_WORLD) than this + -- extension (ADDON_LOADED), so it fires AFTER us. A direct RebuildFrame() + -- here would read stale data. + dirty = true end ext.OnGroupChanged = function() From f5e3b862be0fc98972799c5765e34c40124738a0 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Mon, 16 Feb 2026 15:57:41 -0500 Subject: [PATCH 07/12] fix ! prefix with explicit conditionals and Judgement of Light debuff detection --- Conditionals.lua | 35 ++++++++++++++++++++++++++++++++--- Core.lua | 40 ++++++++++++++++++++++++++++------------ Init.lua | 1 + Utility.lua | 16 ++++++++++++++-- 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index bf8ec57..e1ca333 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -149,6 +149,33 @@ function CleveRoids.InvalidateSpellNameCache() _spellNameToIDsBuilt = false end +-- Check if a debuff is shared by spell ID or by name (for custom/Turtle WoW IDs). +-- Falls back to checking if the debuff NAME matches any known shared debuff name +-- in the spell name cache, even if the specific spell ID is unregistered. +local function IsSharedDebuffByIdOrName(lib, spellID, debuffName) + if not lib then return false end + -- Direct ID check (fast path) + if lib.IsPersonalDebuff and lib:IsPersonalDebuff(spellID) == false then + return true + end + -- Name-based fallback: if ANY spell ID for this name is in sharedDebuffs, treat as shared. + -- This handles custom server spell IDs (e.g., Turtle WoW Judgement variants) that have + -- the same debuff name but different IDs from the hardcoded vanilla entries. + if debuffName then + local knownIDs = GetSpellIDsForName(debuffName) + if knownIDs then + for _, kid in ipairs(knownIDs) do + if lib.sharedDebuffs and lib.sharedDebuffs[kid] then + -- Also register this new spell ID so future checks are fast + lib.sharedDebuffs[spellID] = lib.sharedDebuffs[kid] + return true + end + end + end + end + return false +end + -- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call) -- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache() -- Enhanced with Nampower v2.18+ GetEquippedItems when available @@ -3705,7 +3732,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if gufResult ~= nil then -- GetUnitField was available and searched if gufResult then - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(gufSpellId) == false + local isShared = IsSharedDebuffByIdOrName(lib, gufSpellId, args.name) if isShared then found = true stacks = gufStacks or 0 @@ -3739,7 +3766,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if matched then -- IMPORTANT: Only use fallback for SHARED debuffs -- Personal debuffs must come from tracking table (caster check) - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(debuffSpellID) == false + -- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements) + local isShared = IsSharedDebuffByIdOrName(lib, debuffSpellID, args.name) if isShared then found = true texture = tex @@ -3769,7 +3797,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args) end if matched then -- IMPORTANT: Only use fallback for SHARED debuffs - local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false + -- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements) + local isShared = IsSharedDebuffByIdOrName(lib, buffSpellID, args.name) if isShared then found = true texture = tex diff --git a/Core.lua b/Core.lua index 6318b8b..d532238 100644 --- a/Core.lua +++ b/Core.lua @@ -1881,17 +1881,11 @@ function CleveRoids.ParseMsg(msg) -- With a condition block present, build out the conditionals table - -- optional spam/cancel flags (apply only when we actually have a [] block) + -- ! prefix with explicit conditionals: store as a flag for execution-time anti-toggle, + -- rather than injecting an implicit conditional that gets AND'd with user conditions. + -- The anti-toggle check happens in DoWithConditionals before CastSpellByName. if noSpam and noSpam ~= "" then - local spamCond = CleveRoids.GetSpammableConditional(action) - if spamCond then - conditionals[spamCond] = { action } - -- Also create _groups entry so Multi() finds it when combined with explicit conditionals - if not conditionals._groups then - conditionals._groups = {} - end - conditionals._groups[spamCond] = { { values = { action }, operator = "OR" } } - end + conditionals.noSpam = true end if cancelAura and cancelAura ~= "" then conditionals.cancelaura = action @@ -2481,8 +2475,30 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo end end if action == CastSpellByName then - -- Special case: !Attack should use AttackTarget() which doesn't toggle - -- This is more reliable than CastSpellByName("Attack") which toggles on/off + -- ! prefix anti-toggle: prevent CastSpellByName from toggling off active spells. + -- For Attack/AutoShot/Shoot, use the non-toggling AttackTarget() API. + -- For self-buffs/shapeshifts, skip if already active on the player. + if conditionals.noSpam then + local spamType = CleveRoids.spamConditions[msg] + if spamType == "checkchanneled" then + -- Attack/AutoShot/Shoot: use non-toggling API + if msg == CleveRoids.Localized.Attack then + AttackTarget() + if needRetarget then TargetLastTarget() end + conditionals.target = origTarget + return true + end + -- AutoShot/Shoot: checkchanneled prevents toggle (already handled below) + else + -- Self-buff/shapeshift: skip if already active on player + if CleveRoids.ValidatePlayerBuff(msg) then + if needRetarget then TargetLastTarget() end + conditionals.target = origTarget + return false + end + end + end + -- Legacy path: !Attack without explicit conditionals (checkchanneled injected) if msg == CleveRoids.Localized.Attack and conditionals.checkchanneled then AttackTarget() elseif CleveRoids.hasSuperwow and conditionals.target then diff --git a/Init.lua b/Init.lua index 93e0bb1..61e41e4 100644 --- a/Init.lua +++ b/Init.lua @@ -163,6 +163,7 @@ CleveRoids.ignoreKeywords = { action = true, ignoretooltip = true, cancelaura = true, + noSpam = true, -- ! prefix flag: prevent toggle-off at execution time _operators = true, -- Metadata for AND/OR operator tracking _groups = true, -- Grouped conditional values for AND/OR evaluation multiscan = true, -- Processed before Keywords loop (target resolution) diff --git a/Utility.lua b/Utility.lua index ef841ea..fc28e8e 100644 --- a/Utility.lua +++ b/Utility.lua @@ -2996,10 +2996,16 @@ delayedTrackingFrame:SetScript("OnUpdate", function() if not lib.detectedJudgementDebuffIDs[debuffSpellID] then lib.detectedJudgementDebuffIDs[debuffSpellID] = true lib.judgementSpells[debuffSpellID] = true -- Add to refresh list + -- Also register as shared debuff so [debuff]/[nodebuff] fallback detection works + -- for custom Turtle WoW spell IDs not in the hardcoded sharedDebuffs table + if not lib.sharedDebuffs[debuffSpellID] then + lib.sharedDebuffs[debuffSpellID] = 10 -- Standard judgement duration + CleveRoids.InvalidateSpellNameCache() + end if debug then DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh list", + _string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh + shared list", debuffName, debuffSpellID, pending.castSpellID) ) end @@ -4043,7 +4049,13 @@ ev:SetScript("OnEvent", function() }) -- If this is a Judgement spell cast by a Paladin, schedule a scan to find the actual debuff ID - if CleveRoids.playerClass == "PALADIN" and lib.judgementSpells[spellID] then + -- Check by ID (known judgement debuffs) OR by name (Turtle WoW custom cast spell IDs) + local isJudgementCast = lib.judgementSpells[spellID] + if not isJudgementCast and CleveRoids.playerClass == "PALADIN" then + local castName = _SpellInfo(spellID) + isJudgementCast = castName and _string_find(castName, "^Judgement") + end + if CleveRoids.playerClass == "PALADIN" and isJudgementCast then table.insert(lib.pendingJudgements, { timestamp = GetTime(), castSpellID = spellID, From 37afbd047c8191117a3c7cbe7cda42d65b9e25e4 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Mon, 16 Feb 2026 21:32:37 -0500 Subject: [PATCH 08/12] update pfui compatibility --- Compatibility/pfUI.lua | 4 ++-- Core.lua | 14 +++++++------- Init.lua | 2 +- Utility.lua | 14 ++++++-------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Compatibility/pfUI.lua b/Compatibility/pfUI.lua index 981720f..91d276c 100644 --- a/Compatibility/pfUI.lua +++ b/Compatibility/pfUI.lua @@ -124,7 +124,7 @@ function Extension.HookPfUILibdebuff() -- Check if pfUI 7.6+ with GetUnitField-based libdebuff is active -- If so, pfUI handles combo durations and Carnage internally - we only override on mismatch - local hasPfUI76 = CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil) + local hasPfUI76 = CleveRoids.hasPfUI76 -- pfUI 7.6+ handles all durations internally - no hooks needed if hasPfUI76 then @@ -351,7 +351,7 @@ function Extension.SyncComboDurationToPfUI(guid, spellID, duration) end -- pfUI 7.6+ handles all durations internally - if CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil) then + if CleveRoids.hasPfUI76 then return end diff --git a/Core.lua b/Core.lua index d532238..d023924 100644 --- a/Core.lua +++ b/Core.lua @@ -809,10 +809,10 @@ function CleveRoids.TestForActiveAction(actions) if action.sequence then newSequence = action.sequence newActiveAction = CleveRoids.GetCurrentSequenceAction(newSequence) - -- Check if current step is null/pass/noop - if so, skip this sequence for icon + -- Check if current step is a stop step - if so, skip this sequence for icon if newActiveAction and newActiveAction.action then local stepLower = string.lower(CleveRoids.Trim(newActiveAction.action)) - if stepLower == "null" or stepLower == "pass" or stepLower == "noop" then + if stepLower == "nil" or stepLower == "null" or stepLower == "pass" or stepLower == "noop" then hasActive = false newActiveAction = nil newSequence = nil @@ -3693,15 +3693,15 @@ function CleveRoids.DoCastSequence(sequence) )) end - -- Check for "null" step - does nothing, allows macro to fall through - -- Usage: /castsequence reset=target Sunder Armor, null + -- Check for stop steps - does nothing, allows macro to fall through + -- Sequence stays on this step until a reset condition fires (target/combat/time/modifier) + -- Usage: /castsequence reset=target Sunder Armor, nil local actionLower = string.lower(CleveRoids.Trim(active.action)) - if actionLower == "null" or actionLower == "pass" or actionLower == "noop" then - -- Update sequence state (so per-target tracking works) + if actionLower == "nil" or actionLower == "null" or actionLower == "pass" or actionLower == "noop" then sequence.status = 0 sequence.lastUpdate = GetTime() if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit null step, falling through") + DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit stop step, falling through") end -- Don't cast anything, just return - allows macro to continue to next line return diff --git a/Init.lua b/Init.lua index 61e41e4..e6feef2 100644 --- a/Init.lua +++ b/Init.lua @@ -42,7 +42,7 @@ CleveRoids.spell_tracking = {} -- Format: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime} CleveRoids.castTracking = {} --- pfUI 7.6+ with Nampower 2.27.2+ detected (GUID-based cast tracking available) +-- pfUI 7.6+ with Nampower 2.31.0+ detected (GUID-based cast tracking available) CleveRoids.hasPfUI76 = false -- Combo point tracking (initialized early for /cast hook) diff --git a/Utility.lua b/Utility.lua index fc28e8e..2f4c612 100644 --- a/Utility.lua +++ b/Utility.lua @@ -702,7 +702,7 @@ function lib:HasEnhancedPfUILibdebuff() end -- Verify Nampower version based on pfUI version: - -- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.27.2+ + -- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.31.0+ -- - pfUI 7.4.3 to 7.5.x (legacy): requires Nampower v2.26+ if not GetNampowerVersion then return false end local npMajor, npMinor, npPatch = GetNampowerVersion() @@ -712,10 +712,9 @@ function lib:HasEnhancedPfUILibdebuff() local isPfUI76 = pfUI.libdebuff_slot_ownership ~= nil if isPfUI76 then - -- pfUI 7.6+ requires Nampower v2.27.2+ + -- pfUI 7.6+ requires Nampower v2.31.0+ if npMajor < 2 then return false end - if npMajor == 2 and npMinor < 27 then return false end - if npMajor == 2 and npMinor == 27 and npPatch < 2 then return false end + if npMajor == 2 and npMinor < 31 then return false end else -- Legacy pfUI 7.4.3-7.5.x requires Nampower v2.26+ if npMajor < 2 or (npMajor == 2 and npMinor < 26) then return false end @@ -733,7 +732,7 @@ function lib:HasEnhancedPfUILibdebuff() end -- Check if pfUI v7.6+ with enhanced cast tracking is available --- pfUI 7.6 requires Nampower v2.27.2+ and exposes additional tables +-- pfUI 7.6 requires Nampower v2.31.0+ and exposes additional tables function lib:HasPfUI76() if not pfUI then return false end @@ -744,13 +743,12 @@ function lib:HasPfUI76() if v.major < 7 then return false end if v.major == 7 and (v.minor or 0) < 6 then return false end - -- Verify Nampower v2.27.2+ (pfUI 7.6 hard requirement) + -- Verify Nampower v2.31.0+ (pfUI 7.6 hard requirement) if not GetNampowerVersion then return false end local npMajor, npMinor, npPatch = GetNampowerVersion() npPatch = npPatch or 0 if npMajor < 2 then return false end - if npMajor == 2 and npMinor < 27 then return false end - if npMajor == 2 and npMinor == 27 and npPatch < 2 then return false end + if npMajor == 2 and npMinor < 31 then return false end -- Verify the new tables exist if not pfUI.libdebuff_casts then return false end From ec43a636d623b10c02b4b429108cff6d59252e96 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 17 Feb 2026 19:19:07 -0500 Subject: [PATCH 09/12] update for pfui buff tracking and nampower buff tracking fallback --- Conditionals.lua | 141 ++++++++++++++++++-- Core.lua | 6 +- Extensions/OverflowBuffFrame.lua | 8 +- Init.lua | 2 + NampowerAPI.lua | 21 ++- Utility.lua | 216 +++++++++++++++++++++++++++++-- 6 files changed, 366 insertions(+), 28 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index e1ca333..f053a89 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -877,9 +877,9 @@ CleveRoids.AuraCapStatus = { } -- Overflow buff tracking: buffs applied while buff-capped (v2.34+) --- The server maintains up to 48 buff slots (32 visible + 16 invisible overflow). --- Buffs in slots 33-48 are mechanically active but have no client aura slot, --- so GetPlayerBuff/GetPlayerAuraDuration can't see them. +-- GetUnitField "aura" returns 48 slots: [1-32] = buff slots, [33-48] = debuff slots. +-- Beyond these 48 client slots, the server can hold additional overflow buffs with +-- no aura slot at all -- invisible to GetPlayerBuff and GetPlayerAuraDuration. -- Populated from AURA_CAST_ON_SELF when auraCapStatus indicates buff bar full. -- Format: [spellId] = { timestamp = GetTime(), durationSec = durationMs/1000 } CleveRoids.OverflowBuffs = {} @@ -1036,12 +1036,10 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur -- Check debuff bar full (bit 2) CleveRoids.AuraCapStatus.playerDebuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL) - -- Track overflow buffs: if buff bar is full, this aura may land in server slots 33-48 - -- (no client aura slot). Store spellId + duration so /cancelaura and [mybuff] can find it. - -- But first verify it's actually a buff — debuffs applied while buff-capped go into - -- debuff slots (32-47), not buff overflow. Player debuffs never overflow into buff slots. - if buffCapped and spellId and spellId > 0 and effectAuraName and effectAuraName > 0 then - -- Check debuff slots (32-47) — if the spellId is there, it's a debuff, not overflow + -- Determine if this aura is a buff (not debuff) — debuffs land in GetPlayerAuraDuration + -- slots 32-47, so checking those slots identifies them regardless of cap status. + local isBuffNotDebuff = false + if spellId and spellId > 0 and effectAuraName and effectAuraName > 0 then local isDebuff = false if _G.GetPlayerAuraDuration then for slot = 32, 47 do @@ -1052,8 +1050,11 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur end end end + isBuffNotDebuff = not isDebuff - if not isDebuff then + -- Track overflow buffs: if buff bar is full, this buff has no client aura slot. + -- Store spellId + duration so /cancelaura and [mybuff] can find it. + if buffCapped and isBuffNotDebuff then local entry = CleveRoids.OverflowBuffs[spellId] if not entry then entry = {} @@ -1062,7 +1063,9 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur entry.timestamp = now entry.durationSec = durationMs and (durationMs / 1000) or 0 end - elseif not buffCapped and next(CleveRoids.OverflowBuffs) then + end + + if not buffCapped and next(CleveRoids.OverflowBuffs) then -- No longer buff-capped: some overflow buffs may have gotten real slots. -- Only remove entries that now appear in a visible aura slot. -- The server does NOT auto-migrate overflow buffs into freed slots, @@ -1091,6 +1094,33 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur end end end + + -- NEW: Populate ownBuffCasts and allBuffAuras for all player buffs (not just overflow) + -- Use the isBuffNotDebuff result determined above (avoids redundant slot scanning) + local lib = CleveRoids.libdebuff + if isBuffNotDebuff and spellId and durationMs and durationMs > 0 and lib and not lib.hasPfUIEnhanced then + local spellName = SpellInfo and SpellInfo(spellId) + if spellName then + local _, playerGuidRaw = UnitExists("player") + local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw) + if playerGuid then + lib.ownBuffCasts[playerGuid] = lib.ownBuffCasts[playerGuid] or {} + lib.ownBuffCasts[playerGuid][spellName] = { + startTime = now, + duration = durationMs / 1000, + spellId = spellId, + casterGuid = casterGuid, + } + lib.allBuffAuras[playerGuid] = lib.allBuffAuras[playerGuid] or {} + lib.allBuffAuras[playerGuid][spellName] = lib.allBuffAuras[playerGuid][spellName] or {} + lib.allBuffAuras[playerGuid][spellName][casterGuid or "unknown"] = { + startTime = now, + duration = durationMs / 1000, + rank = 0, + } + end + end + end end local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAuraName, @@ -1126,6 +1156,25 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu spellName, spellId, string.sub(tostring(targetGuid), 1, 16), durationMs / 1000 )) end + + -- NEW: Store in pendingBuffCasts for BUFF_ADDED_OTHER to confirm as buff + -- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff) + local lib = CleveRoids.libdebuff + if lib and not lib.hasPfUIEnhanced then + local spellNameForPending = SpellInfo and SpellInfo(spellId) + if spellNameForPending then + local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid) + if normTargetGuid then + lib.pendingBuffCasts[normTargetGuid] = lib.pendingBuffCasts[normTargetGuid] or {} + lib.pendingBuffCasts[normTargetGuid][spellId] = { + casterGuid = CleveRoids.NormalizeGUID(casterGuid), + duration = durationMs / 1000, + spellName = spellNameForPending, + time = now, + } + end + end + end end -- Store cap status for this target GUID (if available) @@ -3464,6 +3513,36 @@ function CleveRoids.ValidateAura(unit, args, isbuff) end end + -- allBuffAuras fallback for player buff timing: when slow path found the buff but + -- returned no remaining time, check lib.allBuffAuras for cached AURA_CAST timing. + if found and remaining == nil and isPlayer and isbuff and searchName then + local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil + if lib and lib.allBuffAuras then + local _, playerGuidRaw = UnitExists("player") + local playerGuid = CleveRoids.NormalizeGUID(playerGuidRaw) + if playerGuid and lib.allBuffAuras[playerGuid] then + -- Try exact name match first + local casters = lib.allBuffAuras[playerGuid][args.name] + -- Try lowercase match if exact didn't work + if not casters then + for bName, c in pairs(lib.allBuffAuras[playerGuid]) do + if _string_lower(bName) == searchName then + casters = c + break + end + end + end + if casters then + for _, cData in pairs(casters) do + local elapsed = GetTime() - (cData.startTime or 0) + remaining = cData.duration > 0 and (cData.duration - elapsed) or -1 + break + end + end + end + end + end + -- Non-player overflow fallback: buff exists in server slots 33-48 (no client slot) -- AllCasterAuraTracking already has data from AURA_CAST_ON_OTHER for all aura applications. -- If the normal scan didn't find the buff, check there for presence + duration. @@ -3517,6 +3596,46 @@ function CleveRoids.ValidateAura(unit, args, isbuff) -- (showing ~1000s instead of actual remaining time). Skip it for buff time checks -- and rely on all-caster tracking from AURA_CAST events instead. + -- Fast path: Check lib.allBuffAuras (spellName-indexed, O(1) lookup) + -- More efficient than FindAllCasterAuraByName which does ID→name translation + if nonPlayerAuraTimeRemaining == nil and isbuff then + local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil + if lib and lib.allBuffAuras then + local _, targetGuid = UnitExists(unit) + targetGuid = targetGuid and CleveRoids.NormalizeGUID(targetGuid) + if targetGuid then + local buffEntries = lib.allBuffAuras[targetGuid] + if buffEntries then + -- Try exact name match first + local casters = buffEntries[args.name] + -- Try lowercase match if exact didn't work + if not casters and searchName then + for bName, c in pairs(buffEntries) do + if _string_lower(bName) == searchName then + casters = c + break + end + end + end + if casters then + for _, cData in pairs(casters) do + local elapsed = GetTime() - (cData.startTime or 0) + local rem = cData.duration > 0 and (cData.duration - elapsed) or -1 + if rem == nil or rem > 0 or cData.duration <= 0 then + nonPlayerAuraTimeRemaining = rem + if not found then + found = true + stacks = 0 + end + break + end + end + end + end + end + end + end + -- Second try: All-caster tracking from AURA_CAST events (works for any caster) -- Only use if libdebuff didn't find it (libdebuff has more accurate timing for player casts) if nonPlayerAuraTimeRemaining == nil then diff --git a/Core.lua b/Core.lua index d023924..ba19a77 100644 --- a/Core.lua +++ b/Core.lua @@ -2370,7 +2370,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo conditionals.target = origTarget return false end - -- Set target to the found GUID for soft-casting via SuperWoW + -- Set target to the found GUID for soft-casting via SuperWoW or Nampower v2.37+ conditionals.target = scanResult -- Don't need to retarget since we're using GUID directly needRetarget = false @@ -2402,7 +2402,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo end end - if conditionals.target ~= nil and targetBeforeAction and not (CleveRoids.hasSuperwow and action == CastSpellByName) then + if conditionals.target ~= nil and targetBeforeAction and not (action == CastSpellByName and (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken)) then if not UnitIsUnit("target", conditionals.target) then if SpellIsTargeting() then SpellStopCasting() @@ -2501,7 +2501,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo -- Legacy path: !Attack without explicit conditionals (checkchanneled injected) if msg == CleveRoids.Localized.Attack and conditionals.checkchanneled then AttackTarget() - elseif CleveRoids.hasSuperwow and conditionals.target then + elseif (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken) and conditionals.target then -- Let Nampower DLL handle queuing natively via its CastSpellByName hook CastSpellByName(castMsg, conditionals.target) else diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua index cd0418b..e921c92 100644 --- a/Extensions/OverflowBuffFrame.lua +++ b/Extensions/OverflowBuffFrame.lua @@ -1,7 +1,9 @@ --[[ OverflowBuffFrame Extension - Two frames displaying overflow buffs (slots 33-48) for the player - and the current target (if a group/party member). + Two frames displaying server-side overflow buffs (no client aura slot) for + the player and the current target (if a group/party member). + Player data: CleveRoids.OverflowBuffs (AURA_CAST_ON_SELF when buff-capped). + Target data: CleveRoids.AllCasterAuraTracking filtered by UnitBuff visibility. Each frame shows 2 rows of 8 icons = 16 slots. Author: Mewtiny @@ -98,7 +100,7 @@ local function CreateIconButton(parent, index, iconTable) if data.source ~= "player" then return end -- CancelPlayerAuraSpellId(spellId, ignoreMissing) - -- ignoreMissing=1 is required for overflow buffs in invisible slots 33-48 + -- ignoreMissing=1 is required for overflow buffs that have no client aura slot if CancelPlayerAuraSpellId then CancelPlayerAuraSpellId(data.spellId, 1) end diff --git a/Init.lua b/Init.lua index e6feef2..bf62857 100644 --- a/Init.lua +++ b/Init.lua @@ -324,6 +324,8 @@ initFrame:SetScript("OnEvent", function() CleveRoids.hasGetUnitData = API.features.hasGetUnitData CleveRoids.hasGetSpellModifiers = API.features.hasGetSpellModifiers CleveRoids.hasEnhancedSpellFunctions = API.features.hasEnhancedSpellFunctions + -- v2.37+: CastSpellByName supports unit token strings as 2nd param + CleveRoids.hasCastSpellByNameUnitToken = API.features.hasCastSpellByNameUnitToken end -- Initialize the API diff --git a/NampowerAPI.lua b/NampowerAPI.lua index d993693..9b178fc 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -88,8 +88,8 @@ Aura Cancel Functions (v2.34+): - CancelPlayerAuraSlot(auraSlot) - Cancel buff/debuff by raw 0-based aura slot - CancelPlayerAuraSpellId(spellId, [ignoreMissing]) - Cancel buff/debuff by spell ID - Pass ignoreMissing=1 to skip aura-slot presence check (for overflow buffs in - server slots 33-48 that have no client aura slot) + Pass ignoreMissing=1 to skip aura-slot presence check (for server-side + overflow buffs that have no client aura slot in the 48-slot array) GUID String Fix & Talent Helper (v2.35+): - GetUnitData/GetUnitField now return GUID-type fields (charm, summon, charmedBy, @@ -102,7 +102,14 @@ - PlayerIsRooted() - Returns 1 if player is rooted, nil if not - PlayerIsSwimming() - Returns 1 if player is swimming, nil if not - Current version: v2.36.0 + Unit Token Cast & Mouseover (v2.37+): + - CastSpellByName(spellName, unitToken) - 2nd param now accepts unit token strings + (e.g. "mouseover", "party1", "focus") in addition to GUIDs and 1 (self). + Enables soft-casting on unit tokens without changing the player's visible target. + - SetMouseoverUnit(unitToken) - Programmatically set the mouseover unit. + Previously only available via SuperWoW; now also provided by Nampower. + + Current version: v2.37.0 ]] local _G = _G or getfenv(0) @@ -272,6 +279,10 @@ API.VERSION_REQUIREMENTS = { ["PlayerIsMoving"] = { 2, 36, 0, "PlayerIsMoving" }, ["PlayerIsRooted"] = { 2, 36, 0, "PlayerIsRooted" }, ["PlayerIsSwimming"] = { 2, 36, 0, "PlayerIsSwimming" }, + + -- v2.37+ - Unit token cast support and SetMouseoverUnit + ["CastSpellByNameUnitToken"]= { 2, 37, 0 }, -- CastSpellByName accepts unit token strings as 2nd param + ["SetMouseoverUnit"] = { 2, 37, 0, "SetMouseoverUnit" }, } -- Check if a specific feature is available @@ -425,6 +436,10 @@ local function InitializeFeatures() f.hasPlayerIsRooted = API.HasFeature("PlayerIsRooted") f.hasPlayerIsSwimming = API.HasFeature("PlayerIsSwimming") + -- v2.37+ Unit token cast support and SetMouseoverUnit + f.hasCastSpellByNameUnitToken = API.HasFeature("CastSpellByNameUnitToken") + f.hasSetMouseoverUnit = API.HasFeature("SetMouseoverUnit") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() diff --git a/Utility.lua b/Utility.lua index 2f4c612..0e7001c 100644 --- a/Utility.lua +++ b/Utility.lua @@ -328,7 +328,8 @@ do local isUpdatingMouseover = false local function apply(unit) - if CleveRoids.hasSuperwow and _G.SetMouseoverUnit then + if _G.SetMouseoverUnit then + -- Available via SuperWoW or Nampower v2.37+ -- Set flag so UPDATE_MOUSEOVER_UNIT handler knows we triggered this CleveRoids.__mo.selfTriggered = true -- Use empty string instead of nil to properly clear mouseover. @@ -658,7 +659,7 @@ lib.guidToName = lib.guidToName or {} -- duration calculation, and miss/dodge/parry/resist/immune detection. -- Enhanced tables (populated by pfUI or standalone Nampower handlers) -lib.ownDebuffs = lib.ownDebuffs or {} -- [targetGUID][spellName] = {startTime, duration, texture, rank, slot} +lib.ownDebuffs = lib.ownDebuffs or {} -- [targetGUID][spellName] = {startTime, duration, texture, rank, spellId} lib.ownSlots = lib.ownSlots or {} -- [targetGUID][slot] = spellName (LEGACY - empty when pfUI 7.6+ is active) lib.allSlots = lib.allSlots or {} -- [targetGUID][slot] = {spellName, casterGuid, isOurs} (LEGACY - empty when pfUI 7.6+ is active) lib.slotOwnership = lib.slotOwnership or {} -- [targetGUID][auraSlot] = {casterGuid, spellName, spellId, isOurs} (pfUI 7.6+ GetUnitField edition) @@ -667,6 +668,16 @@ lib.pendingCasts = lib.pendingCasts or {} -- [targetGUID][spellName] = {caster lib.recentMisses = lib.recentMisses or {} -- [targetGUID][spellName] = {time, spellId, targetName, reason} for miss/dodge/parry detection lib.iconCache = lib.iconCache or {} -- [spellId] = texture (shared with pfUI 7.6 or standalone) +-- Buff tracking tables (parallel to debuff tables, standalone Nampower mode only) +-- Buff tables are NOT linked to pfUI — pfUI has no public buff tracking tables. +-- They remain empty when hasPfUIEnhanced=true since all buff event handlers are gated by it. +lib.ownBuffCasts = lib.ownBuffCasts or {} -- [targetGUID][buffName] = {startTime, duration, spellId, casterGuid} + -- Player's own buff casts on self or others (AURA_CAST events) +lib.allBuffAuras = lib.allBuffAuras or {} -- [targetGUID][buffName][casterGuid] = {startTime, duration, rank} + -- All buffs from all casters on any unit (confirmed by BUFF_ADDED events) +lib.pendingBuffCasts = lib.pendingBuffCasts or {} -- [targetGUID][spellId] = {casterGuid, duration, spellName, time} + -- Temp storage from AURA_CAST_ON_OTHER, consumed by BUFF_ADDED_OTHER + -- Flag indicating whether enhanced pfUI tracking is available lib.hasPfUIEnhanced = false lib.hasStandaloneNampower = false @@ -702,7 +713,7 @@ function lib:HasEnhancedPfUILibdebuff() end -- Verify Nampower version based on pfUI version: - -- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.31.0+ + -- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.37.0+ -- - pfUI 7.4.3 to 7.5.x (legacy): requires Nampower v2.26+ if not GetNampowerVersion then return false end local npMajor, npMinor, npPatch = GetNampowerVersion() @@ -712,9 +723,9 @@ function lib:HasEnhancedPfUILibdebuff() local isPfUI76 = pfUI.libdebuff_slot_ownership ~= nil if isPfUI76 then - -- pfUI 7.6+ requires Nampower v2.31.0+ + -- pfUI 7.6+ requires Nampower v2.37.0+ if npMajor < 2 then return false end - if npMajor == 2 and npMinor < 31 then return false end + if npMajor == 2 and npMinor < 37 then return false end else -- Legacy pfUI 7.4.3-7.5.x requires Nampower v2.26+ if npMajor < 2 or (npMajor == 2 and npMinor < 26) then return false end @@ -732,7 +743,7 @@ function lib:HasEnhancedPfUILibdebuff() end -- Check if pfUI v7.6+ with enhanced cast tracking is available --- pfUI 7.6 requires Nampower v2.31.0+ and exposes additional tables +-- pfUI 7.6+ requires Nampower v2.37.0+ and exposes additional tables function lib:HasPfUI76() if not pfUI then return false end @@ -743,12 +754,12 @@ function lib:HasPfUI76() if v.major < 7 then return false end if v.major == 7 and (v.minor or 0) < 6 then return false end - -- Verify Nampower v2.31.0+ (pfUI 7.6 hard requirement) + -- Verify Nampower v2.37.0+ (pfUI 7.6+ hard requirement) if not GetNampowerVersion then return false end local npMajor, npMinor, npPatch = GetNampowerVersion() npPatch = npPatch or 0 if npMajor < 2 then return false end - if npMajor == 2 and npMinor < 31 then return false end + if npMajor == 2 and npMinor < 37 then return false end -- Verify the new tables exist if not pfUI.libdebuff_casts then return false end @@ -1182,6 +1193,58 @@ function lib:CleanupStaleTrackingData() end end end + + -- Clean ownBuffCasts (player-cast buffs on targets) + if lib.ownBuffCasts then + for guid, buffs in pairs(lib.ownBuffCasts) do + for buffName, data in pairs(buffs) do + local elapsed = now - (data.startTime or 0) + local dur = data.duration or 0 + -- Remove if expired (duration > 0 and past end time) or stale (no duration and old) + if (dur > 0 and elapsed > dur) or (dur <= 0 and elapsed > staleTime) then + buffs[buffName] = nil + end + end + if not next(buffs) then + lib.ownBuffCasts[guid] = nil + end + end + end + + -- Clean allBuffAuras (all-caster buff tracking) + if lib.allBuffAuras then + for guid, buffs in pairs(lib.allBuffAuras) do + for buffName, casters in pairs(buffs) do + for cGuid, data in pairs(casters) do + local elapsed = now - (data.startTime or 0) + local dur = data.duration or 0 + if (dur > 0 and elapsed > dur) or (dur <= 0 and elapsed > staleTime) then + casters[cGuid] = nil + end + end + if not next(casters) then + buffs[buffName] = nil + end + end + if not next(buffs) then + lib.allBuffAuras[guid] = nil + end + end + end + + -- Clean pendingBuffCasts (short-lived correlation data, 2 second TTL) + if lib.pendingBuffCasts then + for guid, spells in pairs(lib.pendingBuffCasts) do + for spellId, data in pairs(spells) do + if (now - (data.time or 0)) > 2 then + spells[spellId] = nil + end + end + if not next(spells) then + lib.pendingBuffCasts[guid] = nil + end + end + end end -- Get the caster GUID for a debuff on a target @@ -3689,6 +3752,13 @@ if CleveRoids.hasNampower then ev:RegisterEvent("DEBUFF_REMOVED_OTHER") ev:RegisterEvent("UNIT_DIED") -- Instant cleanup on target death + -- BUFF_ADDED/REMOVED events require v2.30+ for auraSlot and state args + if npMajor > 2 or (npMajor == 2 and npMinor >= 30) then + ev:RegisterEvent("BUFF_ADDED_OTHER") + ev:RegisterEvent("BUFF_REMOVED_SELF") + ev:RegisterEvent("BUFF_REMOVED_OTHER") + end + if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Registered UNIT_DIED for instant cleanup (v2.26+)") end @@ -4886,6 +4956,125 @@ ev:SetScript("OnEvent", function() end end + -- NAMPOWER v2.30+ BUFF_ADDED_OTHER - Confirm pending AURA_CAST_ON_OTHER data as buff + elseif event == "BUFF_ADDED_OTHER" then + if lib.hasPfUIEnhanced then return end + + local guid = CleveRoids.NormalizeGUID(arg1) + local spellId = arg3 + local state = arg7 -- 0=added, 1=removed, 2=modified + + if not guid or not spellId then return end + + -- Consume pending AURA_CAST data for this buff + local pending = lib.pendingBuffCasts[guid] and lib.pendingBuffCasts[guid][spellId] + if pending then + local spellName = pending.spellName or (SpellInfo and SpellInfo(spellId)) + if spellName then + local casterGuid = pending.casterGuid + local duration = pending.duration + local now = GetTime() + + -- allBuffAuras: confirmed buff from any caster + lib.allBuffAuras[guid] = lib.allBuffAuras[guid] or {} + lib.allBuffAuras[guid][spellName] = lib.allBuffAuras[guid][spellName] or {} + lib.allBuffAuras[guid][spellName][casterGuid or "unknown"] = { + startTime = now, + duration = duration or 0, + rank = 0, + } + + -- ownBuffCasts: only if player is the caster + local _, playerGuidRaw = UnitExists("player") + local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw) + if playerGuid and casterGuid == playerGuid then + lib.ownBuffCasts[guid] = lib.ownBuffCasts[guid] or {} + lib.ownBuffCasts[guid][spellName] = { + startTime = now, + duration = duration or 0, + spellId = spellId, + casterGuid = casterGuid, + } + end + end + lib.pendingBuffCasts[guid][spellId] = nil + end + + -- NAMPOWER v2.30+ BUFF_REMOVED_SELF - Player's own buffs removed + elseif event == "BUFF_REMOVED_SELF" then + if lib.hasPfUIEnhanced then return end + + local spellId = arg3 + local state = arg7 + if not spellId then return end + if state == 2 then return end -- Stack decrease only, not full removal + + local spellName = SpellInfo and SpellInfo(spellId) + local _, playerGuidRaw = UnitExists("player") + local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw) + + if spellName and playerGuid then + if lib.ownBuffCasts[playerGuid] then + lib.ownBuffCasts[playerGuid][spellName] = nil + if not next(lib.ownBuffCasts[playerGuid]) then + lib.ownBuffCasts[playerGuid] = nil + end + end + if lib.allBuffAuras[playerGuid] then + lib.allBuffAuras[playerGuid][spellName] = nil + if not next(lib.allBuffAuras[playerGuid]) then + lib.allBuffAuras[playerGuid] = nil + end + end + end + + -- Also prune from OverflowBuffs (existing system) + if spellId and CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[spellId] then + CleveRoids.OverflowBuffs[spellId] = nil + end + + -- NAMPOWER v2.30+ BUFF_REMOVED_OTHER - Buffs removed from other units + elseif event == "BUFF_REMOVED_OTHER" then + if lib.hasPfUIEnhanced then return end + + local guid = CleveRoids.NormalizeGUID(arg1) + local spellId = arg3 + local state = arg7 + if not guid or not spellId then return end + if state == 2 then return end -- Stack decrease only, not full removal + + local spellName = SpellInfo and SpellInfo(spellId) + if spellName then + if lib.allBuffAuras[guid] then + lib.allBuffAuras[guid][spellName] = nil + if not next(lib.allBuffAuras[guid]) then + lib.allBuffAuras[guid] = nil + end + end + if lib.ownBuffCasts[guid] then + lib.ownBuffCasts[guid][spellName] = nil + if not next(lib.ownBuffCasts[guid]) then + lib.ownBuffCasts[guid] = nil + end + end + end + + -- Clean pending correlation data + if lib.pendingBuffCasts[guid] then + lib.pendingBuffCasts[guid][spellId] = nil + if not next(lib.pendingBuffCasts[guid]) then + lib.pendingBuffCasts[guid] = nil + end + end + + -- Also clean AllCasterAuraTracking (existing system) + if CleveRoids.AllCasterAuraTracking[guid] then + CleveRoids.AllCasterAuraTracking[guid][spellId] = nil + if not next(CleveRoids.AllCasterAuraTracking[guid]) then + CleveRoids.AllCasterAuraTracking[guid] = nil + end + end + -- NAMPOWER v2.26+ UNIT_DIED - Instant cleanup on target death elseif event == "UNIT_DIED" then local guid = arg1 @@ -4918,6 +5107,17 @@ ev:SetScript("OnEvent", function() lib.recentMisses[guid] = nil end + -- Clean up buff tracking tables + if lib.ownBuffCasts[guid] then + lib.ownBuffCasts[guid] = nil + end + if lib.allBuffAuras[guid] then + lib.allBuffAuras[guid] = nil + end + if lib.pendingBuffCasts[guid] then + lib.pendingBuffCasts[guid] = nil + end + -- Clean up cast tracking for this unit (they can't be casting if dead) if not lib.hasPfUI76 and CleveRoids.castTracking[guid] then CleveRoids.castTracking[guid] = nil From 15091a867b5779280f01aae2fd8b73d33e6da671 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Wed, 18 Feb 2026 17:18:48 -0500 Subject: [PATCH 10/12] fix fallback movement conditional and update nampower support to 2_38 --- Conditionals.lua | 25 +++++++++---------------- Core.lua | 12 ++++++++++++ NampowerAPI.lua | 35 ++++++++++++++++++++++++++++++++++- Utility.lua | 32 ++++++++++++++++++++++++++------ 4 files changed, 81 insertions(+), 23 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index f053a89..dbab3c9 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -2664,25 +2664,18 @@ function CleveRoids.IsPlayerMoving() local headIdx = CleveRoids._posHistoryIndex or 0 if history and count >= 2 then - -- Iterate consecutive pairs in circular buffer order (oldest to newest) - -- Head is the most recent entry, walk backwards through the ring - local samplesAvail = count - for i = 1, samplesAvail - 1 do - -- prevIdx and currIdx walk from oldest to newest - local currOffset = samplesAvail - i -- e.g., 3,2,1,0 for count=4 - local prevOffset = samplesAvail - i + 1 -- e.g., 4,3,2,1 - local currIdx = math.mod((headIdx - currOffset - 1 + 4), 4) + 1 - local prevIdx = math.mod((headIdx - prevOffset - 1 + 4), 4) + 1 - local prev = history[prevIdx] - local curr = history[currIdx] + -- Compare the most recent pair: headIdx (newest) vs. the slot before it. + -- Use (headIdx - 2 + 4) mod 4 to safely wrap backwards in the 1-based ring. + -- This avoids Lua 5.0 math.mod returning negative values for negative inputs. + local prevIdx = math.mod((headIdx - 2 + 4), 4) + 1 + local curr = history[headIdx] + local prev = history[prevIdx] + if curr and prev then local dx = curr.x - prev.x local dy = curr.y - prev.y - local dist = math.sqrt(dx * dx + dy * dy) - if dist > 0.001 then - return true -- Any movement in recent history = moving - end + return math.sqrt(dx * dx + dy * dy) > 0.001 end - return false -- No movement in any recent sample = stopped + return false end -- Legacy fallback for code that might not have history yet diff --git a/Core.lua b/Core.lua index ba19a77..c303348 100644 --- a/Core.lua +++ b/Core.lua @@ -5208,6 +5208,18 @@ function CleveRoids.Frame:SPELLCAST_CHANNEL_START() end end + -- v2.38+: Override channelDuration with the accurate server-provided value stored by + -- SPELL_START_SELF (which fires before this event). This beats the tooltip scan or + -- UNIT_CASTEVENT cast_time fallback, because the server already applied all modifiers. + if CleveRoids._v238ChannelDuration then + if not spellId or not CleveRoids._v238ChannelSpellId + or CleveRoids._v238ChannelSpellId == spellId then + CleveRoids.channelDuration = CleveRoids._v238ChannelDuration + end + CleveRoids._v238ChannelDuration = nil + CleveRoids._v238ChannelSpellId = nil + end + -- Force immediate action update CleveRoids.TestForAllActiveActions() end diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 9b178fc..6c2a7b1 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -109,7 +109,19 @@ - SetMouseoverUnit(unitToken) - Programmatically set the mouseover unit. Previously only available via SuperWoW; now also provided by Nampower. - Current version: v2.37.0 + Spell Duration & Enhanced SPELL_START (v2.38+): + - GetSpellDuration(spellId, [ignoreModifiers]) - Returns duration in ms. + For channeling spells: channel duration. For others: first aura effect duration. + Pass ignoreModifiers=1 to get base duration (ignores talents/buffs). + Returns 0 if spell has no duration, nil if spellId invalid. + - SPELL_START_SELF / SPELL_START_OTHER now include two new parameters: + arg7 = duration (ms) - channel duration for channeling spells, 0 otherwise + arg8 = spellType - 0=Normal, 1=Channeling, 2=Autorepeating + Use spellType to distinguish channeling spells (SPELL_START_OTHER carries both + cast-time and channeling spells since there is no separate SPELL_CHANNEL_START + packet for other players). + + Current version: v2.38.0 ]] local _G = _G or getfenv(0) @@ -283,6 +295,10 @@ API.VERSION_REQUIREMENTS = { -- v2.37+ - Unit token cast support and SetMouseoverUnit ["CastSpellByNameUnitToken"]= { 2, 37, 0 }, -- CastSpellByName accepts unit token strings as 2nd param ["SetMouseoverUnit"] = { 2, 37, 0, "SetMouseoverUnit" }, + + -- v2.38+ - GetSpellDuration and enhanced SPELL_START parameters + ["GetSpellDuration"] = { 2, 38, 0, "GetSpellDuration" }, + ["SpellStartSpellType"] = { 2, 38, 0 }, -- duration + spellType params added to SPELL_START events } -- Check if a specific feature is available @@ -440,6 +456,10 @@ local function InitializeFeatures() f.hasCastSpellByNameUnitToken = API.HasFeature("CastSpellByNameUnitToken") f.hasSetMouseoverUnit = API.HasFeature("SetMouseoverUnit") + -- v2.38+ GetSpellDuration and enhanced SPELL_START parameters + f.hasGetSpellDuration = API.HasFeature("GetSpellDuration") + f.hasSpellStartSpellType = API.HasFeature("SpellStartSpellType") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() @@ -3040,6 +3060,19 @@ function API.GetSpellPower(mode) return _G.GetSpellPower(mode) end +-- Get duration of a spell in milliseconds (v2.38+) +-- For channeling spells: returns the channel duration. +-- For non-channeling spells: returns the first aura effect duration. +-- spellId: spell ID to look up +-- ignoreModifiers: pass 1 to ignore talent/buff modifiers (returns base duration) +-- Returns: duration in ms (0 if spell has no duration), or nil if spellId invalid +function API.GetSpellDuration(spellId, ignoreModifiers) + if not API.features.hasGetSpellDuration or not _G.GetSpellDuration then + return nil + end + return _G.GetSpellDuration(spellId, ignoreModifiers) +end + -------------------------------------------------------------------------------- -- AURA CANCEL FUNCTIONS (v2.34+) -------------------------------------------------------------------------------- diff --git a/Utility.lua b/Utility.lua index 0e7001c..37575c1 100644 --- a/Utility.lua +++ b/Utility.lua @@ -754,12 +754,12 @@ function lib:HasPfUI76() if v.major < 7 then return false end if v.major == 7 and (v.minor or 0) < 6 then return false end - -- Verify Nampower v2.37.0+ (pfUI 7.6+ hard requirement) + -- Verify Nampower v2.38.0+ (pfUI 7.6+ hard requirement, bumped from 2.37 in pfUI update 2026-02-18) if not GetNampowerVersion then return false end local npMajor, npMinor, npPatch = GetNampowerVersion() npPatch = npPatch or 0 if npMajor < 2 then return false end - if npMajor == 2 and npMinor < 37 then return false end + if npMajor == 2 and npMinor < 38 then return false end -- Verify the new tables exist if not pfUI.libdebuff_casts then return false end @@ -4450,19 +4450,38 @@ ev:SetScript("OnEvent", function() -- and these handlers return early (pfUI handles it). elseif event == "SPELL_START_SELF" or event == "SPELL_START_OTHER" then + -- v2.38+: For player's own channeling spells, pre-store the accurate server channel duration + -- BEFORE the pfUI early-return so SPELLCAST_CHANNEL_START (which fires later) can use it. + -- arg8 == 1 means spellType=Channeling; arg7 is the channel duration in ms. + if event == "SPELL_START_SELF" and arg8 == 1 and arg7 and arg7 > 0 then + CleveRoids._v238ChannelDuration = arg7 / 1000 + CleveRoids._v238ChannelSpellId = arg2 + end -- pfUI 7.6 manages castTracking via its own SPELL_START handler if lib.hasPfUI76 then return end local spellId = arg2 local casterGuid = arg3 - local castTime = arg6 -- milliseconds + local castTime = arg6 -- cast time in milliseconds + -- v2.38+: arg7 = channel duration (ms, channeling spells only), arg8 = spellType (0=Normal,1=Channeling,2=Autorepeating) + local channelDuration = arg7 + local spellType = arg8 if not casterGuid or not spellId then return end local spellName = SpellInfo and SpellInfo(spellId) local icon = lib:GetCachedIcon(spellId) local now = GetTime() - local durationSec = castTime and (castTime / 1000) or 0 + + -- For channeling spells (v2.38+: spellType==1), use the channel duration. + -- For normal cast-time spells, use castTime. Fall back to castTime if params unavailable. + local durationMs + if spellType == 1 and channelDuration and channelDuration > 0 then + durationMs = channelDuration + else + durationMs = castTime or 0 + end + local durationSec = durationMs / 1000 CleveRoids.castTracking[casterGuid] = { spellID = spellId, @@ -4474,9 +4493,10 @@ ev:SetScript("OnEvent", function() } if CleveRoids.debug then + local typeStr = spellType == 1 and "channel" or (spellType == 2 and "auto" or "cast") DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ccff[SPELL_START]|r %s (ID:%d) cast by %s - %.1fs", - spellName or "Unknown", spellId, casterGuid, durationSec) + string.format("|cff00ccff[SPELL_START]|r %s (ID:%d) cast by %s - %.1fs [%s]", + spellName or "Unknown", spellId, casterGuid, durationSec, typeStr) ) end From 59b48bf66008b2f5a0d3453ad62e40aa01b72c83 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Wed, 18 Feb 2026 18:04:09 -0500 Subject: [PATCH 11/12] readme update --- Conditionals.lua | 4027 +++++++++++++++++++------------------- README.md | 176 +- SuperCleveRoidMacros.toc | 2 +- 3 files changed, 2102 insertions(+), 2103 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index dbab3c9..95db11e 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -5012,229 +5012,202 @@ 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, + -- ======================================================================== + -- PLAYER-ONLY CONDITIONALS + -- ======================================================================== + -- These conditionals always check player state and are not affected by @unit + -- ======================================================================== - -- 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, + casttime = function(conditionals) + -- Calculate remaining time (0 if not casting) + local timeLeft = 0 - -- 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, - - -- [nohelp] - Target is NOT friendly (cannot assist) - nohelp = function(conditionals) - if not conditionals.target or not UnitExists(conditionals.target) then - return true - end - return not UnitCanAssist("player", conditionals.target) - end, - - harm = function(conditionals) - return conditionals.harm and conditionals.target and UnitExists(conditionals.target) and UnitCanAttack("player", conditionals.target) - end, - - -- [noharm] - Target is NOT hostile (cannot attack) - noharm = function(conditionals) - if not conditionals.target or not UnitExists(conditionals.target) then - return true - end - return not UnitCanAttack("player", conditionals.target) - end, - - stance = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - -- PERFORMANCE: Use specialized function to avoid closure allocation - return OrEqualsNumber(conditionals.stance, i) - end, - - nostance = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - local forbiddenStances = conditionals.nostance - if type(forbiddenStances) ~= "table" then - return i == 0 - end - -- PERFORMANCE: Use specialized function to avoid closure allocation - return AndNotEqualsNumber(forbiddenStances, i) - end, - - noform = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - local forbiddenForms = conditionals.noform - if type(forbiddenForms) ~= "table" then - return i == 0 - end - -- PERFORMANCE: Use specialized function to avoid closure allocation - return AndNotEqualsNumber(forbiddenForms, i) - end, - - form = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - -- PERFORMANCE: Use specialized function to avoid closure allocation - return OrEqualsNumber(conditionals.form, i) - 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. - -- PERFORMANCE: Use event-driven cache for player combat state - local cached = CleveRoids._cachedPlayerInCombat - if cached ~= nil then - return cached - end - -- Fallback if cache not yet initialized - 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. - -- PERFORMANCE: Use event-driven cache for player combat state - local cached = CleveRoids._cachedPlayerInCombat - if cached ~= nil then - return not cached - end - -- Fallback if cache not yet initialized - 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 + local elapsed = GetTime() - CleveRoids.castStartTime + timeLeft = CleveRoids.castDuration - elapsed + -- Don't allow negative time + if timeLeft < 0 then timeLeft = 0 end end - -- Check for channel with time-based prediction + 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, + + -- [nocasttime] - Negated cast time comparison + -- Usage: [nocasttime:<0.5] = true if NOT (casttime < 0.5), i.e., >= 0.5 seconds remaining + nocasttime = 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 + if timeLeft < 0 then timeLeft = 0 end + end + + local check = conditionals.nocasttime + + 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 + return not CleveRoids.comparators[check.operator](timeLeft, check.amount) + end + + return true + 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 == "cast" or CleveRoids.CurrentSpell.type == "channeled" + return 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 - + 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 ~= "cast" and CleveRoids.CurrentSpell.type ~= "channeled" + return 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) + 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, - 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") + -- [nochanneltime] - Negated channel time comparison + -- Usage: [nochanneltime:<0.5] = true if NOT (channeltime < 0.5), i.e., >= 0.5 seconds remaining + nochanneltime = 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 + if timeLeft < 0 then timeLeft = 0 end + end + + local check = conditionals.nochanneltime + + 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 + return not CleveRoids.comparators[check.operator](timeLeft, check.amount) + end + + return true + 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, + + 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, + + 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, + + 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, + + 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, equipped = function(conditionals) @@ -5285,617 +5258,20 @@ CleveRoids.Keywords = { end) end, - dead = function(conditionals) - if not conditionals.target then return false end - return UnitIsDeadOrGhost(conditionals.target) + form = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + -- PERFORMANCE: Use specialized function to avoid closure allocation + return OrEqualsNumber(conditionals.form, i) 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, - - -- [nomember] - check if target is NOT in party or raid - nomember = function(conditionals) - return not ( - CleveRoids.IsTargetInGroupType(conditionals.target, "party") - or CleveRoids.IsTargetInGroupType(conditionals.target, "raid") - ) - end, - - -- [party] or [party:unitid] - check if unit is in your party - -- Default unit is conditionals.target - party = function(conditionals) - local unit = conditionals.party - if unit == true or unit == nil then - unit = conditionals.target + noform = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + local forbiddenForms = conditionals.noform + if type(forbiddenForms) ~= "table" then + return i == 0 end - return CleveRoids.IsTargetInGroupType(unit, "party") - end, - - -- [noparty] or [noparty:unitid] - check if unit is NOT in your party - noparty = function(conditionals) - local unit = conditionals.noparty - if unit == true or unit == nil then - unit = conditionals.target - end - return not CleveRoids.IsTargetInGroupType(unit, "party") - end, - - -- [raid] or [raid:unitid] - check if unit is in your raid - -- Default unit is conditionals.target - raid = function(conditionals) - local unit = conditionals.raid - if unit == true or unit == nil then - unit = conditionals.target - end - return CleveRoids.IsTargetInGroupType(unit, "raid") - end, - - -- [noraid] or [noraid:unitid] - check if unit is NOT in your raid - noraid = function(conditionals) - local unit = conditionals.noraid - if unit == true or unit == nil then - unit = conditionals.target - end - return not CleveRoids.IsTargetInGroupType(unit, "raid") - end, - - -- [tag] - target is tapped (tagged) by anyone - tag = function(conditionals) - return conditionals.target and UnitIsTapped(conditionals.target) - end, - - -- [notag] - target is not tapped - notag = function(conditionals) - return conditionals.target and not UnitIsTapped(conditionals.target) - end, - - -- [mytag] - target is tapped by the player - mytag = function(conditionals) - return conditionals.target and UnitIsTappedByPlayer(conditionals.target) - end, - - -- [nomytag] - target is not tapped by the player - nomytag = function(conditionals) - return conditionals.target and not UnitIsTappedByPlayer(conditionals.target) - end, - - -- [othertag] - target is tapped by someone else (not the player) - othertag = function(conditionals) - return conditionals.target and UnitIsTapped(conditionals.target) and not UnitIsTappedByPlayer(conditionals.target) - end, - - -- [noothertag] - target is not tapped by someone else (not tapped, or tapped by player) - noothertag = function(conditionals) - return conditionals.target and (not UnitIsTapped(conditionals.target) or UnitIsTappedByPlayer(conditionals.target)) - end, - - -- [group] or [group:party] or [group:raid] or [group:party/raid] - -- Checks if the PLAYER is in a group (not unit membership) - group = function(conditionals) - local groupVal = conditionals.group - -- Boolean form [group] - check if in any group - if groupVal == true then - return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 - end - -- Value form [group:party] or [group:raid] or [group:party/raid] - return Multi(groupVal, function(groupType) - if groupType == "party" then - return GetNumPartyMembers() > 0 - elseif groupType == "raid" then - return GetNumRaidMembers() > 0 - end - return false - end, conditionals, "group") - end, - - -- [nogroup] or [nogroup:party] or [nogroup:raid] or [nogroup:party/raid] - -- Checks if the PLAYER is NOT in a group - nogroup = function(conditionals) - local groupVal = conditionals.nogroup - -- Boolean form [nogroup] - check if not in any group - if groupVal == true then - return GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0 - end - -- Value form with De Morgan's law via NegatedMulti - return NegatedMulti(groupVal, function(groupType) - if groupType == "party" then - return GetNumPartyMembers() == 0 - elseif groupType == "raid" then - return GetNumRaidMembers() == 0 - end - return true - end, conditionals, "nogroup") - 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) - -- Extract spell name from args (could be string or table with name/operator/amount) - local spellName = type(v) == "table" and v.name or v - - -- Check if this debuff is PENDING (being cast or queued) - -- This prevents double-application when spamming macros with spell queue - if CleveRoids.IsPendingDebuffCast(spellName, conditionals.target) then - return false -- Treat as if debuff exists (nodebuff returns false) - end - - -- Debuff not pending, check if it actually exists on target - 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 or "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 or "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 or "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 or "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 or "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") + -- PERFORMANCE: Use specialized function to avoid closure allocation + return AndNotEqualsNumber(forbiddenForms, i) end, -- GCD conditional - check if GCD is active/remaining @@ -5985,128 +5361,695 @@ CleveRoids.Keywords = { end, conditionals, "nogcd") end, - channeled = function(conditionals) - -- Use time-based prediction for accuracy + -- [group] or [group:party] or [group:raid] or [group:party/raid] + -- Checks if the PLAYER is in a group (not unit membership) + group = function(conditionals) + local groupVal = conditionals.group + -- Boolean form [group] - check if in any group + if groupVal == true then + return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 + end + -- Value form [group:party] or [group:raid] or [group:party/raid] + return Multi(groupVal, function(groupType) + if groupType == "party" then + return GetNumPartyMembers() > 0 + elseif groupType == "raid" then + return GetNumRaidMembers() > 0 + end + return false + end, conditionals, "group") + end, + + -- [nogroup] or [nogroup:party] or [nogroup:raid] or [nogroup:party/raid] + -- Checks if the PLAYER is NOT in a group + nogroup = function(conditionals) + local groupVal = conditionals.nogroup + -- Boolean form [nogroup] - check if not in any group + if groupVal == true then + return GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0 + end + -- Value form with De Morgan's law via NegatedMulti + return NegatedMulti(groupVal, function(groupType) + if groupType == "party" then + return GetNumPartyMembers() == 0 + elseif groupType == "raid" then + return GetNumRaidMembers() == 0 + end + return true + end, conditionals, "nogroup") + 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, + + -- [incominghit:type] - Check result of last attack received by player + -- Types: crit, crushing, glancing, miss, dodge, parry, blocked, hit + -- Time check: [incominghit:<1] = received hit within last 1 second + -- Examples: [incominghit:crushing] [incominghit:crit/crushing] [incominghit:dodge] + incominghit = function(conditionals) + -- Boolean form [incominghit] - any recent incoming hit + if not conditionals.incominghit or + conditionals.incominghit == true or + (type(conditionals.incominghit) == "table" and table.getn(conditionals.incominghit) == 0) then + return CleveRoids.ValidateIncomingHit(nil, nil, nil) + end + + -- Check for specific hit type(s) - OR logic + return Or(conditionals.incominghit, function(args) + if type(args) == "string" then + return CleveRoids.ValidateIncomingHit(args, nil, nil) + elseif type(args) == "table" then + return CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) + end + return false + end) + end, + + -- [noincominghit:type] - Check that last incoming hit was NOT a specific type + noincominghit = function(conditionals) + -- Boolean form [noincominghit] - no recent incoming hit + if not conditionals.noincominghit or + conditionals.noincominghit == true or + (type(conditionals.noincominghit) == "table" and table.getn(conditionals.noincominghit) == 0) then + return not CleveRoids.ValidateIncomingHit(nil, nil, nil) + end + + -- Check that hit was NOT any of the specified types - AND logic + return NegatedMulti(conditionals.noincominghit, function(args) + if type(args) == "string" then + return not CleveRoids.ValidateIncomingHit(args, nil, nil) + elseif type(args) == "table" then + return not CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) + end + return true + end, conditionals, "noincominghit") + 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, + + -- ======================================================================== + -- AUTO-ATTACK CONDITIONALS (Nampower v2.24+) + -- ======================================================================== + -- Requires NP_EnableAutoAttackEvents=1 CVar + + -- [lastswing:type] - Check result of player's last melee swing + -- Types: crit, glancing, miss, dodge, parry, blocked, offhand/oh, mainhand/mh, hit + -- Time check: [lastswing:<2] = last swing was within 2 seconds + -- Multi-value: [lastswing:crit/glancing] = was crit OR glancing (OR logic) + -- Examples: [lastswing:dodge] [@target,lastswing:crit] [lastswing:offhand] + lastswing = function(conditionals) + -- Boolean form [lastswing] - any recent swing + if not conditionals.lastswing or + conditionals.lastswing == true or + (type(conditionals.lastswing) == "table" and table.getn(conditionals.lastswing) == 0) then + return CleveRoids.ValidateLastSwing(nil, nil, nil) + end + + -- Check for specific swing type(s) - OR logic + return Or(conditionals.lastswing, function(args) + if type(args) == "string" then + return CleveRoids.ValidateLastSwing(args, nil, nil) + elseif type(args) == "table" then + return CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) + end + return false + end) + end, + + -- [nolastswing:type] - Check that last swing was NOT a specific type + -- Uses AND logic: [nolastswing:crit/glancing] = was NOT crit AND NOT glancing + nolastswing = function(conditionals) + -- Boolean form [nolastswing] - no recent swing + if not conditionals.nolastswing or + conditionals.nolastswing == true or + (type(conditionals.nolastswing) == "table" and table.getn(conditionals.nolastswing) == 0) then + return not CleveRoids.ValidateLastSwing(nil, nil, nil) + end + + -- Check that swing was NOT any of the specified types - AND logic + return NegatedMulti(conditionals.nolastswing, function(args) + if type(args) == "string" then + return not CleveRoids.ValidateLastSwing(args, nil, nil) + elseif type(args) == "table" then + return not CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) + end + return true + end, conditionals, "nolastswing") + end, + + -- [mhimbue] - Check main hand has temporary imbue (poison, oil, sharpening stone, etc.) + -- [mhimbue:Name] - Check for specific imbue name + -- [mhimbue:<300] - Check imbue time remaining < 300 seconds + -- [mhimbue:Name<300] - Specific imbue with time check + -- [mhimbue:>#5] - Check imbue charges > 5 + mhimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.mhimbue, conditionals) + return CleveRoids.ValidateWeaponImbue("mh", args) + end, + + nomhimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.nomhimbue, conditionals) + return not CleveRoids.ValidateWeaponImbue("mh", args) + 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, + + -- ========================================================================= + -- MOVEMENT SPEED CONDITIONALS (MonkeySpeed integration) + -- ========================================================================= + -- [moving] - Player is moving (speed > 0) + -- [moving:>100] - Player speed is above 100% (faster than normal run) + -- [moving:<50] - Player speed is below 50% (slower than half speed) + -- Requires MonkeySpeed addon for speed comparisons (basic moving check uses fallback) + moving = function(conditionals) + -- Boolean form [moving] - just check if moving at all + if conditionals.moving == true then + return CleveRoids.IsPlayerMoving() + end + + -- Operator form [moving:>100] - check speed percentage + return Multi(conditionals.moving, function(args) + if type(args) ~= "table" then return false end + + -- Handle multi-comparison (e.g., >50&<150) + if args.comparisons and type(args.comparisons) == "table" then + local speed = CleveRoids.GetPlayerSpeed() + if speed == nil then + CleveRoids.RequireMonkeySpeed("moving speed comparison") + 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](speed, comp.amount) then + return false + end + end + return true + end + + return CleveRoids.ValidateMovingSpeed(args.operator, args.amount) + end, conditionals, "moving") + end, + + -- [nomoving] - Player is NOT moving (speed == 0) + -- [nomoving:>100] - Player speed is NOT above 100% (at or below normal run) + nomoving = function(conditionals) + -- Boolean form [nomoving] - check if NOT moving + if conditionals.nomoving == true then + return not CleveRoids.IsPlayerMoving() + end + + -- Operator form [nomoving:>100] - negate speed comparison + return NegatedMulti(conditionals.nomoving, function(args) + if type(args) ~= "table" then return false end + + if args.comparisons and type(args.comparisons) == "table" then + local speed = CleveRoids.GetPlayerSpeed() + if speed == nil then + -- If MonkeySpeed not available, negated returns true (fail-safe) + 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](speed, comp.amount) then + return true + end + end + return false + end + + return not CleveRoids.ValidateMovingSpeed(args.operator, args.amount) + end, conditionals, "nomoving") + 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, + + -- ======================================================================== + -- AURA CAP CONDITIONALS (Nampower v2.20+ with AURA_CAST events) + -- ======================================================================== + -- Requires NP_EnableAuraCastEvents=1 CVar for accurate tracking + -- Falls back to manual counting if events unavailable + + -- [mybuffcapped] - Player's buff bar is at capacity (32 buffs) + -- Usage: [mybuffcapped] to prevent buff waste + mybuffcapped = function(conditionals) + return CleveRoids.IsPlayerBuffCapped() + end, + + -- [nomybuffcapped] - Player's buff bar has room + nomybuffcapped = function(conditionals) + return not CleveRoids.IsPlayerBuffCapped() + end, + + mybuffcount = function(conditionals) + return Multi(conditionals.mybuffcount,function (v) return CleveRoids.ValidatePlayerAuraCount(v.bigger, v.amount) end, conditionals, "mybuffcount") + end, + + -- [nomybuffcount] - Negated buff count comparison + -- Usage: [nomybuffcount:>15] = true if NOT (buffcount > 15), i.e., buffcount <= 15 + nomybuffcount = function(conditionals) + return NegatedMulti(conditionals.nomybuffcount, function(v) + return not CleveRoids.ValidatePlayerAuraCount(v.bigger, v.amount) + end, conditionals, "nomybuffcount") + end, + + -- [mycc:type] - Check if PLAYER has a specific CC effect + -- Same types as [cc], but always checks the player + -- Examples: [mycc:stun] [mycc:fear/charm] [mycc] (any CC) + mycc = function(conditionals) + -- If no specific CC type, check for ANY loss-of-control on player + if not conditionals.mycc or (type(conditionals.mycc) == "table" and table.getn(conditionals.mycc) == 0) then + return CleveRoids.ValidateUnitAnyCrowdControl("player") + end + + -- Check for specific CC type(s) - OR logic + return Or(conditionals.mycc, function(ccType) + return CleveRoids.ValidateUnitCC("player", ccType) + end) + end, + + -- [nomycc:type] - Check if PLAYER does NOT have a specific CC effect + -- Uses AND logic for negation: [nomycc:stun/fear] = not stunned AND not feared + nomycc = function(conditionals) + -- If no specific CC type, check for NO loss-of-control on player + if not conditionals.nomycc or (type(conditionals.nomycc) == "table" and table.getn(conditionals.nomycc) == 0) then + return not CleveRoids.ValidateUnitAnyCrowdControl("player") + end + + -- Check for absence of specific CC type(s) - AND logic + return NegatedMulti(conditionals.nomycc, function(ccType) + return not CleveRoids.ValidateUnitCC("player", ccType) + end, conditionals, "nomycc") + 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, + + -- [mydebuffcapped] - Player's debuff bar is at capacity (16 debuffs) + mydebuffcapped = function(conditionals) + return CleveRoids.IsPlayerDebuffCapped() + end, + + -- [nomydebuffcapped] - Player's debuff bar has room + nomydebuffcapped = function(conditionals) + return not CleveRoids.IsPlayerDebuffCapped() + 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, + + 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, + + 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, + + 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, + + 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, + + 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, + + 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, + + -- [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, + + -- [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, + + -- [ohimbue] - Check off hand has temporary imbue + -- Same syntax as mhimbue + ohimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.ohimbue, conditionals) + return CleveRoids.ValidateWeaponImbue("oh", args) + end, + + noohimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.noohimbue, conditionals) + return not CleveRoids.ValidateWeaponImbue("oh", args) + 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, + + 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, + + 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, + + 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, + + -- ======================================================================== + -- RESIST TRACKING CONDITIONALS + -- ======================================================================== + + -- [resisted] - Check if last spell was resisted by current target + -- [resisted:type] - Check for specific resist type (full, partial) + -- [resisted:full/partial] - OR logic for multiple types + -- Examples: [resisted] [@target,resisted:full] [resisted:partial] + resisted = function(conditionals) + -- If no specific resist type, check for ANY resist on current target + if not conditionals.resisted or + conditionals.resisted == true or + (type(conditionals.resisted) == "table" and table.getn(conditionals.resisted) == 0) then + return CleveRoids.CheckResistState(nil) + end + + -- Check for specific resist type(s) - OR logic + return Multi(conditionals.resisted, function(resistType) + return CleveRoids.CheckResistState(resistType) + end, conditionals, "resisted") + end, + + -- [noresisted] - Check if last spell was NOT resisted by current target + -- [noresisted:type] - Check target does NOT have specific resist type + -- [noresisted:full/partial] - AND logic: not full AND not partial + noresisted = function(conditionals) + -- If no specific resist type, check for NO resist on current target + if not conditionals.noresisted or + conditionals.noresisted == true or + (type(conditionals.noresisted) == "table" and table.getn(conditionals.noresisted) == 0) then + return not CleveRoids.CheckResistState(nil) + end + + -- Check for absence of specific resist type(s) - AND logic + return NegatedMulti(conditionals.noresisted, function(resistType) + return not CleveRoids.CheckResistState(resistType) + end, conditionals, "noresisted") + end, + + resting = function() + return IsResting() == 1 + end, + + noresting = function() + return IsResting() == nil + 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 == "channeled" + + return CleveRoids.CurrentSpell.type == "cast" or CleveRoids.CurrentSpell.type == "channeled" end, - nochanneled = function(conditionals) - -- Use time-based prediction for accuracy + 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 ~= "channeled" + + return CleveRoids.CurrentSpell.type ~= "cast" and 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 + -- [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, - -- [nochanneltime] - Negated channel time comparison - -- Usage: [nochanneltime:<0.5] = true if NOT (channeltime < 0.5), i.e., >= 0.5 seconds remaining - nochanneltime = function(conditionals) - -- Calculate remaining time (0 if not channeling) - local timeLeft = 0 + -- 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 - if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then - local elapsed = GetTime() - CleveRoids.channelStartTime - timeLeft = CleveRoids.channelDuration - elapsed - if timeLeft < 0 then timeLeft = 0 end - end - - local check = conditionals.nochanneltime - - 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 - return not CleveRoids.comparators[check.operator](timeLeft, check.amount) - end - - return true - 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, - - -- [nocasttime] - Negated cast time comparison - -- Usage: [nocasttime:<0.5] = true if NOT (casttime < 0.5), i.e., >= 0.5 seconds remaining - nocasttime = 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 - if timeLeft < 0 then timeLeft = 0 end - end - - local check = conditionals.nocasttime - - 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 - return not CleveRoids.comparators[check.operator](timeLeft, check.amount) - end - - return true + -- [noslamclip] - True if casting Slam NOW will NOT clip the auto-attack + -- Usage: /cast [noslamclip] Slam + noslamclip = function(conditionals) + return CleveRoids.ValidateNoSlamClip() end, -- [spellcasttime] - Check a spell's total cast time from tooltip (includes haste/talents) @@ -6156,259 +6099,20 @@ CleveRoids.Keywords = { return true end, - -- [targeting:unit] - Target is targeting specified unit - -- [targeting:tank] - Target is targeting ANY player marked as tank in pfUI - targeting = function(conditionals) - local target = conditionals.target or "target" + stance = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + -- PERFORMANCE: Use specialized function to avoid closure allocation + return OrEqualsNumber(conditionals.stance, i) + end, - -- Handle single "tank" keyword directly (most common case) - local val = conditionals.targeting - if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then - return CleveRoids.IsTargetingAnyTank(target) + nostance = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + local forbiddenStances = conditionals.nostance + if type(forbiddenStances) ~= "table" then + return i == 0 end - - return Or(val, function (unit) - if unit == "tank" then - return CleveRoids.IsTargetingAnyTank(target) - end - return (UnitIsUnit(target .. "target", unit) == 1) - end) - end, - - -- [notargeting:unit] - Target is NOT targeting specified unit - -- [notargeting:tank] - Target is NOT targeting ANY tank (loose mob!) - notargeting = function(conditionals) - local target = conditionals.target or "target" - - -- Handle single "tank" keyword directly (most common case) - local val = conditionals.notargeting - if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then - return not CleveRoids.IsTargetingAnyTank(target) - end - - return NegatedMulti(val, function (unit) - if unit == "tank" then - return not CleveRoids.IsTargetingAnyTank(target) - end - return UnitIsUnit(target .. "target", unit) ~= 1 - end, conditionals, "notargeting") - end, - - isplayer = function(conditionals) - return UnitIsPlayer(conditionals.target) - end, - - -- [noisplayer] - Target is NOT a player (same as isnpc) - noisplayer = function(conditionals) - return not UnitIsPlayer(conditionals.target) - end, - - isnpc = function(conditionals) - return not UnitIsPlayer(conditionals.target) - end, - - -- [noisnpc] - Target is NOT an NPC (same as isplayer) - noisnpc = function(conditionals) - return UnitIsPlayer(conditionals.target) - end, - - -- [istank] - Target unit is marked as tank in pfUI - -- [istank:unit] - Specified unit is marked as tank - istank = function(conditionals) - if conditionals.istank and type(conditionals.istank) == "table" then - -- Check specific unit: [istank:focus] - return Or(conditionals.istank, function(unit) - local name = UnitName(unit) - return CleveRoids.IsPlayerTank(name) - end) - end - -- Check target: [istank] - local target = conditionals.target or "target" - local name = UnitName(target) - return CleveRoids.IsPlayerTank(name) - end, - - -- [noistank] - Target unit is NOT marked as tank in pfUI - noistank = function(conditionals) - if conditionals.noistank and type(conditionals.noistank) == "table" then - -- Check specific unit: [noistank:focus] - return NegatedMulti(conditionals.noistank, function(unit) - return not CleveRoids.IsPlayerTank(UnitName(unit)) - end, conditionals, "noistank") - end - -- Check target: [noistank] - local target = conditionals.target or "target" - local name = UnitName(target) - return not CleveRoids.IsPlayerTank(name) - end, - - -- [inrange] or [inrange:Spell] - Target is in range of spell - -- [inrange:Spell>N] - More than N enemies are in range of spell (count mode) - inrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return Multi(conditionals.inrange, function(args) - -- Check for count mode: [inrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - -- Resolve spell range once outside the loop for UnitXP distance checks - -- IsSpellInRange may return stale results during UnitXP target iteration - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance <= spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 1 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - -- Use API wrapper which handles self-cast spells correctly - local result = API.IsSpellInRange(checkValue, target) - return result == 1 - end, conditionals, "inrange") - end, - - -- [noinrange] or [noinrange:Spell] - Target is NOT in range of spell - -- [noinrange:Spell>N] - More than N enemies are NOT in range of spell (count mode) - noinrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return NegatedMulti(conditionals.noinrange, function(args) - -- Check for count mode: [noinrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance > spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 0 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - -- Use API wrapper which handles self-cast spells correctly - local result = API.IsSpellInRange(checkValue, target) - return result == 0 - end, conditionals, "noinrange") - end, - - -- [outrange] or [outrange:Spell] - Target is out of range of spell - -- [outrange:Spell>N] - More than N enemies are out of range of spell (count mode) - outrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return Multi(conditionals.outrange, function(args) - -- Check for count mode: [outrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance > spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 0 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - -- Use API wrapper which handles self-cast spells correctly - local result = API.IsSpellInRange(checkValue, target) - return result == 0 - end, conditionals, "outrange") - end, - - -- [nooutrange] or [nooutrange:Spell] - Target is NOT out of range (same as inrange) - -- [nooutrange:Spell>N] - More than N enemies are NOT out of range (count mode) - nooutrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return NegatedMulti(conditionals.nooutrange, function(args) - -- Check for count mode: [nooutrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance <= spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 1 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - local result = API.IsSpellInRange(checkValue, target) - return result == 1 -- In range = nooutrange passes - end, conditionals, "nooutrange") - 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 + -- PERFORMANCE: Use specialized function to avoid closure allocation + return AndNotEqualsNumber(forbiddenStances, i) end, stat = function(conditionals) @@ -6488,76 +6192,94 @@ CleveRoids.Keywords = { end, conditionals, "nostat") 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) + 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, - 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") + 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, - pet = function(conditionals) - if not UnitExists("pet") then - return false - end + -- Alias for swingtimer + stimer = function(conditionals) + return Multi(conditionals.stimer, function(args) + if type(args) ~= "table" 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, + -- 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 - nopet = function(conditionals) - if not UnitExists("pet") then - return true - end + local attackSpeed = UnitAttackSpeed("player") + if not attackSpeed or attackSpeed <= 0 then return false end - return NegatedMulti(conditionals.nopet, function(petType) - local currentPet = UnitCreatureFamily("pet") - if not currentPet then + 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 string.lower(currentPet) ~= string.lower(petType) - end, conditionals, "nopet") + + return CleveRoids.ValidateSwingTimer(args.operator, args.amount) + end, conditionals, "stimer") + 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, -- [swimming] - Player is currently swimming (Nampower v2.36+) @@ -6584,321 +6306,6 @@ CleveRoids.Keywords = { return PlayerIsSwimming() ~= 1 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] - Player is behind target - -- [behind:>N] - Player is behind more than N enemies (count mode) - behind = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [behind:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.behind) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", "player", unit) == true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("behind", "player", unit) == true - end, - - -- [nobehind] - Player is NOT behind target - -- [nobehind:>N] - Player is NOT behind more than N enemies (count mode) - nobehind = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [nobehind:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.nobehind) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", "player", unit) ~= true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("behind", "player", unit) ~= true - end, - - -- [insight] - Target is in line of sight - -- [insight:>N] - More than N enemies are in line of sight (count mode) - insight = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [insight:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.insight) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("inSight", "player", unit) == true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("inSight", "player", unit) == true - end, - - -- [noinsight] - Target is NOT in line of sight - -- [noinsight:>N] - More than N enemies are NOT in line of sight (count mode) - noinsight = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [noinsight:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.noinsight) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("inSight", "player", unit) ~= true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("inSight", "player", unit) ~= true - end, - - -- [meleerange] - Target is in melee range - -- [meleerange:>N] - More than N enemies are in melee range (count mode) - meleerange = function(conditionals) - -- Check for count mode: [meleerange:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.meleerange) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - if CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit, "meleeAutoAttack") - return distance and distance <= 5 - else - return CheckInteractDistance(unit, 3) - end - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - 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] - Target is NOT in melee range - -- [nomeleerange:>N] - More than N enemies are NOT in melee range (count mode) - nomeleerange = function(conditionals) - -- Check for count mode: [nomeleerange:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.nomeleerange) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - 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) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - 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, - - -- [nomybuffcount] - Negated buff count comparison - -- Usage: [nomybuffcount:>15] = true if NOT (buffcount > 15), i.e., buffcount <= 15 - nomybuffcount = function(conditionals) - return NegatedMulti(conditionals.nomybuffcount, function(v) - return not CleveRoids.ValidatePlayerAuraCount(v.bigger, v.amount) - end, conditionals, "nomybuffcount") - end, - - -- [mhimbue] - Check main hand has temporary imbue (poison, oil, sharpening stone, etc.) - -- [mhimbue:Name] - Check for specific imbue name - -- [mhimbue:<300] - Check imbue time remaining < 300 seconds - -- [mhimbue:Name<300] - Specific imbue with time check - -- [mhimbue:>#5] - Check imbue charges > 5 - mhimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.mhimbue, conditionals) - return CleveRoids.ValidateWeaponImbue("mh", args) - end, - - nomhimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.nomhimbue, conditionals) - return not CleveRoids.ValidateWeaponImbue("mh", args) - end, - - -- [ohimbue] - Check off hand has temporary imbue - -- Same syntax as mhimbue - ohimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.ohimbue, conditionals) - return CleveRoids.ValidateWeaponImbue("oh", args) - end, - - noohimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.noohimbue, conditionals) - return not CleveRoids.ValidateWeaponImbue("oh", args) - 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 - - local isImmune = CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) - return not isImmune - 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) @@ -6940,44 +6347,6 @@ CleveRoids.Keywords = { 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) @@ -7016,42 +6385,11 @@ CleveRoids.Keywords = { 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") + -- 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, -- Threat percentage conditional (reads server data via CHAT_MSG_ADDON) @@ -7107,6 +6445,71 @@ CleveRoids.Keywords = { end, conditionals, "nothreat") 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, + -- Time-To-Kill conditional (requires TimeToKill addon) -- Usage: [ttk:<10] - true if target will die in less than 10 seconds ttk = function(conditionals) @@ -7173,69 +6576,317 @@ CleveRoids.Keywords = { 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 + 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 - return CleveRoids.ValidateTTE(args.operator, args.amount) - end, conditionals, "tte") - end, + -- Check if it's a spell first + local spell = CleveRoids.GetSpell(name) + if spell then + return CleveRoids.CheckSpellUsable(name) + 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 + -- 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 - return not CleveRoids.ValidateTTE(args.operator, args.amount) - end, conditionals, "notte") + -- 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, + + 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, + + -- ======================================================================== + -- UNIT-ID CONDITIONALS + -- ======================================================================== + -- These conditionals check the specified unit (default: current target) + -- and support @unit syntax: [@focus,buff:Power Word: Fortitude] + -- ======================================================================== + + 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, + + -- [behind] - Player is behind target + -- [behind:>N] - Player is behind more than N enemies (count mode) + behind = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [behind:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.behind) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("behind", "player", unit) == true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + local unit = conditionals.target or "target" + if not UnitExists(unit) then return false end + + return UnitXP("behind", "player", unit) == true + end, + + -- [nobehind] - Player is NOT behind target + -- [nobehind:>N] - Player is NOT behind more than N enemies (count mode) + nobehind = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [nobehind:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.nobehind) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("behind", "player", unit) ~= true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + local unit = conditionals.target or "target" + if not UnitExists(unit) then return false end + + return UnitXP("behind", "player", unit) ~= true + 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, + + -- [buffcapped] - Target's buff bar is at capacity (32 buffs) + buffcapped = function(conditionals) + local target = conditionals.target or "target" + return CleveRoids.IsTargetBuffCapped(target) + end, + + -- [nobuffcapped] - Target's buff bar has room + nobuffcapped = function(conditionals) + local target = conditionals.target or "target" + return not CleveRoids.IsTargetBuffCapped(target) + 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, + + -- ======================================================================== + -- CC (Crowd Control) Conditionals - Requires BuffLib for full functionality + -- ======================================================================== + + -- [cc:type] - Check if target has a specific CC effect + -- Types: stun, fear, root, snare/slow, sleep, charm, polymorph, banish, horror, + -- disarm, silence, daze, sap, freeze, knockout/incap, disorient, shackle + -- Special: [cc] or [cc:any] checks for any loss-of-control effect + -- Examples: [cc:stun] [@focus,cc:fear] [cc:stun/fear/root] + cc = function(conditionals) + local target = conditionals.target or "target" + + -- If no specific CC type, check for ANY loss-of-control + if not conditionals.cc or (type(conditionals.cc) == "table" and table.getn(conditionals.cc) == 0) then + return CleveRoids.ValidateUnitAnyCrowdControl(target) + end + + -- Check for specific CC type(s) - OR logic + return Or(conditionals.cc, function(ccType) + return CleveRoids.ValidateUnitCC(target, ccType) + end) + end, + + -- [nocc:type] - Check if target does NOT have a specific CC effect + -- Uses AND logic for negation: [nocc:stun/fear] = not stunned AND not feared + nocc = function(conditionals) + local target = conditionals.target or "target" + + -- If no specific CC type, check for NO loss-of-control + if not conditionals.nocc or (type(conditionals.nocc) == "table" and table.getn(conditionals.nocc) == 0) then + return not CleveRoids.ValidateUnitAnyCrowdControl(target) + end + + -- Check for absence of specific CC type(s) - AND logic (must be missing ALL) + return NegatedMulti(conditionals.nocc, function(ccType) + return not CleveRoids.ValidateUnitCC(target, ccType) + end, conditionals, "nocc") + 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, + + 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. + -- PERFORMANCE: Use event-driven cache for player combat state + local cached = CleveRoids._cachedPlayerInCombat + if cached ~= nil then + return cached + end + -- Fallback if cache not yet initialized + 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. + -- PERFORMANCE: Use event-driven cache for player combat state + local cached = CleveRoids._cachedPlayerInCombat + if cached ~= nil then + return not cached + end + -- Fallback if cache not yet initialized + return not UnitAffectingCombat("player") + end end, -- ========================================================================= @@ -7360,187 +7011,480 @@ CleveRoids.Keywords = { end, conditionals, "nocursive") 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() + dead = function(conditionals) + if not conditionals.target then return false end + return UnitIsDeadOrGhost(conditionals.target) 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() + nodead = function(conditionals) + if not conditionals.target then return false end + return not UnitIsDeadOrGhost(conditionals.target) 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() + debuff = function(conditionals) + return Multi(conditionals.debuff, function(v) + return CleveRoids.ValidateUnitDebuff(conditionals.target, v) + end, conditionals, "debuff") 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() + nodebuff = function(conditionals) + return NegatedMulti(conditionals.nodebuff, function(v) + -- Extract spell name from args (could be string or table with name/operator/amount) + local spellName = type(v) == "table" and v.name or v + + -- Check if this debuff is PENDING (being cast or queued) + -- This prevents double-application when spamming macros with spell queue + if CleveRoids.IsPendingDebuffCast(spellName, conditionals.target) then + return false -- Treat as if debuff exists (nodebuff returns false) + end + + -- Debuff not pending, check if it actually exists on target + return not CleveRoids.ValidateUnitDebuff(conditionals.target, v) + end, conditionals, "nodebuff") end, - -- Checks if the target uses a specific power type (mana, rage, energy) - powertype = function(conditionals) + -- [debuffcapped] - Target's debuff bar is at capacity + -- For NPCs: 16 debuff slots (+ 32 overflow into buff slots = 48 visual total) + -- Usage: [debuffcapped] to stop DoT spam when target is capped + debuffcapped = function(conditionals) + local target = conditionals.target or "target" + return CleveRoids.IsTargetDebuffCapped(target) + end, + + -- [nodebuffcapped] - Target's debuff bar has room + nodebuffcapped = function(conditionals) + local target = conditionals.target or "target" + return not CleveRoids.IsTargetDebuffCapped(target) + 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, + + exists = function(conditionals) + return UnitExists(conditionals.target) + end, + + noexists = function(conditionals) + return not UnitExists(conditionals.target) + end, + + harm = function(conditionals) + return conditionals.harm and conditionals.target and UnitExists(conditionals.target) and UnitCanAttack("player", conditionals.target) + end, + + -- [noharm] - Target is NOT hostile (cannot attack) + noharm = function(conditionals) + if not conditionals.target or not UnitExists(conditionals.target) then + return true + end + return not UnitCanAttack("player", conditionals.target) + end, + + help = function(conditionals) + return conditionals.help and conditionals.target and UnitExists(conditionals.target) and UnitCanAssist("player", conditionals.target) + end, + + -- [nohelp] - Target is NOT friendly (cannot assist) + nohelp = function(conditionals) + if not conditionals.target or not UnitExists(conditionals.target) then + return true + end + return not UnitCanAssist("player", conditionals.target) + 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 or "target", args.operator, args.amount) + end, conditionals, "hp") + 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, + + 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 + + local isImmune = CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) + return not isImmune + end, + + -- [inrange] or [inrange:Spell] - Target is in range of spell + -- [inrange:Spell>N] - More than N enemies are in range of spell (count mode) + inrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return Multi(conditionals.inrange, function(args) + -- Check for count mode: [inrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + -- Resolve spell range once outside the loop for UnitXP distance checks + -- IsSpellInRange may return stale results during UnitXP target iteration + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance <= spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 1 + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + -- Use API wrapper which handles self-cast spells correctly + local result = API.IsSpellInRange(checkValue, target) + return result == 1 + end, conditionals, "inrange") + end, + + -- [noinrange] or [noinrange:Spell] - Target is NOT in range of spell + -- [noinrange:Spell>N] - More than N enemies are NOT in range of spell (count mode) + noinrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return NegatedMulti(conditionals.noinrange, function(args) + -- Check for count mode: [noinrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance > spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 0 + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + -- Use API wrapper which handles self-cast spells correctly + local result = API.IsSpellInRange(checkValue, target) + return result == 0 + end, conditionals, "noinrange") + end, + + -- [insight] - Target is in line of sight + -- [insight:>N] - More than N enemies are in line of sight (count mode) + insight = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [insight:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.insight) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("inSight", "player", unit) == true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior 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) + return UnitXP("inSight", "player", unit) == true end, - -- Checks if the target does NOT use a specific power type - nopowertype = function(conditionals) + -- [noinsight] - Target is NOT in line of sight + -- [noinsight:>N] - More than N enemies are NOT in line of sight (count mode) + noinsight = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [noinsight:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.noinsight) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("inSight", "player", unit) ~= true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + local unit = conditionals.target or "target" + if not UnitExists(unit) then return false end + + return UnitXP("inSight", "player", unit) ~= true + end, + + isnpc = function(conditionals) + return not UnitIsPlayer(conditionals.target) + end, + + -- [noisnpc] - Target is NOT an NPC (same as isplayer) + noisnpc = function(conditionals) + return UnitIsPlayer(conditionals.target) + end, + + isplayer = function(conditionals) + return UnitIsPlayer(conditionals.target) + end, + + -- [noisplayer] - Target is NOT a player (same as isnpc) + noisplayer = function(conditionals) + return not UnitIsPlayer(conditionals.target) + end, + + -- [istank] - Target unit is marked as tank in pfUI + -- [istank:unit] - Specified unit is marked as tank + istank = function(conditionals) + if conditionals.istank and type(conditionals.istank) == "table" then + -- Check specific unit: [istank:focus] + return Or(conditionals.istank, function(unit) + local name = UnitName(unit) + return CleveRoids.IsPlayerTank(name) + end) + end + -- Check target: [istank] + local target = conditionals.target or "target" + local name = UnitName(target) + return CleveRoids.IsPlayerTank(name) + end, + + -- [noistank] - Target unit is NOT marked as tank in pfUI + noistank = function(conditionals) + if conditionals.noistank and type(conditionals.noistank) == "table" then + -- Check specific unit: [noistank:focus] + return NegatedMulti(conditionals.noistank, function(unit) + return not CleveRoids.IsPlayerTank(UnitName(unit)) + end, conditionals, "noistank") + end + -- Check target: [noistank] + local target = conditionals.target or "target" + local name = UnitName(target) + return not CleveRoids.IsPlayerTank(name) + 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 or "target", args.operator, args.amount) + end, conditionals, "level") + end, + + -- [meleerange] - Target is in melee range + -- [meleerange:>N] - More than N enemies are in melee range (count mode) + meleerange = function(conditionals) + -- Check for count mode: [meleerange:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.meleerange) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + if CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit, "meleeAutoAttack") + return distance and distance <= 5 + else + return CheckInteractDistance(unit, 3) + end + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + 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] - Target is NOT in melee range + -- [nomeleerange:>N] - More than N enemies are NOT in melee range (count mode) + nomeleerange = function(conditionals) + -- Check for count mode: [nomeleerange:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.nomeleerange) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + 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) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior 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") + 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, - -- ======================================================================== - -- CC (Crowd Control) Conditionals - Requires BuffLib for full functionality - -- ======================================================================== - - -- [cc:type] - Check if target has a specific CC effect - -- Types: stun, fear, root, snare/slow, sleep, charm, polymorph, banish, horror, - -- disarm, silence, daze, sap, freeze, knockout/incap, disorient, shackle - -- Special: [cc] or [cc:any] checks for any loss-of-control effect - -- Examples: [cc:stun] [@focus,cc:fear] [cc:stun/fear/root] - cc = function(conditionals) - local target = conditionals.target or "target" - - -- If no specific CC type, check for ANY loss-of-control - if not conditionals.cc or (type(conditionals.cc) == "table" and table.getn(conditionals.cc) == 0) then - return CleveRoids.ValidateUnitAnyCrowdControl(target) - end - - -- Check for specific CC type(s) - OR logic - return Or(conditionals.cc, function(ccType) - return CleveRoids.ValidateUnitCC(target, ccType) + member = function(conditionals) + return Or(conditionals.member, function(v) + return + CleveRoids.IsTargetInGroupType(conditionals.target, "party") + or CleveRoids.IsTargetInGroupType(conditionals.target, "raid") end) end, - -- [nocc:type] - Check if target does NOT have a specific CC effect - -- Uses AND logic for negation: [nocc:stun/fear] = not stunned AND not feared - nocc = function(conditionals) - local target = conditionals.target or "target" - - -- If no specific CC type, check for NO loss-of-control - if not conditionals.nocc or (type(conditionals.nocc) == "table" and table.getn(conditionals.nocc) == 0) then - return not CleveRoids.ValidateUnitAnyCrowdControl(target) - end - - -- Check for absence of specific CC type(s) - AND logic (must be missing ALL) - return NegatedMulti(conditionals.nocc, function(ccType) - return not CleveRoids.ValidateUnitCC(target, ccType) - end, conditionals, "nocc") + -- [nomember] - check if target is NOT in party or raid + nomember = function(conditionals) + return not ( + CleveRoids.IsTargetInGroupType(conditionals.target, "party") + or CleveRoids.IsTargetInGroupType(conditionals.target, "raid") + ) end, - -- [mycc:type] - Check if PLAYER has a specific CC effect - -- Same types as [cc], but always checks the player - -- Examples: [mycc:stun] [mycc:fear/charm] [mycc] (any CC) - mycc = function(conditionals) - -- If no specific CC type, check for ANY loss-of-control on player - if not conditionals.mycc or (type(conditionals.mycc) == "table" and table.getn(conditionals.mycc) == 0) then - return CleveRoids.ValidateUnitAnyCrowdControl("player") - end - - -- Check for specific CC type(s) - OR logic - return Or(conditionals.mycc, function(ccType) - return CleveRoids.ValidateUnitCC("player", ccType) - end) + -- [mytag] - target is tapped by the player + mytag = function(conditionals) + return conditionals.target and UnitIsTappedByPlayer(conditionals.target) end, - -- [nomycc:type] - Check if PLAYER does NOT have a specific CC effect - -- Uses AND logic for negation: [nomycc:stun/fear] = not stunned AND not feared - nomycc = function(conditionals) - -- If no specific CC type, check for NO loss-of-control on player - if not conditionals.nomycc or (type(conditionals.nomycc) == "table" and table.getn(conditionals.nomycc) == 0) then - return not CleveRoids.ValidateUnitAnyCrowdControl("player") - end - - -- Check for absence of specific CC type(s) - AND logic - return NegatedMulti(conditionals.nomycc, function(ccType) - return not CleveRoids.ValidateUnitCC("player", ccType) - end, conditionals, "nomycc") - end, - - -- ======================================================================== - -- RESIST TRACKING CONDITIONALS - -- ======================================================================== - - -- [resisted] - Check if last spell was resisted by current target - -- [resisted:type] - Check for specific resist type (full, partial) - -- [resisted:full/partial] - OR logic for multiple types - -- Examples: [resisted] [@target,resisted:full] [resisted:partial] - resisted = function(conditionals) - -- If no specific resist type, check for ANY resist on current target - if not conditionals.resisted or - conditionals.resisted == true or - (type(conditionals.resisted) == "table" and table.getn(conditionals.resisted) == 0) then - return CleveRoids.CheckResistState(nil) - end - - -- Check for specific resist type(s) - OR logic - return Multi(conditionals.resisted, function(resistType) - return CleveRoids.CheckResistState(resistType) - end, conditionals, "resisted") - end, - - -- [noresisted] - Check if last spell was NOT resisted by current target - -- [noresisted:type] - Check target does NOT have specific resist type - -- [noresisted:full/partial] - AND logic: not full AND not partial - noresisted = function(conditionals) - -- If no specific resist type, check for NO resist on current target - if not conditionals.noresisted or - conditionals.noresisted == true or - (type(conditionals.noresisted) == "table" and table.getn(conditionals.noresisted) == 0) then - return not CleveRoids.CheckResistState(nil) - end - - -- Check for absence of specific resist type(s) - AND logic - return NegatedMulti(conditionals.noresisted, function(resistType) - return not CleveRoids.CheckResistState(resistType) - end, conditionals, "noresisted") + -- [nomytag] - target is not tapped by the player + nomytag = function(conditionals) + return conditionals.target and not UnitIsTappedByPlayer(conditionals.target) end, -- ======================================================================== @@ -7603,226 +7547,297 @@ CleveRoids.Keywords = { end, conditionals, "noname") end, - -- ======================================================================== - -- AUTO-ATTACK CONDITIONALS (Nampower v2.24+) - -- ======================================================================== - -- Requires NP_EnableAutoAttackEvents=1 CVar + -- [othertag] - target is tapped by someone else (not the player) + othertag = function(conditionals) + return conditionals.target and UnitIsTapped(conditionals.target) and not UnitIsTappedByPlayer(conditionals.target) + end, - -- [lastswing:type] - Check result of player's last melee swing - -- Types: crit, glancing, miss, dodge, parry, blocked, offhand/oh, mainhand/mh, hit - -- Time check: [lastswing:<2] = last swing was within 2 seconds - -- Multi-value: [lastswing:crit/glancing] = was crit OR glancing (OR logic) - -- Examples: [lastswing:dodge] [@target,lastswing:crit] [lastswing:offhand] - lastswing = function(conditionals) - -- Boolean form [lastswing] - any recent swing - if not conditionals.lastswing or - conditionals.lastswing == true or - (type(conditionals.lastswing) == "table" and table.getn(conditionals.lastswing) == 0) then - return CleveRoids.ValidateLastSwing(nil, nil, nil) - end + -- [noothertag] - target is not tapped by someone else (not tapped, or tapped by player) + noothertag = function(conditionals) + return conditionals.target and (not UnitIsTapped(conditionals.target) or UnitIsTappedByPlayer(conditionals.target)) + end, - -- Check for specific swing type(s) - OR logic - return Or(conditionals.lastswing, function(args) - if type(args) == "string" then - return CleveRoids.ValidateLastSwing(args, nil, nil) - elseif type(args) == "table" then - return CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) + -- [outrange] or [outrange:Spell] - Target is out of range of spell + -- [outrange:Spell>N] - More than N enemies are out of range of spell (count mode) + outrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return Multi(conditionals.outrange, function(args) + -- Check for count mode: [outrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance > spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 0 + end) + return CleveRoids.comparators[args.operator](count, args.amount) end - return false - end) + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + -- Use API wrapper which handles self-cast spells correctly + local result = API.IsSpellInRange(checkValue, target) + return result == 0 + end, conditionals, "outrange") end, - -- [nolastswing:type] - Check that last swing was NOT a specific type - -- Uses AND logic: [nolastswing:crit/glancing] = was NOT crit AND NOT glancing - nolastswing = function(conditionals) - -- Boolean form [nolastswing] - no recent swing - if not conditionals.nolastswing or - conditionals.nolastswing == true or - (type(conditionals.nolastswing) == "table" and table.getn(conditionals.nolastswing) == 0) then - return not CleveRoids.ValidateLastSwing(nil, nil, nil) - end - - -- Check that swing was NOT any of the specified types - AND logic - return NegatedMulti(conditionals.nolastswing, function(args) - if type(args) == "string" then - return not CleveRoids.ValidateLastSwing(args, nil, nil) - elseif type(args) == "table" then - return not CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) + -- [nooutrange] or [nooutrange:Spell] - Target is NOT out of range (same as inrange) + -- [nooutrange:Spell>N] - More than N enemies are NOT out of range (count mode) + nooutrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return NegatedMulti(conditionals.nooutrange, function(args) + -- Check for count mode: [nooutrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance <= spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 1 + end) + return CleveRoids.comparators[args.operator](count, args.amount) end - return true - end, conditionals, "nolastswing") + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + local result = API.IsSpellInRange(checkValue, target) + return result == 1 -- In range = nooutrange passes + end, conditionals, "nooutrange") end, - -- [incominghit:type] - Check result of last attack received by player - -- Types: crit, crushing, glancing, miss, dodge, parry, blocked, hit - -- Time check: [incominghit:<1] = received hit within last 1 second - -- Examples: [incominghit:crushing] [incominghit:crit/crushing] [incominghit:dodge] - incominghit = function(conditionals) - -- Boolean form [incominghit] - any recent incoming hit - if not conditionals.incominghit or - conditionals.incominghit == true or - (type(conditionals.incominghit) == "table" and table.getn(conditionals.incominghit) == 0) then - return CleveRoids.ValidateIncomingHit(nil, nil, nil) + -- [party] or [party:unitid] - check if unit is in your party + -- Default unit is conditionals.target + party = function(conditionals) + local unit = conditionals.party + if unit == true or unit == nil then + unit = conditionals.target end - - -- Check for specific hit type(s) - OR logic - return Or(conditionals.incominghit, function(args) - if type(args) == "string" then - return CleveRoids.ValidateIncomingHit(args, nil, nil) - elseif type(args) == "table" then - return CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) - end - return false - end) + return CleveRoids.IsTargetInGroupType(unit, "party") end, - -- [noincominghit:type] - Check that last incoming hit was NOT a specific type - noincominghit = function(conditionals) - -- Boolean form [noincominghit] - no recent incoming hit - if not conditionals.noincominghit or - conditionals.noincominghit == true or - (type(conditionals.noincominghit) == "table" and table.getn(conditionals.noincominghit) == 0) then - return not CleveRoids.ValidateIncomingHit(nil, nil, nil) + -- [noparty] or [noparty:unitid] - check if unit is NOT in your party + noparty = function(conditionals) + local unit = conditionals.noparty + if unit == true or unit == nil then + unit = conditionals.target end - - -- Check that hit was NOT any of the specified types - AND logic - return NegatedMulti(conditionals.noincominghit, function(args) - if type(args) == "string" then - return not CleveRoids.ValidateIncomingHit(args, nil, nil) - elseif type(args) == "table" then - return not CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) - end - return true - end, conditionals, "noincominghit") + return not CleveRoids.IsTargetInGroupType(unit, "party") end, - -- ======================================================================== - -- AURA CAP CONDITIONALS (Nampower v2.20+ with AURA_CAST events) - -- ======================================================================== - -- Requires NP_EnableAuraCastEvents=1 CVar for accurate tracking - -- Falls back to manual counting if events unavailable - - -- [mybuffcapped] - Player's buff bar is at capacity (32 buffs) - -- Usage: [mybuffcapped] to prevent buff waste - mybuffcapped = function(conditionals) - return CleveRoids.IsPlayerBuffCapped() - end, - - -- [nomybuffcapped] - Player's buff bar has room - nomybuffcapped = function(conditionals) - return not CleveRoids.IsPlayerBuffCapped() - end, - - -- [mydebuffcapped] - Player's debuff bar is at capacity (16 debuffs) - mydebuffcapped = function(conditionals) - return CleveRoids.IsPlayerDebuffCapped() - end, - - -- [nomydebuffcapped] - Player's debuff bar has room - nomydebuffcapped = function(conditionals) - return not CleveRoids.IsPlayerDebuffCapped() - end, - - -- [debuffcapped] - Target's debuff bar is at capacity - -- For NPCs: 16 debuff slots (+ 32 overflow into buff slots = 48 visual total) - -- Usage: [debuffcapped] to stop DoT spam when target is capped - debuffcapped = function(conditionals) - local target = conditionals.target or "target" - return CleveRoids.IsTargetDebuffCapped(target) - end, - - -- [nodebuffcapped] - Target's debuff bar has room - nodebuffcapped = function(conditionals) - local target = conditionals.target or "target" - return not CleveRoids.IsTargetDebuffCapped(target) - end, - - -- [buffcapped] - Target's buff bar is at capacity (32 buffs) - buffcapped = function(conditionals) - local target = conditionals.target or "target" - return CleveRoids.IsTargetBuffCapped(target) - end, - - -- [nobuffcapped] - Target's buff bar has room - nobuffcapped = function(conditionals) - local target = conditionals.target or "target" - return not CleveRoids.IsTargetBuffCapped(target) - end, - - -- ========================================================================= - -- MOVEMENT SPEED CONDITIONALS (MonkeySpeed integration) - -- ========================================================================= - -- [moving] - Player is moving (speed > 0) - -- [moving:>100] - Player speed is above 100% (faster than normal run) - -- [moving:<50] - Player speed is below 50% (slower than half speed) - -- Requires MonkeySpeed addon for speed comparisons (basic moving check uses fallback) - moving = function(conditionals) - -- Boolean form [moving] - just check if moving at all - if conditionals.moving == true then - return CleveRoids.IsPlayerMoving() - end - - -- Operator form [moving:>100] - check speed percentage - return Multi(conditionals.moving, function(args) + power = function(conditionals) + return Multi(conditionals.power, function(args) if type(args) ~= "table" then return false end - -- Handle multi-comparison (e.g., >50&<150) + -- Handle multi-comparison (e.g., >50&<80) if args.comparisons and type(args.comparisons) == "table" then - local speed = CleveRoids.GetPlayerSpeed() - if speed == nil then - CleveRoids.RequireMonkeySpeed("moving speed comparison") - return false - end + 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](speed, comp.amount) then + if not CleveRoids.comparators[comp.operator](powerPercent, comp.amount) then return false end end return true end - return CleveRoids.ValidateMovingSpeed(args.operator, args.amount) - end, conditionals, "moving") + return CleveRoids.ValidatePower(conditionals.target or "target", args.operator, args.amount) + end, conditionals, "power") end, - -- [nomoving] - Player is NOT moving (speed == 0) - -- [nomoving:>100] - Player speed is NOT above 100% (at or below normal run) - nomoving = function(conditionals) - -- Boolean form [nomoving] - check if NOT moving - if conditionals.nomoving == true then - return not CleveRoids.IsPlayerMoving() - end - - -- Operator form [nomoving:>100] - negate speed comparison - return NegatedMulti(conditionals.nomoving, function(args) + 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, - if args.comparisons and type(args.comparisons) == "table" then - local speed = CleveRoids.GetPlayerSpeed() - if speed == nil then - -- If MonkeySpeed not available, negated returns true (fail-safe) - return true - 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 - for _, comp in ipairs(args.comparisons) do - if not CleveRoids.operators[comp.operator] then - return true - end - if not CleveRoids.comparators[comp.operator](speed, comp.amount) then - return true - end - end - return false + 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 not CleveRoids.ValidateMovingSpeed(args.operator, args.amount) - end, conditionals, "nomoving") - 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, + + -- [raid] or [raid:unitid] - check if unit is in your raid + -- Default unit is conditionals.target + raid = function(conditionals) + local unit = conditionals.raid + if unit == true or unit == nil then + unit = conditionals.target + end + return CleveRoids.IsTargetInGroupType(unit, "raid") + end, + + -- [noraid] or [noraid:unitid] - check if unit is NOT in your raid + noraid = function(conditionals) + local unit = conditionals.noraid + if unit == true or unit == nil then + unit = conditionals.target + end + return not CleveRoids.IsTargetInGroupType(unit, "raid") + end, + + rawhp = function(conditionals) + return Multi(conditionals.rawhp, function(args) + if type(args) ~= "table" then return false end + return CleveRoids.ValidateRawHp(conditionals.target or "target", args.operator, args.amount) + end, conditionals, "rawhp") + 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 or "target", args.operator, args.amount) + end, conditionals, "rawpower") + end, + + -- [tag] - target is tapped (tagged) by anyone + tag = function(conditionals) + return conditionals.target and UnitIsTapped(conditionals.target) + end, + + -- [notag] - target is not tapped + notag = function(conditionals) + return conditionals.target and not UnitIsTapped(conditionals.target) + end, + + target = function(conditionals) + return CleveRoids.IsValidTarget(conditionals.target, conditionals.help) + end, + + -- [targeting:unit] - Target is targeting specified unit + -- [targeting:tank] - Target is targeting ANY player marked as tank in pfUI + targeting = function(conditionals) + local target = conditionals.target or "target" + + -- Handle single "tank" keyword directly (most common case) + local val = conditionals.targeting + if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then + return CleveRoids.IsTargetingAnyTank(target) + end + + return Or(val, function (unit) + if unit == "tank" then + return CleveRoids.IsTargetingAnyTank(target) + end + return (UnitIsUnit(target .. "target", unit) == 1) + end) + end, + + -- [notargeting:unit] - Target is NOT targeting specified unit + -- [notargeting:tank] - Target is NOT targeting ANY tank (loose mob!) + notargeting = function(conditionals) + local target = conditionals.target or "target" + + -- Handle single "tank" keyword directly (most common case) + local val = conditionals.notargeting + if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then + return not CleveRoids.IsTargetingAnyTank(target) + end + + return NegatedMulti(val, function (unit) + if unit == "tank" then + return not CleveRoids.IsTargetingAnyTank(target) + end + return UnitIsUnit(target .. "target", unit) ~= 1 + end, conditionals, "notargeting") + 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, + } -- ============================================================================= diff --git a/README.md b/README.md index 299ab2d..6effe24 100644 --- a/README.md +++ b/README.md @@ -154,63 +154,56 @@ These always evaluate against the player. Cannot be redirected with `@unit`. | Conditional | Description | No | Multi | Example | NP | Addon | |-------------|-------------|:--:|:-----:|---------|:--:|-------| -| ***State & Modifiers*** | | | | | | | -| `mod` | Modifier key pressed | ✅ | ✅ | `[mod:alt/ctrl]` | | | -| `combat` | In combat (player default, or unit) | ✅ | ✅ | `[combat]` `[combat:target]` | | | -| `form` / `stance` | Shapeshift form or stance index | ✅ | ✅ | `[form:1/3]` `[stance:2]` | | | -| `stealth` | In stealth (Rogue/Druid) | ✅ | — | `[stealth]` | | | -| `group` | Player is in group type | ✅ | ✅ | `[group:party/raid]` | | | -| `resting` | In rest area | ✅ | — | `[resting]` | | | -| `swimming` | Swimming (Druid only) | ✅ | — | `[swimming]` | | | -| `moving` | Moving / speed % | ✅ | ✅ | `[moving]` `[moving:>100&<200]` | | MonkeySpeed (speed %) | -| `zone` | Current zone name | ✅ | ✅ | `[zone:Ironforge/Stormwind]` | | | -| ***Resources*** | | | | | | | -| `myhp` | Player HP % | — | ✅ | `[myhp:<30]` `[myhp:>20&<50]` | | | -| `myrawhp` | Player raw HP value | — | ✅ | `[myrawhp:>1000]` | | | -| `myhplost` | Player HP deficit (max - current) | — | ✅ | `[myhplost:>500]` | | | -| `mypower` | Player mana/rage/energy % | — | ✅ | `[mypower:>50]` | | | -| `myrawpower` | Player raw power value | — | ✅ | `[myrawpower:>=40]` | | | -| `mypowerlost` | Player power deficit | — | ✅ | `[mypowerlost:>200]` | | | -| `druidmana` | Druid mana while shapeshifted | — | ✅ | `[druidmana:>=500]` | | | -| `combo` | Combo points | ✅ | ✅ | `[combo:>=4]` | | | -| `stat` | Player stat value | ✅ | ✅ | `[stat:agi>100]` `[stat:ap>1000]` | | | -| `mylevel` | Player level | — | ✅ | `[mylevel:=60]` | | | -| ***Player Auras*** | | | | | | | -| `mybuff` | Player has buff (with time/stacks) | ✅ | ✅ | `[mybuff:Thorns<5]` `[nomybuff:MotW/GotW]` | | | -| `mydebuff` | Player has debuff (with time/stacks) | ✅ | ✅ | `[mydebuff:Curse_of_Agony]` | | | -| `mybuffcount` | Player buff slot count (32 max) | ✅ | ✅ | `[mybuffcount:>15]` `[nomybuffcount:>28]` | | | -| `mybuffcapped` | Player buff bar full (32) | ✅ | — | `[nomybuffcapped]` | v2.20 | | -| `mydebuffcapped` | Player debuff bar full (16) | ✅ | — | `[nomydebuffcapped]` | v2.20 | | -| `mycc` | Player has CC effect | ✅ | ✅ | `[mycc:stun/silence]` `[nomycc]` | | | -| ***Spells & Cooldowns*** | | | | | | | -| `cooldown` | Spell/item CD remaining (ignores GCD) | ✅ | ✅ | `[cooldown:Sprint<5]` | | | | `cdgcd` | Spell/item CD remaining (includes GCD) | ✅ | ✅ | `[cdgcd:Sprint>0]` | | | -| `gcd` | GCD is active / time remaining | ✅ | ✅ | `[gcd]` `[gcd:<1]` | | | -| `usable` | Spell/item is usable now | ✅ | ✅ | `[usable:Overpower]` | | | -| `reactive` | Reactive ability available (on bar) | ✅ | ✅ | `[reactive:Overpower]` | | | -| `known` | Spell/talent known (with rank) | ✅ | ✅ | `[known:Berserk]` `[known:Frostbolt>#2]` | | | -| `selfcasting` | Player is casting or channeling | ✅ | ✅ | `[selfcasting]` `[noselfcasting:Hearthstone]` | | | -| `checkcasting` | True if NOT casting (specific spell) | — | ✅ | `[checkcasting]` `[checkcasting:Frostbolt]` | | | | `channeled` | Player is channeling | ✅ | ✅ | `[channeled]` `[channeled:Arcane_Missiles]` | | | +| `checkcasting` | True if NOT casting (specific spell) | — | ✅ | `[checkcasting]` `[checkcasting:Frostbolt]` | | | | `checkchanneled` | True if NOT channeling (specific spell) | — | ✅ | `[checkchanneled]` `[checkchanneled:Evocation]` | | | -| `spellcasttime` | Spell cast time from tooltip (real-time) | ✅ | ✅ | `[spellcasttime:>1.5]` `[spellcasttime:Frostbolt<2]` | | | -| `queuedspell` | Spell is queued | ✅ | ✅ | `[queuedspell]` `[queuedspell:Fireball]` | v2.12 | | -| `onswingpending` | On-swing spell pending | ✅ | — | `[onswingpending]` | v2.12 | | -| ***Equipment*** | | | | | | | +| `combat` | In combat (player default, or unit) | ✅ | ✅ | `[combat]` `[combat:target]` | | | +| `combo` | Combo points | ✅ | ✅ | `[combo:>=4]` | | | +| `cooldown` | Spell/item CD remaining (ignores GCD) | ✅ | ✅ | `[cooldown:Sprint<5]` | | | +| `druidmana` | Druid mana while shapeshifted | — | ✅ | `[druidmana:>=500]` | | | | `equipped` | Item or weapon type equipped | ✅ | ✅ | `[equipped:Daggers/Swords]` | | | -| `mhimbue` | Main hand has temporary imbue | ✅ | ✅ | `[mhimbue:Instant_Poison<300]` `[mhimbue:>#5]` | | | -| `ohimbue` | Off hand has temporary imbue | ✅ | ✅ | `[noohimbue]` `[ohimbue:Crippling_Poison]` | | | -| `pet` | Has active pet (with family) | ✅ | ✅ | `[pet]` `[pet:Cat/Wolf]` | | | -| ***Melee Tracking*** | | | | | | | -| `lastswing` | Player melee swing type/timing | ✅ | ✅ | `[lastswing:dodge]` `[lastswing:<2]` | v2.24 | | -| `incominghit` | Incoming attack type/timing | ✅ | ✅ | `[incominghit:crushing]` `[noincominghit:crit]` | v2.24 | | -| `swingtimer` | Swing timer % elapsed | ✅ | ✅ | `[swingtimer:>80]` | | SP_SwingTimer | -| `stimer` | Alias for swingtimer | ✅ | ✅ | `[stimer:<15]` | | SP_SwingTimer | -| `slamclip` / `noslamclip` | Slam now will/won't clip auto-attack | ✅ | — | `[noslamclip]` | | SP_SwingTimer | -| `nextslamclip` / `nonextslamclip` | Instant now will/won't cause next Slam to clip | ✅ | — | `[nonextslamclip]` | | SP_SwingTimer | -| ***Target Existence*** | | | | | | | +| `form` / `stance` | Shapeshift form or stance index | ✅ | ✅ | `[form:1/3]` `[stance:2]` | | | +| `gcd` | GCD is active / time remaining | ✅ | ✅ | `[gcd]` `[gcd:<1]` | | | +| `group` | Player is in group type | ✅ | ✅ | `[group:party/raid]` | | | | `hastarget` | Player has a target | — | — | `[hastarget]` | | | +| `incominghit` | Incoming attack type/timing | ✅ | ✅ | `[incominghit:crushing]` `[noincominghit:crit]` | v2.24 | | +| `known` | Spell/talent known (with rank) | ✅ | ✅ | `[known:Berserk]` `[known:Frostbolt>#2]` | | | +| `lastswing` | Player melee swing type/timing | ✅ | ✅ | `[lastswing:dodge]` `[lastswing:<2]` | v2.24 | | +| `mhimbue` | Main hand has temporary imbue | ✅ | ✅ | `[mhimbue:Instant_Poison<300]` `[mhimbue:>#5]` | | | +| `mod` | Modifier key pressed | ✅ | ✅ | `[mod:alt/ctrl]` | | | +| `moving` | Moving / speed % | ✅ | ✅ | `[moving]` `[moving:>100&<200]` | | MonkeySpeed (speed %) | +| `mybuff` | Player has buff (with time/stacks) | ✅ | ✅ | `[mybuff:Thorns<5]` `[nomybuff:MotW/GotW]` | | | +| `mybuffcapped` | Player buff bar full (32) | ✅ | — | `[nomybuffcapped]` | v2.20 | | +| `mybuffcount` | Player buff slot count (32 max) | ✅ | ✅ | `[mybuffcount:>15]` `[nomybuffcount:>28]` | | | +| `mycc` | Player has CC effect | ✅ | ✅ | `[mycc:stun/silence]` `[nomycc]` | | | +| `mydebuff` | Player has debuff (with time/stacks) | ✅ | ✅ | `[mydebuff:Curse_of_Agony]` | | | +| `mydebuffcapped` | Player debuff bar full (16) | ✅ | — | `[nomydebuffcapped]` | v2.20 | | +| `myhp` | Player HP % | — | ✅ | `[myhp:<30]` `[myhp:>20&<50]` | | | +| `myhplost` | Player HP deficit (max - current) | — | ✅ | `[myhplost:>500]` | | | +| `mylevel` | Player level | — | ✅ | `[mylevel:=60]` | | | +| `mypower` | Player mana/rage/energy % | — | ✅ | `[mypower:>50]` | | | +| `mypowerlost` | Player power deficit | — | ✅ | `[mypowerlost:>200]` | | | +| `myrawhp` | Player raw HP value | — | ✅ | `[myrawhp:>1000]` | | | +| `myrawpower` | Player raw power value | — | ✅ | `[myrawpower:>=40]` | | | +| `nextslamclip` / `nonextslamclip` | Instant now will/won't cause next Slam to clip | ✅ | — | `[nonextslamclip]` | | SP_SwingTimer | | `notarget` | Player has no target | — | — | `[notarget]` | | | +| `ohimbue` | Off hand has temporary imbue | ✅ | ✅ | `[noohimbue]` `[ohimbue:Crippling_Poison]` | | | +| `onswingpending` | On-swing spell pending | ✅ | — | `[onswingpending]` | v2.12 | | +| `pet` | Has active pet (with family) | ✅ | ✅ | `[pet]` `[pet:Cat/Wolf]` | | | +| `queuedspell` | Spell is queued | ✅ | ✅ | `[queuedspell]` `[queuedspell:Fireball]` | v2.12 | | +| `reactive` | Reactive ability available (on bar) | ✅ | ✅ | `[reactive:Overpower]` | | | +| `resting` | In rest area | ✅ | — | `[resting]` | | | +| `selfcasting` | Player is casting or channeling | ✅ | ✅ | `[selfcasting]` `[noselfcasting:Hearthstone]` | | | +| `slamclip` / `noslamclip` | Slam now will/won't clip auto-attack | ✅ | — | `[noslamclip]` | | SP_SwingTimer | +| `spellcasttime` | Spell cast time from tooltip (real-time) | ✅ | ✅ | `[spellcasttime:>1.5]` `[spellcasttime:Frostbolt<2]` | | | +| `stat` | Player stat value | ✅ | ✅ | `[stat:agi>100]` `[stat:ap>1000]` | | | +| `stealth` | In stealth (Rogue/Druid) | ✅ | — | `[stealth]` | | | +| `stimer` | Alias for swingtimer | ✅ | ✅ | `[stimer:<15]` | | SP_SwingTimer | +| `swimming` | Swimming (Druid only) | ✅ | — | `[swimming]` | | | +| `swingtimer` | Swing timer % elapsed | ✅ | ✅ | `[swingtimer:>80]` | | SP_SwingTimer | +| `usable` | Spell/item is usable now | ✅ | ✅ | `[usable:Overpower]` | | | +| `zone` | Current zone name | ✅ | ✅ | `[zone:Ironforge/Stormwind]` | | | ### Target Conditionals (default @target) @@ -218,62 +211,53 @@ These default to checking the current target. Most can be redirected with `@unit | Conditional | Description | No | Multi | Example | NP | Addon | |-------------|-------------|:--:|:-----:|---------|:--:|-------| -| ***Existence & Disposition*** | | | | | | | -| `exists` | Unit exists | ✅ | — | `[@mouseover,exists]` | | | | `alive` | Unit is alive | ✅ | — | `[alive]` | | | -| `dead` | Unit is dead | ✅ | — | `[dead]` | | | -| `help` | Unit is friendly | ✅ | — | `[help]` | | | -| `harm` | Unit is hostile | ✅ | — | `[harm]` | | | -| `isplayer` | Unit is a player | ✅ | — | `[isplayer]` | | | -| `isnpc` | Unit is an NPC | ✅ | — | `[isnpc]` | | | -| ***Resources*** | | | | | | | -| `hp` | Target HP % | — | ✅ | `[hp:<20]` `[hp:>30&<70]` | | | -| `rawhp` | Target raw HP value | — | ✅ | `[rawhp:>5000]` | | | -| `hplost` | Target HP deficit (max - current) | — | ✅ | `[hplost:>1000]` | | | -| `power` | Target mana/rage/energy % | — | ✅ | `[power:<30]` | | | -| `rawpower` | Target raw power value | — | ✅ | `[rawpower:>500]` | | | -| `powerlost` | Target power deficit | — | ✅ | `[powerlost:>100]` | | | -| `powertype` | Target power type | ✅ | ✅ | `[powertype:mana/rage]` | | | -| `level` | Target level (skull = 63) | — | ✅ | `[level:>60]` | | | -| ***Classification*** | | | | | | | -| `class` | Target class (players only) | ✅ | ✅ | `[class:Warrior/Priest]` | | | -| `type` | Creature type | ✅ | ✅ | `[type:Undead/Beast]` | | | -| `name` | Exact name match (case-insensitive) | ✅ | ✅ | `[name:Onyxia]` `[noname:Critter/Dummy]` | | | -| ***Target Auras*** | | | | | | | +| `behind` | Behind target / count behind | ✅ | ✅ | `[behind]` `[behind:>=2]` | | UnitXP_SP3 | | `buff` | Target has buff (with time/stacks) | ✅ | ✅ | `[buff:Shield>#3]` `[nobuff:Renew/Rejuv]` | | | -| `debuff` | Target has debuff (with time/stacks) | ✅ | ✅ | `[debuff:Moonfire<4]` `[nodebuff:Rake]` | | | | `buffcapped` | Target buff bar full (32) | ✅ | — | `[nobuffcapped]` | v2.20 | | -| `debuffcapped` | Target debuff bar full | ✅ | — | `[nodebuffcapped]` | v2.20 | | -| `cc` | Target has CC effect | ✅ | ✅ | `[cc:stun/fear]` `[nocc:polymorph]` | | | -| `cursive` | GUID-based debuff tracking (time remaining) | ✅ | ✅ | `[cursive:Rake<3]` `[nocursive:Rip]` | | Cursive | -| ***Casting*** | | | | | | | | `casting` | Target is casting (specific spell) | ✅ | ✅ | `[casting]` `[casting:Heal]` | | | | `casttime` | Target cast time remaining (seconds) | ✅ | ✅ | `[casttime:<0.5]` | | | +| `cc` | Target has CC effect | ✅ | ✅ | `[cc:stun/fear]` `[nocc:polymorph]` | | | | `channeltime` | Target channel time remaining (seconds) | ✅ | ✅ | `[channeltime:<0.5]` | | | -| ***Relationship*** | | | | | | | -| `party` | Unit is in your party | ✅ | ✅ | `[party]` `[party:focus]` | | | -| `raid` | Unit is in your raid | ✅ | ✅ | `[raid]` `[raid:mouseover]` | | | -| `member` | Target is in party or raid | ✅ | — | `[member]` | | | -| `targeting` | Unit is targeting you or a tank | ✅ | ✅ | `[targeting:player]` `[notargeting:tank]` | | pfUI (for tank) | -| `istank` | Unit is marked as tank | ✅ | — | `[istank]` `[@focus,istank]` | | pfUI | -| `tag` | Target is tapped by anyone | ✅ | — | `[tag]` `[notag]` | | | -| `mytag` | Target is tapped by you | ✅ | — | `[mytag]` `[nomytag]` | | | -| `othertag` | Target is tapped by someone else | ✅ | — | `[othertag]` `[noothertag]` | | | -| ***Range & Position*** | | | | | | | +| `class` | Target class (players only) | ✅ | ✅ | `[class:Warrior/Priest]` | | | +| `cursive` | GUID-based debuff tracking (time remaining) | ✅ | ✅ | `[cursive:Rake<3]` `[nocursive:Rip]` | | Cursive | +| `dead` | Unit is dead | ✅ | — | `[dead]` | | | +| `debuff` | Target has debuff (with time/stacks) | ✅ | ✅ | `[debuff:Moonfire<4]` `[nodebuff:Rake]` | | | +| `debuffcapped` | Target debuff bar full | ✅ | — | `[nodebuffcapped]` | v2.20 | | | `distance` | Distance in yards | ✅ | ✅ | `[distance:<40]` | | UnitXP_SP3 | -| `meleerange` | In melee range / count in melee | ✅ | ✅ | `[meleerange]` `[meleerange:>1]` | | UnitXP_SP3 | -| `inrange` | In spell range / count in range | ✅ | ✅ | `[inrange:Charge]` `[inrange:Multi-Shot>1]` | | UnitXP_SP3 (count) | -| `outrange` | Out of spell range / count out | ✅ | ✅ | `[outrange:Charge]` `[outrange:Charge>0]` | | UnitXP_SP3 (count) | -| `insight` | In line of sight / count in LoS | ✅ | ✅ | `[insight]` `[insight:>0]` | | UnitXP_SP3 | -| `behind` | Behind target / count behind | ✅ | ✅ | `[behind]` `[behind:>=2]` | | UnitXP_SP3 | -| `multiscan` | Scan enemies by priority, soft-cast | — | ✅ | `[multiscan:nearest]` `[multiscan:skull]` | | UnitXP_SP3 | -| ***Immunity*** | | | | | | | +| `exists` | Unit exists | ✅ | — | `[@mouseover,exists]` | | | +| `harm` | Unit is hostile | ✅ | — | `[harm]` | | | +| `help` | Unit is friendly | ✅ | — | `[help]` | | | +| `hp` | Target HP % | — | ✅ | `[hp:<20]` `[hp:>30&<70]` | | | +| `hplost` | Target HP deficit (max - current) | — | ✅ | `[hplost:>1000]` | | | | `immune` | Target is immune (school or CC type) | ✅ | ✅ | `[noimmune:fire]` `[noimmune:stun]` | | | +| `inrange` | In spell range / count in range | ✅ | ✅ | `[inrange:Charge]` `[inrange:Multi-Shot>1]` | | UnitXP_SP3 (count) | +| `insight` | In line of sight / count in LoS | ✅ | ✅ | `[insight]` `[insight:>0]` | | UnitXP_SP3 | +| `isnpc` | Unit is an NPC | ✅ | — | `[isnpc]` | | | +| `isplayer` | Unit is a player | ✅ | — | `[isplayer]` | | | +| `istank` | Unit is marked as tank | ✅ | — | `[istank]` `[@focus,istank]` | | pfUI | +| `level` | Target level (skull = 63) | — | ✅ | `[level:>60]` | | | +| `meleerange` | In melee range / count in melee | ✅ | ✅ | `[meleerange]` `[meleerange:>1]` | | UnitXP_SP3 | +| `member` | Target is in party or raid | ✅ | — | `[member]` | | | +| `multiscan` | Scan enemies by priority, soft-cast | — | ✅ | `[multiscan:nearest]` `[multiscan:skull]` | | UnitXP_SP3 | +| `mytag` | Target is tapped by you | ✅ | — | `[mytag]` `[nomytag]` | | | +| `name` | Exact name match (case-insensitive) | ✅ | ✅ | `[name:Onyxia]` `[noname:Critter/Dummy]` | | | +| `othertag` | Target is tapped by someone else | ✅ | — | `[othertag]` `[noothertag]` | | | +| `outrange` | Out of spell range / count out | ✅ | ✅ | `[outrange:Charge]` `[outrange:Charge>0]` | | UnitXP_SP3 (count) | +| `party` | Unit is in your party | ✅ | ✅ | `[party]` `[party:focus]` | | | +| `power` | Target mana/rage/energy % | — | ✅ | `[power:<30]` | | | +| `powerlost` | Target power deficit | — | ✅ | `[powerlost:>100]` | | | +| `powertype` | Target power type | ✅ | ✅ | `[powertype:mana/rage]` | | | +| `raid` | Unit is in your raid | ✅ | ✅ | `[raid]` `[raid:mouseover]` | | | +| `rawhp` | Target raw HP value | — | ✅ | `[rawhp:>5000]` | | | +| `rawpower` | Target raw power value | — | ✅ | `[rawpower:>500]` | | | | `resisted` | Last spell was resisted | ✅ | ✅ | `[resisted]` `[resisted:full/partial]` | | | -| ***Addon Integrations*** | | | | | | | +| `tag` | Target is tapped by anyone | ✅ | — | `[tag]` `[notag]` | | | +| `targeting` | Unit is targeting you or a tank | ✅ | ✅ | `[targeting:player]` `[notargeting:tank]` | | pfUI (for tank) | | `threat` | Threat % on target (100 = pull) | ✅ | ✅ | `[threat:>80]` | | TWThreat | -| `ttk` | Time to kill target (seconds) | ✅ | ✅ | `[ttk:<10]` | | TimeToKill | | `tte` | Time to execute threshold (seconds) | ✅ | ✅ | `[tte:<5]` | | TimeToKill | +| `ttk` | Time to kill target (seconds) | ✅ | ✅ | `[ttk:<10]` | | TimeToKill | +| `type` | Creature type | ✅ | ✅ | `[type:Undead/Beast]` | | | ### Multi-Unit Count Mode diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index afadf78..de63299 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -2,7 +2,7 @@ ## Author: DWG, _brain, Torio, yani9o, Avitasia ## Title: [Super]CleveRoidMacros ## Notes: /cleveroid for settings -## Version: 2.0 +## Version: 2.1 ## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar, Cursive, XPerl, LunaUnitFrames, DragonflightReloaded, -Dragonflight3 ## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools Localization.lua From f6585bb84bd9f5476227297c5f0aad8700e22b0b Mon Sep 17 00:00:00 2001 From: Torio <41833378+jrc13245@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:03:45 -0500 Subject: [PATCH 12/12] revert alphabetize conditionals table --- Conditionals.lua | 4019 +++++++++++++++++++++++----------------------- 1 file changed, 2002 insertions(+), 2017 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index 95db11e..dbab3c9 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -5012,202 +5012,229 @@ end -- A list of Conditionals and their functions to validate them CleveRoids.Keywords = { + exists = function(conditionals) + return UnitExists(conditionals.target) + end, - -- ======================================================================== - -- PLAYER-ONLY CONDITIONALS - -- ======================================================================== - -- These conditionals always check player state and are not affected by @unit - -- ======================================================================== + noexists = function(conditionals) + return not UnitExists(conditionals.target) + end, - casttime = function(conditionals) - -- Calculate remaining time (0 if not casting) - local timeLeft = 0 + -- 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, + + -- [nohelp] - Target is NOT friendly (cannot assist) + nohelp = function(conditionals) + if not conditionals.target or not UnitExists(conditionals.target) then + return true + end + return not UnitCanAssist("player", conditionals.target) + end, + + harm = function(conditionals) + return conditionals.harm and conditionals.target and UnitExists(conditionals.target) and UnitCanAttack("player", conditionals.target) + end, + + -- [noharm] - Target is NOT hostile (cannot attack) + noharm = function(conditionals) + if not conditionals.target or not UnitExists(conditionals.target) then + return true + end + return not UnitCanAttack("player", conditionals.target) + end, + + stance = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + -- PERFORMANCE: Use specialized function to avoid closure allocation + return OrEqualsNumber(conditionals.stance, i) + end, + + nostance = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + local forbiddenStances = conditionals.nostance + if type(forbiddenStances) ~= "table" then + return i == 0 + end + -- PERFORMANCE: Use specialized function to avoid closure allocation + return AndNotEqualsNumber(forbiddenStances, i) + end, + + noform = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + local forbiddenForms = conditionals.noform + if type(forbiddenForms) ~= "table" then + return i == 0 + end + -- PERFORMANCE: Use specialized function to avoid closure allocation + return AndNotEqualsNumber(forbiddenForms, i) + end, + + form = function(conditionals) + local i = CleveRoids.GetCurrentShapeshiftIndex() + -- PERFORMANCE: Use specialized function to avoid closure allocation + return OrEqualsNumber(conditionals.form, i) + 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. + -- PERFORMANCE: Use event-driven cache for player combat state + local cached = CleveRoids._cachedPlayerInCombat + if cached ~= nil then + return cached + end + -- Fallback if cache not yet initialized + 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. + -- PERFORMANCE: Use event-driven cache for player combat state + local cached = CleveRoids._cachedPlayerInCombat + if cached ~= nil then + return not cached + end + -- Fallback if cache not yet initialized + 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 elapsed = GetTime() - CleveRoids.castStartTime - timeLeft = CleveRoids.castDuration - elapsed - -- Don't allow negative time - if timeLeft < 0 then timeLeft = 0 end + local remaining = CleveRoids.castDuration - (GetTime() - CleveRoids.castStartTime) + if remaining <= 0.1 then + return false -- Cast is done + 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, - - -- [nocasttime] - Negated cast time comparison - -- Usage: [nocasttime:<0.5] = true if NOT (casttime < 0.5), i.e., >= 0.5 seconds remaining - nocasttime = 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 - if timeLeft < 0 then timeLeft = 0 end - end - - local check = conditionals.nocasttime - - 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 - return not CleveRoids.comparators[check.operator](timeLeft, check.amount) - end - - return true - 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 + -- 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 == "channeled" + + return CleveRoids.CurrentSpell.type == "cast" or CleveRoids.CurrentSpell.type == "channeled" end, - nochanneled = function(conditionals) - -- Use time-based prediction for accuracy + 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 ~= "channeled" + + return CleveRoids.CurrentSpell.type ~= "cast" and 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 + 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, - -- [nochanneltime] - Negated channel time comparison - -- Usage: [nochanneltime:<0.5] = true if NOT (channeltime < 0.5), i.e., >= 0.5 seconds remaining - nochanneltime = 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 - if timeLeft < 0 then timeLeft = 0 end - end - - local check = conditionals.nochanneltime - - 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 - return not CleveRoids.comparators[check.operator](timeLeft, check.amount) - end - - return true - 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, - - 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, - - 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, - - 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, - - 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") + 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) @@ -5258,20 +5285,617 @@ CleveRoids.Keywords = { end) end, - form = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - -- PERFORMANCE: Use specialized function to avoid closure allocation - return OrEqualsNumber(conditionals.form, i) + dead = function(conditionals) + if not conditionals.target then return false end + return UnitIsDeadOrGhost(conditionals.target) end, - noform = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - local forbiddenForms = conditionals.noform - if type(forbiddenForms) ~= "table" then - return i == 0 + 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, + + -- [nomember] - check if target is NOT in party or raid + nomember = function(conditionals) + return not ( + CleveRoids.IsTargetInGroupType(conditionals.target, "party") + or CleveRoids.IsTargetInGroupType(conditionals.target, "raid") + ) + end, + + -- [party] or [party:unitid] - check if unit is in your party + -- Default unit is conditionals.target + party = function(conditionals) + local unit = conditionals.party + if unit == true or unit == nil then + unit = conditionals.target end - -- PERFORMANCE: Use specialized function to avoid closure allocation - return AndNotEqualsNumber(forbiddenForms, i) + return CleveRoids.IsTargetInGroupType(unit, "party") + end, + + -- [noparty] or [noparty:unitid] - check if unit is NOT in your party + noparty = function(conditionals) + local unit = conditionals.noparty + if unit == true or unit == nil then + unit = conditionals.target + end + return not CleveRoids.IsTargetInGroupType(unit, "party") + end, + + -- [raid] or [raid:unitid] - check if unit is in your raid + -- Default unit is conditionals.target + raid = function(conditionals) + local unit = conditionals.raid + if unit == true or unit == nil then + unit = conditionals.target + end + return CleveRoids.IsTargetInGroupType(unit, "raid") + end, + + -- [noraid] or [noraid:unitid] - check if unit is NOT in your raid + noraid = function(conditionals) + local unit = conditionals.noraid + if unit == true or unit == nil then + unit = conditionals.target + end + return not CleveRoids.IsTargetInGroupType(unit, "raid") + end, + + -- [tag] - target is tapped (tagged) by anyone + tag = function(conditionals) + return conditionals.target and UnitIsTapped(conditionals.target) + end, + + -- [notag] - target is not tapped + notag = function(conditionals) + return conditionals.target and not UnitIsTapped(conditionals.target) + end, + + -- [mytag] - target is tapped by the player + mytag = function(conditionals) + return conditionals.target and UnitIsTappedByPlayer(conditionals.target) + end, + + -- [nomytag] - target is not tapped by the player + nomytag = function(conditionals) + return conditionals.target and not UnitIsTappedByPlayer(conditionals.target) + end, + + -- [othertag] - target is tapped by someone else (not the player) + othertag = function(conditionals) + return conditionals.target and UnitIsTapped(conditionals.target) and not UnitIsTappedByPlayer(conditionals.target) + end, + + -- [noothertag] - target is not tapped by someone else (not tapped, or tapped by player) + noothertag = function(conditionals) + return conditionals.target and (not UnitIsTapped(conditionals.target) or UnitIsTappedByPlayer(conditionals.target)) + end, + + -- [group] or [group:party] or [group:raid] or [group:party/raid] + -- Checks if the PLAYER is in a group (not unit membership) + group = function(conditionals) + local groupVal = conditionals.group + -- Boolean form [group] - check if in any group + if groupVal == true then + return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 + end + -- Value form [group:party] or [group:raid] or [group:party/raid] + return Multi(groupVal, function(groupType) + if groupType == "party" then + return GetNumPartyMembers() > 0 + elseif groupType == "raid" then + return GetNumRaidMembers() > 0 + end + return false + end, conditionals, "group") + end, + + -- [nogroup] or [nogroup:party] or [nogroup:raid] or [nogroup:party/raid] + -- Checks if the PLAYER is NOT in a group + nogroup = function(conditionals) + local groupVal = conditionals.nogroup + -- Boolean form [nogroup] - check if not in any group + if groupVal == true then + return GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0 + end + -- Value form with De Morgan's law via NegatedMulti + return NegatedMulti(groupVal, function(groupType) + if groupType == "party" then + return GetNumPartyMembers() == 0 + elseif groupType == "raid" then + return GetNumRaidMembers() == 0 + end + return true + end, conditionals, "nogroup") + 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) + -- Extract spell name from args (could be string or table with name/operator/amount) + local spellName = type(v) == "table" and v.name or v + + -- Check if this debuff is PENDING (being cast or queued) + -- This prevents double-application when spamming macros with spell queue + if CleveRoids.IsPendingDebuffCast(spellName, conditionals.target) then + return false -- Treat as if debuff exists (nodebuff returns false) + end + + -- Debuff not pending, check if it actually exists on target + 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 or "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 or "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 or "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 or "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 or "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, -- GCD conditional - check if GCD is active/remaining @@ -5361,695 +5985,128 @@ CleveRoids.Keywords = { end, conditionals, "nogcd") end, - -- [group] or [group:party] or [group:raid] or [group:party/raid] - -- Checks if the PLAYER is in a group (not unit membership) - group = function(conditionals) - local groupVal = conditionals.group - -- Boolean form [group] - check if in any group - if groupVal == true then - return GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0 - end - -- Value form [group:party] or [group:raid] or [group:party/raid] - return Multi(groupVal, function(groupType) - if groupType == "party" then - return GetNumPartyMembers() > 0 - elseif groupType == "raid" then - return GetNumRaidMembers() > 0 - end - return false - end, conditionals, "group") - end, - - -- [nogroup] or [nogroup:party] or [nogroup:raid] or [nogroup:party/raid] - -- Checks if the PLAYER is NOT in a group - nogroup = function(conditionals) - local groupVal = conditionals.nogroup - -- Boolean form [nogroup] - check if not in any group - if groupVal == true then - return GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0 - end - -- Value form with De Morgan's law via NegatedMulti - return NegatedMulti(groupVal, function(groupType) - if groupType == "party" then - return GetNumPartyMembers() == 0 - elseif groupType == "raid" then - return GetNumRaidMembers() == 0 - end - return true - end, conditionals, "nogroup") - 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, - - -- [incominghit:type] - Check result of last attack received by player - -- Types: crit, crushing, glancing, miss, dodge, parry, blocked, hit - -- Time check: [incominghit:<1] = received hit within last 1 second - -- Examples: [incominghit:crushing] [incominghit:crit/crushing] [incominghit:dodge] - incominghit = function(conditionals) - -- Boolean form [incominghit] - any recent incoming hit - if not conditionals.incominghit or - conditionals.incominghit == true or - (type(conditionals.incominghit) == "table" and table.getn(conditionals.incominghit) == 0) then - return CleveRoids.ValidateIncomingHit(nil, nil, nil) - end - - -- Check for specific hit type(s) - OR logic - return Or(conditionals.incominghit, function(args) - if type(args) == "string" then - return CleveRoids.ValidateIncomingHit(args, nil, nil) - elseif type(args) == "table" then - return CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) - end - return false - end) - end, - - -- [noincominghit:type] - Check that last incoming hit was NOT a specific type - noincominghit = function(conditionals) - -- Boolean form [noincominghit] - no recent incoming hit - if not conditionals.noincominghit or - conditionals.noincominghit == true or - (type(conditionals.noincominghit) == "table" and table.getn(conditionals.noincominghit) == 0) then - return not CleveRoids.ValidateIncomingHit(nil, nil, nil) - end - - -- Check that hit was NOT any of the specified types - AND logic - return NegatedMulti(conditionals.noincominghit, function(args) - if type(args) == "string" then - return not CleveRoids.ValidateIncomingHit(args, nil, nil) - elseif type(args) == "table" then - return not CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) - end - return true - end, conditionals, "noincominghit") - 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, - - -- ======================================================================== - -- AUTO-ATTACK CONDITIONALS (Nampower v2.24+) - -- ======================================================================== - -- Requires NP_EnableAutoAttackEvents=1 CVar - - -- [lastswing:type] - Check result of player's last melee swing - -- Types: crit, glancing, miss, dodge, parry, blocked, offhand/oh, mainhand/mh, hit - -- Time check: [lastswing:<2] = last swing was within 2 seconds - -- Multi-value: [lastswing:crit/glancing] = was crit OR glancing (OR logic) - -- Examples: [lastswing:dodge] [@target,lastswing:crit] [lastswing:offhand] - lastswing = function(conditionals) - -- Boolean form [lastswing] - any recent swing - if not conditionals.lastswing or - conditionals.lastswing == true or - (type(conditionals.lastswing) == "table" and table.getn(conditionals.lastswing) == 0) then - return CleveRoids.ValidateLastSwing(nil, nil, nil) - end - - -- Check for specific swing type(s) - OR logic - return Or(conditionals.lastswing, function(args) - if type(args) == "string" then - return CleveRoids.ValidateLastSwing(args, nil, nil) - elseif type(args) == "table" then - return CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) - end - return false - end) - end, - - -- [nolastswing:type] - Check that last swing was NOT a specific type - -- Uses AND logic: [nolastswing:crit/glancing] = was NOT crit AND NOT glancing - nolastswing = function(conditionals) - -- Boolean form [nolastswing] - no recent swing - if not conditionals.nolastswing or - conditionals.nolastswing == true or - (type(conditionals.nolastswing) == "table" and table.getn(conditionals.nolastswing) == 0) then - return not CleveRoids.ValidateLastSwing(nil, nil, nil) - end - - -- Check that swing was NOT any of the specified types - AND logic - return NegatedMulti(conditionals.nolastswing, function(args) - if type(args) == "string" then - return not CleveRoids.ValidateLastSwing(args, nil, nil) - elseif type(args) == "table" then - return not CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) - end - return true - end, conditionals, "nolastswing") - end, - - -- [mhimbue] - Check main hand has temporary imbue (poison, oil, sharpening stone, etc.) - -- [mhimbue:Name] - Check for specific imbue name - -- [mhimbue:<300] - Check imbue time remaining < 300 seconds - -- [mhimbue:Name<300] - Specific imbue with time check - -- [mhimbue:>#5] - Check imbue charges > 5 - mhimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.mhimbue, conditionals) - return CleveRoids.ValidateWeaponImbue("mh", args) - end, - - nomhimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.nomhimbue, conditionals) - return not CleveRoids.ValidateWeaponImbue("mh", args) - 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, - - -- ========================================================================= - -- MOVEMENT SPEED CONDITIONALS (MonkeySpeed integration) - -- ========================================================================= - -- [moving] - Player is moving (speed > 0) - -- [moving:>100] - Player speed is above 100% (faster than normal run) - -- [moving:<50] - Player speed is below 50% (slower than half speed) - -- Requires MonkeySpeed addon for speed comparisons (basic moving check uses fallback) - moving = function(conditionals) - -- Boolean form [moving] - just check if moving at all - if conditionals.moving == true then - return CleveRoids.IsPlayerMoving() - end - - -- Operator form [moving:>100] - check speed percentage - return Multi(conditionals.moving, function(args) - if type(args) ~= "table" then return false end - - -- Handle multi-comparison (e.g., >50&<150) - if args.comparisons and type(args.comparisons) == "table" then - local speed = CleveRoids.GetPlayerSpeed() - if speed == nil then - CleveRoids.RequireMonkeySpeed("moving speed comparison") - 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](speed, comp.amount) then - return false - end - end - return true - end - - return CleveRoids.ValidateMovingSpeed(args.operator, args.amount) - end, conditionals, "moving") - end, - - -- [nomoving] - Player is NOT moving (speed == 0) - -- [nomoving:>100] - Player speed is NOT above 100% (at or below normal run) - nomoving = function(conditionals) - -- Boolean form [nomoving] - check if NOT moving - if conditionals.nomoving == true then - return not CleveRoids.IsPlayerMoving() - end - - -- Operator form [nomoving:>100] - negate speed comparison - return NegatedMulti(conditionals.nomoving, function(args) - if type(args) ~= "table" then return false end - - if args.comparisons and type(args.comparisons) == "table" then - local speed = CleveRoids.GetPlayerSpeed() - if speed == nil then - -- If MonkeySpeed not available, negated returns true (fail-safe) - 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](speed, comp.amount) then - return true - end - end - return false - end - - return not CleveRoids.ValidateMovingSpeed(args.operator, args.amount) - end, conditionals, "nomoving") - 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, - - -- ======================================================================== - -- AURA CAP CONDITIONALS (Nampower v2.20+ with AURA_CAST events) - -- ======================================================================== - -- Requires NP_EnableAuraCastEvents=1 CVar for accurate tracking - -- Falls back to manual counting if events unavailable - - -- [mybuffcapped] - Player's buff bar is at capacity (32 buffs) - -- Usage: [mybuffcapped] to prevent buff waste - mybuffcapped = function(conditionals) - return CleveRoids.IsPlayerBuffCapped() - end, - - -- [nomybuffcapped] - Player's buff bar has room - nomybuffcapped = function(conditionals) - return not CleveRoids.IsPlayerBuffCapped() - end, - - mybuffcount = function(conditionals) - return Multi(conditionals.mybuffcount,function (v) return CleveRoids.ValidatePlayerAuraCount(v.bigger, v.amount) end, conditionals, "mybuffcount") - end, - - -- [nomybuffcount] - Negated buff count comparison - -- Usage: [nomybuffcount:>15] = true if NOT (buffcount > 15), i.e., buffcount <= 15 - nomybuffcount = function(conditionals) - return NegatedMulti(conditionals.nomybuffcount, function(v) - return not CleveRoids.ValidatePlayerAuraCount(v.bigger, v.amount) - end, conditionals, "nomybuffcount") - end, - - -- [mycc:type] - Check if PLAYER has a specific CC effect - -- Same types as [cc], but always checks the player - -- Examples: [mycc:stun] [mycc:fear/charm] [mycc] (any CC) - mycc = function(conditionals) - -- If no specific CC type, check for ANY loss-of-control on player - if not conditionals.mycc or (type(conditionals.mycc) == "table" and table.getn(conditionals.mycc) == 0) then - return CleveRoids.ValidateUnitAnyCrowdControl("player") - end - - -- Check for specific CC type(s) - OR logic - return Or(conditionals.mycc, function(ccType) - return CleveRoids.ValidateUnitCC("player", ccType) - end) - end, - - -- [nomycc:type] - Check if PLAYER does NOT have a specific CC effect - -- Uses AND logic for negation: [nomycc:stun/fear] = not stunned AND not feared - nomycc = function(conditionals) - -- If no specific CC type, check for NO loss-of-control on player - if not conditionals.nomycc or (type(conditionals.nomycc) == "table" and table.getn(conditionals.nomycc) == 0) then - return not CleveRoids.ValidateUnitAnyCrowdControl("player") - end - - -- Check for absence of specific CC type(s) - AND logic - return NegatedMulti(conditionals.nomycc, function(ccType) - return not CleveRoids.ValidateUnitCC("player", ccType) - end, conditionals, "nomycc") - 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, - - -- [mydebuffcapped] - Player's debuff bar is at capacity (16 debuffs) - mydebuffcapped = function(conditionals) - return CleveRoids.IsPlayerDebuffCapped() - end, - - -- [nomydebuffcapped] - Player's debuff bar has room - nomydebuffcapped = function(conditionals) - return not CleveRoids.IsPlayerDebuffCapped() - 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, - - 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, - - 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, - - 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, - - 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, - - 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, - - 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, - - -- [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, - - -- [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, - - -- [ohimbue] - Check off hand has temporary imbue - -- Same syntax as mhimbue - ohimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.ohimbue, conditionals) - return CleveRoids.ValidateWeaponImbue("oh", args) - end, - - noohimbue = function(conditionals) - local args = CleveRoids.ParseImbueArgs(conditionals.noohimbue, conditionals) - return not CleveRoids.ValidateWeaponImbue("oh", args) - 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, - - 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, - - 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, - - 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, - - -- ======================================================================== - -- RESIST TRACKING CONDITIONALS - -- ======================================================================== - - -- [resisted] - Check if last spell was resisted by current target - -- [resisted:type] - Check for specific resist type (full, partial) - -- [resisted:full/partial] - OR logic for multiple types - -- Examples: [resisted] [@target,resisted:full] [resisted:partial] - resisted = function(conditionals) - -- If no specific resist type, check for ANY resist on current target - if not conditionals.resisted or - conditionals.resisted == true or - (type(conditionals.resisted) == "table" and table.getn(conditionals.resisted) == 0) then - return CleveRoids.CheckResistState(nil) - end - - -- Check for specific resist type(s) - OR logic - return Multi(conditionals.resisted, function(resistType) - return CleveRoids.CheckResistState(resistType) - end, conditionals, "resisted") - end, - - -- [noresisted] - Check if last spell was NOT resisted by current target - -- [noresisted:type] - Check target does NOT have specific resist type - -- [noresisted:full/partial] - AND logic: not full AND not partial - noresisted = function(conditionals) - -- If no specific resist type, check for NO resist on current target - if not conditionals.noresisted or - conditionals.noresisted == true or - (type(conditionals.noresisted) == "table" and table.getn(conditionals.noresisted) == 0) then - return not CleveRoids.CheckResistState(nil) - end - - -- Check for absence of specific resist type(s) - AND logic - return NegatedMulti(conditionals.noresisted, function(resistType) - return not CleveRoids.CheckResistState(resistType) - end, conditionals, "noresisted") - end, - - resting = function() - return IsResting() == 1 - end, - - noresting = function() - return IsResting() == nil - 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 + 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 == "cast" or CleveRoids.CurrentSpell.type == "channeled" + return 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 - + 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 ~= "cast" and CleveRoids.CurrentSpell.type ~= "channeled" + return CleveRoids.CurrentSpell.type ~= "channeled" 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() + 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, - -- 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 + -- [nochanneltime] - Negated channel time comparison + -- Usage: [nochanneltime:<0.5] = true if NOT (channeltime < 0.5), i.e., >= 0.5 seconds remaining + nochanneltime = function(conditionals) + -- Calculate remaining time (0 if not channeling) + local timeLeft = 0 - -- [noslamclip] - True if casting Slam NOW will NOT clip the auto-attack - -- Usage: /cast [noslamclip] Slam - noslamclip = function(conditionals) - return CleveRoids.ValidateNoSlamClip() + if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then + local elapsed = GetTime() - CleveRoids.channelStartTime + timeLeft = CleveRoids.channelDuration - elapsed + if timeLeft < 0 then timeLeft = 0 end + end + + local check = conditionals.nochanneltime + + 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 + return not CleveRoids.comparators[check.operator](timeLeft, check.amount) + end + + return true + 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, + + -- [nocasttime] - Negated cast time comparison + -- Usage: [nocasttime:<0.5] = true if NOT (casttime < 0.5), i.e., >= 0.5 seconds remaining + nocasttime = 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 + if timeLeft < 0 then timeLeft = 0 end + end + + local check = conditionals.nocasttime + + 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 + return not CleveRoids.comparators[check.operator](timeLeft, check.amount) + end + + return true end, -- [spellcasttime] - Check a spell's total cast time from tooltip (includes haste/talents) @@ -6099,20 +6156,259 @@ CleveRoids.Keywords = { return true end, - stance = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - -- PERFORMANCE: Use specialized function to avoid closure allocation - return OrEqualsNumber(conditionals.stance, i) + -- [targeting:unit] - Target is targeting specified unit + -- [targeting:tank] - Target is targeting ANY player marked as tank in pfUI + targeting = function(conditionals) + local target = conditionals.target or "target" + + -- Handle single "tank" keyword directly (most common case) + local val = conditionals.targeting + if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then + return CleveRoids.IsTargetingAnyTank(target) + end + + return Or(val, function (unit) + if unit == "tank" then + return CleveRoids.IsTargetingAnyTank(target) + end + return (UnitIsUnit(target .. "target", unit) == 1) + end) end, - nostance = function(conditionals) - local i = CleveRoids.GetCurrentShapeshiftIndex() - local forbiddenStances = conditionals.nostance - if type(forbiddenStances) ~= "table" then - return i == 0 + -- [notargeting:unit] - Target is NOT targeting specified unit + -- [notargeting:tank] - Target is NOT targeting ANY tank (loose mob!) + notargeting = function(conditionals) + local target = conditionals.target or "target" + + -- Handle single "tank" keyword directly (most common case) + local val = conditionals.notargeting + if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then + return not CleveRoids.IsTargetingAnyTank(target) end - -- PERFORMANCE: Use specialized function to avoid closure allocation - return AndNotEqualsNumber(forbiddenStances, i) + + return NegatedMulti(val, function (unit) + if unit == "tank" then + return not CleveRoids.IsTargetingAnyTank(target) + end + return UnitIsUnit(target .. "target", unit) ~= 1 + end, conditionals, "notargeting") + end, + + isplayer = function(conditionals) + return UnitIsPlayer(conditionals.target) + end, + + -- [noisplayer] - Target is NOT a player (same as isnpc) + noisplayer = function(conditionals) + return not UnitIsPlayer(conditionals.target) + end, + + isnpc = function(conditionals) + return not UnitIsPlayer(conditionals.target) + end, + + -- [noisnpc] - Target is NOT an NPC (same as isplayer) + noisnpc = function(conditionals) + return UnitIsPlayer(conditionals.target) + end, + + -- [istank] - Target unit is marked as tank in pfUI + -- [istank:unit] - Specified unit is marked as tank + istank = function(conditionals) + if conditionals.istank and type(conditionals.istank) == "table" then + -- Check specific unit: [istank:focus] + return Or(conditionals.istank, function(unit) + local name = UnitName(unit) + return CleveRoids.IsPlayerTank(name) + end) + end + -- Check target: [istank] + local target = conditionals.target or "target" + local name = UnitName(target) + return CleveRoids.IsPlayerTank(name) + end, + + -- [noistank] - Target unit is NOT marked as tank in pfUI + noistank = function(conditionals) + if conditionals.noistank and type(conditionals.noistank) == "table" then + -- Check specific unit: [noistank:focus] + return NegatedMulti(conditionals.noistank, function(unit) + return not CleveRoids.IsPlayerTank(UnitName(unit)) + end, conditionals, "noistank") + end + -- Check target: [noistank] + local target = conditionals.target or "target" + local name = UnitName(target) + return not CleveRoids.IsPlayerTank(name) + end, + + -- [inrange] or [inrange:Spell] - Target is in range of spell + -- [inrange:Spell>N] - More than N enemies are in range of spell (count mode) + inrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return Multi(conditionals.inrange, function(args) + -- Check for count mode: [inrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + -- Resolve spell range once outside the loop for UnitXP distance checks + -- IsSpellInRange may return stale results during UnitXP target iteration + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance <= spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 1 + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + -- Use API wrapper which handles self-cast spells correctly + local result = API.IsSpellInRange(checkValue, target) + return result == 1 + end, conditionals, "inrange") + end, + + -- [noinrange] or [noinrange:Spell] - Target is NOT in range of spell + -- [noinrange:Spell>N] - More than N enemies are NOT in range of spell (count mode) + noinrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return NegatedMulti(conditionals.noinrange, function(args) + -- Check for count mode: [noinrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance > spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 0 + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + -- Use API wrapper which handles self-cast spells correctly + local result = API.IsSpellInRange(checkValue, target) + return result == 0 + end, conditionals, "noinrange") + end, + + -- [outrange] or [outrange:Spell] - Target is out of range of spell + -- [outrange:Spell>N] - More than N enemies are out of range of spell (count mode) + outrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return Multi(conditionals.outrange, function(args) + -- Check for count mode: [outrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance > spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 0 + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + -- Use API wrapper which handles self-cast spells correctly + local result = API.IsSpellInRange(checkValue, target) + return result == 0 + end, conditionals, "outrange") + end, + + -- [nooutrange] or [nooutrange:Spell] - Target is NOT out of range (same as inrange) + -- [nooutrange:Spell>N] - More than N enemies are NOT out of range (count mode) + nooutrange = function(conditionals) + if not IsSpellInRange then return end + local API = CleveRoids.NampowerAPI + return NegatedMulti(conditionals.nooutrange, function(args) + -- Check for count mode: [nooutrange:Multi-Shot>1] + if type(args) == "table" and args.operator and args.amount then + local spellName = args.name or conditionals.action + local spellId = API.GetSpellIdFromName(spellName) + local spellRange = spellId and API.GetSpellRange(spellId) + local count = CleveRoids.CountEnemiesMatching(function(unit) + if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit) + if distance then + return distance <= spellRange + end + end + local result = API.IsSpellInRange(spellName, unit) + return result == 1 + end) + return CleveRoids.comparators[args.operator](count, args.amount) + end + + -- Original single-target behavior + local target = conditionals.target or "target" + local checkValue = (type(args) == "string" and args) or conditionals.action + + local result = API.IsSpellInRange(checkValue, target) + return result == 1 -- In range = nooutrange passes + end, conditionals, "nooutrange") + 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) @@ -6192,94 +6488,76 @@ CleveRoids.Keywords = { end, conditionals, "nostat") 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"])) - ) + 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, - 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"])) - ) + 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, - -- Alias for swingtimer - stimer = function(conditionals) - return Multi(conditionals.stimer, function(args) - if type(args) ~= "table" then return false end + pet = function(conditionals) + if not UnitExists("pet") 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 + 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, - local attackSpeed = UnitAttackSpeed("player") - if not attackSpeed or attackSpeed <= 0 then return false end + nopet = function(conditionals) + if not UnitExists("pet") then + return true + 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 NegatedMulti(conditionals.nopet, function(petType) + local currentPet = UnitCreatureFamily("pet") + if not currentPet then return true end - - return CleveRoids.ValidateSwingTimer(args.operator, args.amount) - end, conditionals, "stimer") - 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") + return string.lower(currentPet) ~= string.lower(petType) + end, conditionals, "nopet") end, -- [swimming] - Player is currently swimming (Nampower v2.36+) @@ -6306,6 +6584,321 @@ CleveRoids.Keywords = { return PlayerIsSwimming() ~= 1 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] - Player is behind target + -- [behind:>N] - Player is behind more than N enemies (count mode) + behind = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [behind:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.behind) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("behind", "player", unit) == true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + local unit = conditionals.target or "target" + if not UnitExists(unit) then return false end + + return UnitXP("behind", "player", unit) == true + end, + + -- [nobehind] - Player is NOT behind target + -- [nobehind:>N] - Player is NOT behind more than N enemies (count mode) + nobehind = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [nobehind:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.nobehind) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("behind", "player", unit) ~= true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + local unit = conditionals.target or "target" + if not UnitExists(unit) then return false end + + return UnitXP("behind", "player", unit) ~= true + end, + + -- [insight] - Target is in line of sight + -- [insight:>N] - More than N enemies are in line of sight (count mode) + insight = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [insight:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.insight) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("inSight", "player", unit) == true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + local unit = conditionals.target or "target" + if not UnitExists(unit) then return false end + + return UnitXP("inSight", "player", unit) == true + end, + + -- [noinsight] - Target is NOT in line of sight + -- [noinsight:>N] - More than N enemies are NOT in line of sight (count mode) + noinsight = function(conditionals) + if not CleveRoids.hasUnitXP then return false end + + -- Check for count mode: [noinsight:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.noinsight) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + return UnitXP("inSight", "player", unit) ~= true + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + local unit = conditionals.target or "target" + if not UnitExists(unit) then return false end + + return UnitXP("inSight", "player", unit) ~= true + end, + + -- [meleerange] - Target is in melee range + -- [meleerange:>N] - More than N enemies are in melee range (count mode) + meleerange = function(conditionals) + -- Check for count mode: [meleerange:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.meleerange) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + if CleveRoids.hasUnitXP then + local distance = UnitXP("distanceBetween", "player", unit, "meleeAutoAttack") + return distance and distance <= 5 + else + return CheckInteractDistance(unit, 3) + end + end) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + 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] - Target is NOT in melee range + -- [nomeleerange:>N] - More than N enemies are NOT in melee range (count mode) + nomeleerange = function(conditionals) + -- Check for count mode: [nomeleerange:>1] + local countArgs = CleveRoids.GetCountModeArgs(conditionals.nomeleerange) + if countArgs then + local count = CleveRoids.CountEnemiesMatching(function(unit) + 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) + return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) + end + + -- Original single-target behavior + 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, + + -- [nomybuffcount] - Negated buff count comparison + -- Usage: [nomybuffcount:>15] = true if NOT (buffcount > 15), i.e., buffcount <= 15 + nomybuffcount = function(conditionals) + return NegatedMulti(conditionals.nomybuffcount, function(v) + return not CleveRoids.ValidatePlayerAuraCount(v.bigger, v.amount) + end, conditionals, "nomybuffcount") + end, + + -- [mhimbue] - Check main hand has temporary imbue (poison, oil, sharpening stone, etc.) + -- [mhimbue:Name] - Check for specific imbue name + -- [mhimbue:<300] - Check imbue time remaining < 300 seconds + -- [mhimbue:Name<300] - Specific imbue with time check + -- [mhimbue:>#5] - Check imbue charges > 5 + mhimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.mhimbue, conditionals) + return CleveRoids.ValidateWeaponImbue("mh", args) + end, + + nomhimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.nomhimbue, conditionals) + return not CleveRoids.ValidateWeaponImbue("mh", args) + end, + + -- [ohimbue] - Check off hand has temporary imbue + -- Same syntax as mhimbue + ohimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.ohimbue, conditionals) + return CleveRoids.ValidateWeaponImbue("oh", args) + end, + + noohimbue = function(conditionals) + local args = CleveRoids.ParseImbueArgs(conditionals.noohimbue, conditionals) + return not CleveRoids.ValidateWeaponImbue("oh", args) + 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 + + local isImmune = CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) + return not isImmune + 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) @@ -6347,6 +6940,44 @@ CleveRoids.Keywords = { 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) @@ -6385,11 +7016,42 @@ CleveRoids.Keywords = { end, conditionals, "noswingtimer") 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") + -- 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) @@ -6445,71 +7107,6 @@ CleveRoids.Keywords = { end, conditionals, "nothreat") 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, - -- Time-To-Kill conditional (requires TimeToKill addon) -- Usage: [ttk:<10] - true if target will die in less than 10 seconds ttk = function(conditionals) @@ -6576,317 +7173,69 @@ CleveRoids.Keywords = { end, conditionals, "nottk") 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 + -- 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 - -- Check if it's a spell first - local spell = CleveRoids.GetSpell(name) - if spell then - return CleveRoids.CheckSpellUsable(name) - 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 - -- 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 + 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 - 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") + return CleveRoids.ValidateTTE(args.operator, args.amount) + end, conditionals, "tte") 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, + -- Negated TTE conditional + notte = function(conditionals) + return NegatedMulti(conditionals.notte, function(args) + if type(args) ~= "table" then return false 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, - - -- ======================================================================== - -- UNIT-ID CONDITIONALS - -- ======================================================================== - -- These conditionals check the specified unit (default: current target) - -- and support @unit syntax: [@focus,buff:Power Word: Fortitude] - -- ======================================================================== - - 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, - - -- [behind] - Player is behind target - -- [behind:>N] - Player is behind more than N enemies (count mode) - behind = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [behind:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.behind) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", "player", unit) == true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("behind", "player", unit) == true - end, - - -- [nobehind] - Player is NOT behind target - -- [nobehind:>N] - Player is NOT behind more than N enemies (count mode) - nobehind = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [nobehind:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.nobehind) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("behind", "player", unit) ~= true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("behind", "player", unit) ~= true - 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, - - -- [buffcapped] - Target's buff bar is at capacity (32 buffs) - buffcapped = function(conditionals) - local target = conditionals.target or "target" - return CleveRoids.IsTargetBuffCapped(target) - end, - - -- [nobuffcapped] - Target's buff bar has room - nobuffcapped = function(conditionals) - local target = conditionals.target or "target" - return not CleveRoids.IsTargetBuffCapped(target) - 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, - - -- ======================================================================== - -- CC (Crowd Control) Conditionals - Requires BuffLib for full functionality - -- ======================================================================== - - -- [cc:type] - Check if target has a specific CC effect - -- Types: stun, fear, root, snare/slow, sleep, charm, polymorph, banish, horror, - -- disarm, silence, daze, sap, freeze, knockout/incap, disorient, shackle - -- Special: [cc] or [cc:any] checks for any loss-of-control effect - -- Examples: [cc:stun] [@focus,cc:fear] [cc:stun/fear/root] - cc = function(conditionals) - local target = conditionals.target or "target" - - -- If no specific CC type, check for ANY loss-of-control - if not conditionals.cc or (type(conditionals.cc) == "table" and table.getn(conditionals.cc) == 0) then - return CleveRoids.ValidateUnitAnyCrowdControl(target) - end - - -- Check for specific CC type(s) - OR logic - return Or(conditionals.cc, function(ccType) - return CleveRoids.ValidateUnitCC(target, ccType) - end) - end, - - -- [nocc:type] - Check if target does NOT have a specific CC effect - -- Uses AND logic for negation: [nocc:stun/fear] = not stunned AND not feared - nocc = function(conditionals) - local target = conditionals.target or "target" - - -- If no specific CC type, check for NO loss-of-control - if not conditionals.nocc or (type(conditionals.nocc) == "table" and table.getn(conditionals.nocc) == 0) then - return not CleveRoids.ValidateUnitAnyCrowdControl(target) - end - - -- Check for absence of specific CC type(s) - AND logic (must be missing ALL) - return NegatedMulti(conditionals.nocc, function(ccType) - return not CleveRoids.ValidateUnitCC(target, ccType) - end, conditionals, "nocc") - 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, - - 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. - -- PERFORMANCE: Use event-driven cache for player combat state - local cached = CleveRoids._cachedPlayerInCombat - if cached ~= nil then - return cached - end - -- Fallback if cache not yet initialized - 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 + 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 - return not UnitAffectingCombat(unit) - end, conditionals, "nocombat") - else - -- Otherwise, this is a bare [nocombat]. Default to checking the player. - -- PERFORMANCE: Use event-driven cache for player combat state - local cached = CleveRoids._cachedPlayerInCombat - if cached ~= nil then - return not cached + + 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 - -- Fallback if cache not yet initialized - return not UnitAffectingCombat("player") - end + + return not CleveRoids.ValidateTTE(args.operator, args.amount) + end, conditionals, "notte") end, -- ========================================================================= @@ -7011,480 +7360,187 @@ CleveRoids.Keywords = { end, conditionals, "nocursive") end, - dead = function(conditionals) - if not conditionals.target then return false end - return UnitIsDeadOrGhost(conditionals.target) + -- 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, - nodead = function(conditionals) - if not conditionals.target then return false end - return not UnitIsDeadOrGhost(conditionals.target) + -- [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, - debuff = function(conditionals) - return Multi(conditionals.debuff, function(v) - return CleveRoids.ValidateUnitDebuff(conditionals.target, v) - end, conditionals, "debuff") + -- [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, - nodebuff = function(conditionals) - return NegatedMulti(conditionals.nodebuff, function(v) - -- Extract spell name from args (could be string or table with name/operator/amount) - local spellName = type(v) == "table" and v.name or v + -- [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, - -- Check if this debuff is PENDING (being cast or queued) - -- This prevents double-application when spamming macros with spell queue - if CleveRoids.IsPendingDebuffCast(spellName, conditionals.target) then - return false -- Treat as if debuff exists (nodebuff returns false) + -- 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 - -- Debuff not pending, check if it actually exists on target - return not CleveRoids.ValidateUnitDebuff(conditionals.target, v) - end, conditionals, "nodebuff") - end, - - -- [debuffcapped] - Target's debuff bar is at capacity - -- For NPCs: 16 debuff slots (+ 32 overflow into buff slots = 48 visual total) - -- Usage: [debuffcapped] to stop DoT spam when target is capped - debuffcapped = function(conditionals) - local target = conditionals.target or "target" - return CleveRoids.IsTargetDebuffCapped(target) - end, - - -- [nodebuffcapped] - Target's debuff bar has room - nodebuffcapped = function(conditionals) - local target = conditionals.target or "target" - return not CleveRoids.IsTargetDebuffCapped(target) - 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, - - exists = function(conditionals) - return UnitExists(conditionals.target) - end, - - noexists = function(conditionals) - return not UnitExists(conditionals.target) - end, - - harm = function(conditionals) - return conditionals.harm and conditionals.target and UnitExists(conditionals.target) and UnitCanAttack("player", conditionals.target) - end, - - -- [noharm] - Target is NOT hostile (cannot attack) - noharm = function(conditionals) - if not conditionals.target or not UnitExists(conditionals.target) then - return true - end - return not UnitCanAttack("player", conditionals.target) - end, - - help = function(conditionals) - return conditionals.help and conditionals.target and UnitExists(conditionals.target) and UnitCanAssist("player", conditionals.target) - end, - - -- [nohelp] - Target is NOT friendly (cannot assist) - nohelp = function(conditionals) - if not conditionals.target or not UnitExists(conditionals.target) then - return true - end - return not UnitCanAssist("player", conditionals.target) - 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 or "target", args.operator, args.amount) - end, conditionals, "hp") - 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, - - 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 - - local isImmune = CleveRoids.CheckImmunity(conditionals.target or "target", checkValue) - return not isImmune - end, - - -- [inrange] or [inrange:Spell] - Target is in range of spell - -- [inrange:Spell>N] - More than N enemies are in range of spell (count mode) - inrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return Multi(conditionals.inrange, function(args) - -- Check for count mode: [inrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - -- Resolve spell range once outside the loop for UnitXP distance checks - -- IsSpellInRange may return stale results during UnitXP target iteration - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance <= spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 1 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - -- Use API wrapper which handles self-cast spells correctly - local result = API.IsSpellInRange(checkValue, target) - return result == 1 - end, conditionals, "inrange") - end, - - -- [noinrange] or [noinrange:Spell] - Target is NOT in range of spell - -- [noinrange:Spell>N] - More than N enemies are NOT in range of spell (count mode) - noinrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return NegatedMulti(conditionals.noinrange, function(args) - -- Check for count mode: [noinrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance > spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 0 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - -- Use API wrapper which handles self-cast spells correctly - local result = API.IsSpellInRange(checkValue, target) - return result == 0 - end, conditionals, "noinrange") - end, - - -- [insight] - Target is in line of sight - -- [insight:>N] - More than N enemies are in line of sight (count mode) - insight = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [insight:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.insight) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("inSight", "player", unit) == true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("inSight", "player", unit) == true - end, - - -- [noinsight] - Target is NOT in line of sight - -- [noinsight:>N] - More than N enemies are NOT in line of sight (count mode) - noinsight = function(conditionals) - if not CleveRoids.hasUnitXP then return false end - - -- Check for count mode: [noinsight:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.noinsight) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - return UnitXP("inSight", "player", unit) ~= true - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - local unit = conditionals.target or "target" - if not UnitExists(unit) then return false end - - return UnitXP("inSight", "player", unit) ~= true - end, - - isnpc = function(conditionals) - return not UnitIsPlayer(conditionals.target) - end, - - -- [noisnpc] - Target is NOT an NPC (same as isplayer) - noisnpc = function(conditionals) - return UnitIsPlayer(conditionals.target) - end, - - isplayer = function(conditionals) - return UnitIsPlayer(conditionals.target) - end, - - -- [noisplayer] - Target is NOT a player (same as isnpc) - noisplayer = function(conditionals) - return not UnitIsPlayer(conditionals.target) - end, - - -- [istank] - Target unit is marked as tank in pfUI - -- [istank:unit] - Specified unit is marked as tank - istank = function(conditionals) - if conditionals.istank and type(conditionals.istank) == "table" then - -- Check specific unit: [istank:focus] - return Or(conditionals.istank, function(unit) - local name = UnitName(unit) - return CleveRoids.IsPlayerTank(name) - end) - end - -- Check target: [istank] - local target = conditionals.target or "target" - local name = UnitName(target) - return CleveRoids.IsPlayerTank(name) - end, - - -- [noistank] - Target unit is NOT marked as tank in pfUI - noistank = function(conditionals) - if conditionals.noistank and type(conditionals.noistank) == "table" then - -- Check specific unit: [noistank:focus] - return NegatedMulti(conditionals.noistank, function(unit) - return not CleveRoids.IsPlayerTank(UnitName(unit)) - end, conditionals, "noistank") - end - -- Check target: [noistank] - local target = conditionals.target or "target" - local name = UnitName(target) - return not CleveRoids.IsPlayerTank(name) - 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 or "target", args.operator, args.amount) - end, conditionals, "level") - end, - - -- [meleerange] - Target is in melee range - -- [meleerange:>N] - More than N enemies are in melee range (count mode) - meleerange = function(conditionals) - -- Check for count mode: [meleerange:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.meleerange) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - if CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit, "meleeAutoAttack") - return distance and distance <= 5 - else - return CheckInteractDistance(unit, 3) - end - end) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - 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] - Target is NOT in melee range - -- [nomeleerange:>N] - More than N enemies are NOT in melee range (count mode) - nomeleerange = function(conditionals) - -- Check for count mode: [nomeleerange:>1] - local countArgs = CleveRoids.GetCountModeArgs(conditionals.nomeleerange) - if countArgs then - local count = CleveRoids.CountEnemiesMatching(function(unit) - 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) - return CleveRoids.comparators[countArgs.operator](count, countArgs.amount) - end - - -- Original single-target behavior - 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, - - member = function(conditionals) - return Or(conditionals.member, function(v) - return - CleveRoids.IsTargetInGroupType(conditionals.target, "party") - or CleveRoids.IsTargetInGroupType(conditionals.target, "raid") end) end, - -- [nomember] - check if target is NOT in party or raid - nomember = function(conditionals) - return not ( - CleveRoids.IsTargetInGroupType(conditionals.target, "party") - or CleveRoids.IsTargetInGroupType(conditionals.target, "raid") - ) + -- 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, - -- [mytag] - target is tapped by the player - mytag = function(conditionals) - return conditionals.target and UnitIsTappedByPlayer(conditionals.target) + -- ======================================================================== + -- CC (Crowd Control) Conditionals - Requires BuffLib for full functionality + -- ======================================================================== + + -- [cc:type] - Check if target has a specific CC effect + -- Types: stun, fear, root, snare/slow, sleep, charm, polymorph, banish, horror, + -- disarm, silence, daze, sap, freeze, knockout/incap, disorient, shackle + -- Special: [cc] or [cc:any] checks for any loss-of-control effect + -- Examples: [cc:stun] [@focus,cc:fear] [cc:stun/fear/root] + cc = function(conditionals) + local target = conditionals.target or "target" + + -- If no specific CC type, check for ANY loss-of-control + if not conditionals.cc or (type(conditionals.cc) == "table" and table.getn(conditionals.cc) == 0) then + return CleveRoids.ValidateUnitAnyCrowdControl(target) + end + + -- Check for specific CC type(s) - OR logic + return Or(conditionals.cc, function(ccType) + return CleveRoids.ValidateUnitCC(target, ccType) + end) end, - -- [nomytag] - target is not tapped by the player - nomytag = function(conditionals) - return conditionals.target and not UnitIsTappedByPlayer(conditionals.target) + -- [nocc:type] - Check if target does NOT have a specific CC effect + -- Uses AND logic for negation: [nocc:stun/fear] = not stunned AND not feared + nocc = function(conditionals) + local target = conditionals.target or "target" + + -- If no specific CC type, check for NO loss-of-control + if not conditionals.nocc or (type(conditionals.nocc) == "table" and table.getn(conditionals.nocc) == 0) then + return not CleveRoids.ValidateUnitAnyCrowdControl(target) + end + + -- Check for absence of specific CC type(s) - AND logic (must be missing ALL) + return NegatedMulti(conditionals.nocc, function(ccType) + return not CleveRoids.ValidateUnitCC(target, ccType) + end, conditionals, "nocc") + end, + + -- [mycc:type] - Check if PLAYER has a specific CC effect + -- Same types as [cc], but always checks the player + -- Examples: [mycc:stun] [mycc:fear/charm] [mycc] (any CC) + mycc = function(conditionals) + -- If no specific CC type, check for ANY loss-of-control on player + if not conditionals.mycc or (type(conditionals.mycc) == "table" and table.getn(conditionals.mycc) == 0) then + return CleveRoids.ValidateUnitAnyCrowdControl("player") + end + + -- Check for specific CC type(s) - OR logic + return Or(conditionals.mycc, function(ccType) + return CleveRoids.ValidateUnitCC("player", ccType) + end) + end, + + -- [nomycc:type] - Check if PLAYER does NOT have a specific CC effect + -- Uses AND logic for negation: [nomycc:stun/fear] = not stunned AND not feared + nomycc = function(conditionals) + -- If no specific CC type, check for NO loss-of-control on player + if not conditionals.nomycc or (type(conditionals.nomycc) == "table" and table.getn(conditionals.nomycc) == 0) then + return not CleveRoids.ValidateUnitAnyCrowdControl("player") + end + + -- Check for absence of specific CC type(s) - AND logic + return NegatedMulti(conditionals.nomycc, function(ccType) + return not CleveRoids.ValidateUnitCC("player", ccType) + end, conditionals, "nomycc") + end, + + -- ======================================================================== + -- RESIST TRACKING CONDITIONALS + -- ======================================================================== + + -- [resisted] - Check if last spell was resisted by current target + -- [resisted:type] - Check for specific resist type (full, partial) + -- [resisted:full/partial] - OR logic for multiple types + -- Examples: [resisted] [@target,resisted:full] [resisted:partial] + resisted = function(conditionals) + -- If no specific resist type, check for ANY resist on current target + if not conditionals.resisted or + conditionals.resisted == true or + (type(conditionals.resisted) == "table" and table.getn(conditionals.resisted) == 0) then + return CleveRoids.CheckResistState(nil) + end + + -- Check for specific resist type(s) - OR logic + return Multi(conditionals.resisted, function(resistType) + return CleveRoids.CheckResistState(resistType) + end, conditionals, "resisted") + end, + + -- [noresisted] - Check if last spell was NOT resisted by current target + -- [noresisted:type] - Check target does NOT have specific resist type + -- [noresisted:full/partial] - AND logic: not full AND not partial + noresisted = function(conditionals) + -- If no specific resist type, check for NO resist on current target + if not conditionals.noresisted or + conditionals.noresisted == true or + (type(conditionals.noresisted) == "table" and table.getn(conditionals.noresisted) == 0) then + return not CleveRoids.CheckResistState(nil) + end + + -- Check for absence of specific resist type(s) - AND logic + return NegatedMulti(conditionals.noresisted, function(resistType) + return not CleveRoids.CheckResistState(resistType) + end, conditionals, "noresisted") end, -- ======================================================================== @@ -7547,297 +7603,226 @@ CleveRoids.Keywords = { end, conditionals, "noname") end, - -- [othertag] - target is tapped by someone else (not the player) - othertag = function(conditionals) - return conditionals.target and UnitIsTapped(conditionals.target) and not UnitIsTappedByPlayer(conditionals.target) - end, + -- ======================================================================== + -- AUTO-ATTACK CONDITIONALS (Nampower v2.24+) + -- ======================================================================== + -- Requires NP_EnableAutoAttackEvents=1 CVar - -- [noothertag] - target is not tapped by someone else (not tapped, or tapped by player) - noothertag = function(conditionals) - return conditionals.target and (not UnitIsTapped(conditionals.target) or UnitIsTappedByPlayer(conditionals.target)) - end, - - -- [outrange] or [outrange:Spell] - Target is out of range of spell - -- [outrange:Spell>N] - More than N enemies are out of range of spell (count mode) - outrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return Multi(conditionals.outrange, function(args) - -- Check for count mode: [outrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance > spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 0 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - -- Use API wrapper which handles self-cast spells correctly - local result = API.IsSpellInRange(checkValue, target) - return result == 0 - end, conditionals, "outrange") - end, - - -- [nooutrange] or [nooutrange:Spell] - Target is NOT out of range (same as inrange) - -- [nooutrange:Spell>N] - More than N enemies are NOT out of range (count mode) - nooutrange = function(conditionals) - if not IsSpellInRange then return end - local API = CleveRoids.NampowerAPI - return NegatedMulti(conditionals.nooutrange, function(args) - -- Check for count mode: [nooutrange:Multi-Shot>1] - if type(args) == "table" and args.operator and args.amount then - local spellName = args.name or conditionals.action - local spellId = API.GetSpellIdFromName(spellName) - local spellRange = spellId and API.GetSpellRange(spellId) - local count = CleveRoids.CountEnemiesMatching(function(unit) - if spellRange and spellRange > 0 and CleveRoids.hasUnitXP then - local distance = UnitXP("distanceBetween", "player", unit) - if distance then - return distance <= spellRange - end - end - local result = API.IsSpellInRange(spellName, unit) - return result == 1 - end) - return CleveRoids.comparators[args.operator](count, args.amount) - end - - -- Original single-target behavior - local target = conditionals.target or "target" - local checkValue = (type(args) == "string" and args) or conditionals.action - - local result = API.IsSpellInRange(checkValue, target) - return result == 1 -- In range = nooutrange passes - end, conditionals, "nooutrange") - end, - - -- [party] or [party:unitid] - check if unit is in your party - -- Default unit is conditionals.target - party = function(conditionals) - local unit = conditionals.party - if unit == true or unit == nil then - unit = conditionals.target + -- [lastswing:type] - Check result of player's last melee swing + -- Types: crit, glancing, miss, dodge, parry, blocked, offhand/oh, mainhand/mh, hit + -- Time check: [lastswing:<2] = last swing was within 2 seconds + -- Multi-value: [lastswing:crit/glancing] = was crit OR glancing (OR logic) + -- Examples: [lastswing:dodge] [@target,lastswing:crit] [lastswing:offhand] + lastswing = function(conditionals) + -- Boolean form [lastswing] - any recent swing + if not conditionals.lastswing or + conditionals.lastswing == true or + (type(conditionals.lastswing) == "table" and table.getn(conditionals.lastswing) == 0) then + return CleveRoids.ValidateLastSwing(nil, nil, nil) end - return CleveRoids.IsTargetInGroupType(unit, "party") - end, - -- [noparty] or [noparty:unitid] - check if unit is NOT in your party - noparty = function(conditionals) - local unit = conditionals.noparty - if unit == true or unit == nil then - unit = conditionals.target - end - return not CleveRoids.IsTargetInGroupType(unit, "party") - 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 + -- Check for specific swing type(s) - OR logic + return Or(conditionals.lastswing, function(args) + if type(args) == "string" then + return CleveRoids.ValidateLastSwing(args, nil, nil) + elseif type(args) == "table" then + return CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) end - - return CleveRoids.ValidatePower(conditionals.target or "target", args.operator, args.amount) - end, conditionals, "power") - 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, - - -- 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 + -- [nolastswing:type] - Check that last swing was NOT a specific type + -- Uses AND logic: [nolastswing:crit/glancing] = was NOT crit AND NOT glancing + nolastswing = function(conditionals) + -- Boolean form [nolastswing] - no recent swing + if not conditionals.nolastswing or + conditionals.nolastswing == true or + (type(conditionals.nolastswing) == "table" and table.getn(conditionals.nolastswing) == 0) then + return not CleveRoids.ValidateLastSwing(nil, nil, nil) + 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 + -- Check that swing was NOT any of the specified types - AND logic + return NegatedMulti(conditionals.nolastswing, function(args) + if type(args) == "string" then + return not CleveRoids.ValidateLastSwing(args, nil, nil) + elseif type(args) == "table" then + return not CleveRoids.ValidateLastSwing(args.name, args.operator, args.amount) end - return true - end, conditionals, "nopowertype") + end, conditionals, "nolastswing") end, - -- [raid] or [raid:unitid] - check if unit is in your raid - -- Default unit is conditionals.target - raid = function(conditionals) - local unit = conditionals.raid - if unit == true or unit == nil then - unit = conditionals.target + -- [incominghit:type] - Check result of last attack received by player + -- Types: crit, crushing, glancing, miss, dodge, parry, blocked, hit + -- Time check: [incominghit:<1] = received hit within last 1 second + -- Examples: [incominghit:crushing] [incominghit:crit/crushing] [incominghit:dodge] + incominghit = function(conditionals) + -- Boolean form [incominghit] - any recent incoming hit + if not conditionals.incominghit or + conditionals.incominghit == true or + (type(conditionals.incominghit) == "table" and table.getn(conditionals.incominghit) == 0) then + return CleveRoids.ValidateIncomingHit(nil, nil, nil) end - return CleveRoids.IsTargetInGroupType(unit, "raid") + + -- Check for specific hit type(s) - OR logic + return Or(conditionals.incominghit, function(args) + if type(args) == "string" then + return CleveRoids.ValidateIncomingHit(args, nil, nil) + elseif type(args) == "table" then + return CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) + end + return false + end) end, - -- [noraid] or [noraid:unitid] - check if unit is NOT in your raid - noraid = function(conditionals) - local unit = conditionals.noraid - if unit == true or unit == nil then - unit = conditionals.target + -- [noincominghit:type] - Check that last incoming hit was NOT a specific type + noincominghit = function(conditionals) + -- Boolean form [noincominghit] - no recent incoming hit + if not conditionals.noincominghit or + conditionals.noincominghit == true or + (type(conditionals.noincominghit) == "table" and table.getn(conditionals.noincominghit) == 0) then + return not CleveRoids.ValidateIncomingHit(nil, nil, nil) end - return not CleveRoids.IsTargetInGroupType(unit, "raid") + + -- Check that hit was NOT any of the specified types - AND logic + return NegatedMulti(conditionals.noincominghit, function(args) + if type(args) == "string" then + return not CleveRoids.ValidateIncomingHit(args, nil, nil) + elseif type(args) == "table" then + return not CleveRoids.ValidateIncomingHit(args.name, args.operator, args.amount) + end + return true + end, conditionals, "noincominghit") end, - rawhp = function(conditionals) - return Multi(conditionals.rawhp, function(args) - if type(args) ~= "table" then return false end - return CleveRoids.ValidateRawHp(conditionals.target or "target", args.operator, args.amount) - end, conditionals, "rawhp") + -- ======================================================================== + -- AURA CAP CONDITIONALS (Nampower v2.20+ with AURA_CAST events) + -- ======================================================================== + -- Requires NP_EnableAuraCastEvents=1 CVar for accurate tracking + -- Falls back to manual counting if events unavailable + + -- [mybuffcapped] - Player's buff bar is at capacity (32 buffs) + -- Usage: [mybuffcapped] to prevent buff waste + mybuffcapped = function(conditionals) + return CleveRoids.IsPlayerBuffCapped() end, - rawpower = function(conditionals) - return Multi(conditionals.rawpower, function(args) + -- [nomybuffcapped] - Player's buff bar has room + nomybuffcapped = function(conditionals) + return not CleveRoids.IsPlayerBuffCapped() + end, + + -- [mydebuffcapped] - Player's debuff bar is at capacity (16 debuffs) + mydebuffcapped = function(conditionals) + return CleveRoids.IsPlayerDebuffCapped() + end, + + -- [nomydebuffcapped] - Player's debuff bar has room + nomydebuffcapped = function(conditionals) + return not CleveRoids.IsPlayerDebuffCapped() + end, + + -- [debuffcapped] - Target's debuff bar is at capacity + -- For NPCs: 16 debuff slots (+ 32 overflow into buff slots = 48 visual total) + -- Usage: [debuffcapped] to stop DoT spam when target is capped + debuffcapped = function(conditionals) + local target = conditionals.target or "target" + return CleveRoids.IsTargetDebuffCapped(target) + end, + + -- [nodebuffcapped] - Target's debuff bar has room + nodebuffcapped = function(conditionals) + local target = conditionals.target or "target" + return not CleveRoids.IsTargetDebuffCapped(target) + end, + + -- [buffcapped] - Target's buff bar is at capacity (32 buffs) + buffcapped = function(conditionals) + local target = conditionals.target or "target" + return CleveRoids.IsTargetBuffCapped(target) + end, + + -- [nobuffcapped] - Target's buff bar has room + nobuffcapped = function(conditionals) + local target = conditionals.target or "target" + return not CleveRoids.IsTargetBuffCapped(target) + end, + + -- ========================================================================= + -- MOVEMENT SPEED CONDITIONALS (MonkeySpeed integration) + -- ========================================================================= + -- [moving] - Player is moving (speed > 0) + -- [moving:>100] - Player speed is above 100% (faster than normal run) + -- [moving:<50] - Player speed is below 50% (slower than half speed) + -- Requires MonkeySpeed addon for speed comparisons (basic moving check uses fallback) + moving = function(conditionals) + -- Boolean form [moving] - just check if moving at all + if conditionals.moving == true then + return CleveRoids.IsPlayerMoving() + end + + -- Operator form [moving:>100] - check speed percentage + return Multi(conditionals.moving, function(args) if type(args) ~= "table" then return false end - -- Handle multi-comparison (e.g., >500&<1000) + -- Handle multi-comparison (e.g., >50&<150) 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) + local speed = CleveRoids.GetPlayerSpeed() + if speed == nil then + CleveRoids.RequireMonkeySpeed("moving speed comparison") + 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](power, comp.amount) then + if not CleveRoids.comparators[comp.operator](speed, comp.amount) then return false end end return true end - return CleveRoids.ValidateRawPower(conditionals.target or "target", args.operator, args.amount) - end, conditionals, "rawpower") + return CleveRoids.ValidateMovingSpeed(args.operator, args.amount) + end, conditionals, "moving") end, - -- [tag] - target is tapped (tagged) by anyone - tag = function(conditionals) - return conditionals.target and UnitIsTapped(conditionals.target) - end, - - -- [notag] - target is not tapped - notag = function(conditionals) - return conditionals.target and not UnitIsTapped(conditionals.target) - end, - - target = function(conditionals) - return CleveRoids.IsValidTarget(conditionals.target, conditionals.help) - end, - - -- [targeting:unit] - Target is targeting specified unit - -- [targeting:tank] - Target is targeting ANY player marked as tank in pfUI - targeting = function(conditionals) - local target = conditionals.target or "target" - - -- Handle single "tank" keyword directly (most common case) - local val = conditionals.targeting - if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then - return CleveRoids.IsTargetingAnyTank(target) + -- [nomoving] - Player is NOT moving (speed == 0) + -- [nomoving:>100] - Player speed is NOT above 100% (at or below normal run) + nomoving = function(conditionals) + -- Boolean form [nomoving] - check if NOT moving + if conditionals.nomoving == true then + return not CleveRoids.IsPlayerMoving() end - return Or(val, function (unit) - if unit == "tank" then - return CleveRoids.IsTargetingAnyTank(target) + -- Operator form [nomoving:>100] - negate speed comparison + return NegatedMulti(conditionals.nomoving, function(args) + if type(args) ~= "table" then return false end + + if args.comparisons and type(args.comparisons) == "table" then + local speed = CleveRoids.GetPlayerSpeed() + if speed == nil then + -- If MonkeySpeed not available, negated returns true (fail-safe) + 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](speed, comp.amount) then + return true + end + end + return false end - return (UnitIsUnit(target .. "target", unit) == 1) - end) - end, - - -- [notargeting:unit] - Target is NOT targeting specified unit - -- [notargeting:tank] - Target is NOT targeting ANY tank (loose mob!) - notargeting = function(conditionals) - local target = conditionals.target or "target" - - -- Handle single "tank" keyword directly (most common case) - local val = conditionals.notargeting - if val == "tank" or (type(val) == "table" and val[1] == "tank" and not val[2]) then - return not CleveRoids.IsTargetingAnyTank(target) - end - - return NegatedMulti(val, function (unit) - if unit == "tank" then - return not CleveRoids.IsTargetingAnyTank(target) - end - return UnitIsUnit(target .. "target", unit) ~= 1 - end, conditionals, "notargeting") - 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, + return not CleveRoids.ValidateMovingSpeed(args.operator, args.amount) + end, conditionals, "nomoving") + end } -- =============================================================================