mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
6513 lines
258 KiB
Lua
6513 lines
258 KiB
Lua
--[[
|
||
Author: Dennis Werner Garske (DWG) / brian / Mewtiny / brian / Mewtiny
|
||
License: MIT License
|
||
]]
|
||
|
||
-- DEBUG: Error catcher for "attempt to index a function value"
|
||
-- Remove this block once the error is identified
|
||
local _G = _G or getfenv(0)
|
||
local originalErrorHandler = geterrorhandler and geterrorhandler()
|
||
if seterrorhandler then
|
||
seterrorhandler(function(msg)
|
||
if msg and string.find(msg, "index a function value") then
|
||
local trace = debugstack and debugstack(2, 20, 0) or "no stack available"
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000[SCRM DEBUG] index function error:|r " .. tostring(msg))
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00Stack:|r " .. tostring(trace))
|
||
end
|
||
if originalErrorHandler then
|
||
return originalErrorHandler(msg)
|
||
end
|
||
end)
|
||
end
|
||
|
||
-- Setup to wrap our stuff in a table so we don't pollute the global environment
|
||
local CleveRoids = _G.CleveRoids or {}
|
||
_G.CleveRoids = CleveRoids
|
||
CleveRoids.lastItemIndexTime = 0
|
||
CleveRoids.initializationTimer = nil
|
||
CleveRoids.isActionUpdateQueued = true
|
||
CleveRoids.lastEquipTime = CleveRoids.lastEquipTime or {}
|
||
CleveRoids.lastWeaponSwapTime = 0
|
||
CleveRoids.equipInProgress = false
|
||
|
||
-- PERFORMANCE: Event throttling to reduce spam from high-frequency events
|
||
CleveRoids.lastUnitAuraUpdate = 0
|
||
CleveRoids.lastUnitHealthUpdate = 0
|
||
CleveRoids.lastUnitPowerUpdate = 0
|
||
CleveRoids.EVENT_THROTTLE = 0.1 -- 100ms throttle for high-frequency events
|
||
|
||
-- PERFORMANCE: Spell ID cache to avoid repeated GetSpellIdForName lookups
|
||
CleveRoids.spellIdCache = {}
|
||
|
||
-- PERFORMANCE: Spell name construction cache
|
||
CleveRoids.spellNameCache = {}
|
||
|
||
-- PERFORMANCE: Upvalues for frequently called global functions (avoid global lookups)
|
||
local GetTime = GetTime
|
||
local UnitExists = UnitExists
|
||
local UnitAffectingCombat = UnitAffectingCombat
|
||
local GetContainerItemLink = GetContainerItemLink
|
||
local GetContainerItemInfo = GetContainerItemInfo
|
||
local GetContainerNumSlots = GetContainerNumSlots
|
||
local GetInventoryItemLink = GetInventoryItemLink
|
||
local GetItemInfo = GetItemInfo
|
||
local PickupContainerItem = PickupContainerItem
|
||
local PickupInventoryItem = PickupInventoryItem
|
||
local EquipCursorItem = EquipCursorItem
|
||
local CursorHasItem = CursorHasItem
|
||
local ClearCursor = ClearCursor
|
||
local TargetUnit = TargetUnit
|
||
local pcall = pcall
|
||
local pairs = pairs
|
||
local ipairs = ipairs
|
||
local type = type
|
||
local tonumber = tonumber
|
||
local tostring = tostring
|
||
local string_find = string.find
|
||
local string_lower = string.lower
|
||
local string_gsub = string.gsub
|
||
local table_insert = table.insert
|
||
local table_getn = table.getn
|
||
|
||
-- PERFORMANCE: Module-level constant for boolean conditionals (avoid per-call table creation)
|
||
local BOOLEAN_CONDITIONALS = {
|
||
combat = true,
|
||
nocombat = true,
|
||
stealth = true,
|
||
nostealth = true,
|
||
channeled = true,
|
||
nochanneled = true,
|
||
checkchanneled = true,
|
||
checkcasting = true,
|
||
dead = true,
|
||
alive = true,
|
||
help = true,
|
||
harm = true,
|
||
exists = true,
|
||
party = true,
|
||
raid = true,
|
||
resting = true,
|
||
noresting = true,
|
||
isplayer = true,
|
||
isnpc = true,
|
||
mhimbue = true,
|
||
nomhimbue = true,
|
||
ohimbue = true,
|
||
noohimbue = true,
|
||
group = true,
|
||
nogroup = true,
|
||
}
|
||
|
||
local requirementCheckFrame = CreateFrame("Frame")
|
||
requirementCheckFrame:RegisterEvent("ADDON_LOADED")
|
||
requirementCheckFrame:SetScript("OnEvent", function()
|
||
if arg1 ~= "SuperCleveRoidMacros" then return end
|
||
|
||
-- Check requirements immediately when our addon loads
|
||
local hasSuperwow = CleveRoids.hasSuperwow
|
||
local hasNampower = (IsSpellInRange ~= nil)
|
||
local hasUnitXP = pcall(UnitXP, "nop", "nop")
|
||
|
||
if not hasSuperwow or not hasNampower or not hasUnitXP then
|
||
-- Show errors
|
||
if not hasSuperwow then
|
||
CleveRoids.Print("|cFFFF0000SuperCleveRoidMacros|r requires |cFF00FFFFbalakethelock's SuperWoW|r:")
|
||
CleveRoids.Print("https://github.com/balakethelock/SuperWoW")
|
||
end
|
||
if not hasNampower then
|
||
CleveRoids.Print("|cFFFF0000SuperCleveRoidMacros|r requires |cFF00FFFFAvitasia's Nampower|r:")
|
||
CleveRoids.Print("https://gitea.com/avitasia/nampower")
|
||
end
|
||
if not hasUnitXP then
|
||
CleveRoids.Print("|cFFFF0000SuperCleveRoidMacros|r requires |cFF00FFFFKonaka's UnitXP_SP3|r:")
|
||
CleveRoids.Print("https://codeberg.org/konaka/UnitXP_SP3")
|
||
end
|
||
|
||
-- Disable immediately
|
||
CleveRoids.DisableAddon("Missing Requirements")
|
||
|
||
-- Unregister this check frame
|
||
this:UnregisterAllEvents()
|
||
return
|
||
end
|
||
|
||
-- Requirements met - allow normal initialization
|
||
CleveRoids.Print("|cFF4477FFSuperCleveR|r|cFFFFFFFFoid Macros|r |cFF00FF00Loaded|r - See the README.")
|
||
|
||
-- Unregister this check frame
|
||
this:UnregisterAllEvents()
|
||
end)
|
||
|
||
local SLOT_TO_INVID = {
|
||
["MainHandSlot"] = 16,
|
||
["SecondaryHandSlot"] = 17,
|
||
["RangedSlot"] = 18,
|
||
["HeadSlot"] = 1,
|
||
["NeckSlot"] = 2,
|
||
["ShoulderSlot"] = 3,
|
||
["ChestSlot"] = 5,
|
||
["WaistSlot"] = 6,
|
||
["LegsSlot"] = 7,
|
||
["FeetSlot"] = 8,
|
||
["WristSlot"] = 9,
|
||
["HandsSlot"] = 10,
|
||
["Finger0Slot"] = 11,
|
||
["Finger1Slot"] = 12,
|
||
["Trinket0Slot"] = 13,
|
||
["Trinket1Slot"] = 14,
|
||
["BackSlot"] = 15,
|
||
["ShirtSlot"] = 4,
|
||
["TabardSlot"] = 19,
|
||
}
|
||
|
||
local function GetInventoryIdFromSlot(slotName)
|
||
return SLOT_TO_INVID[slotName] or GetInventorySlotInfo(slotName)
|
||
end
|
||
|
||
-- Check if slot 18 is a relic (no GCD) for current player class
|
||
local function IsRelicSlot(slot)
|
||
if slot ~= 18 then return false end
|
||
local playerClass = CleveRoids.playerClass
|
||
return playerClass == "PALADIN" or playerClass == "DRUID" or playerClass == "SHAMAN"
|
||
end
|
||
|
||
-- PERFORMANCE: Cache GetTime() result for cooldown checks within same frame
|
||
-- Slot 18 (relic/idol/libram/totem) has no GCD for Paladin/Druid/Shaman
|
||
local function IsSlotOnCooldown(slot, now)
|
||
-- Relics have no GCD - skip cooldown check entirely
|
||
if IsRelicSlot(slot) then
|
||
return false
|
||
end
|
||
|
||
now = now or GetTime()
|
||
local slotTime = CleveRoids.lastEquipTime[slot] or 0
|
||
local globalTime = CleveRoids.lastGlobalEquipTime or 0
|
||
|
||
return (now - slotTime) < CleveRoids.EQUIP_COOLDOWN or
|
||
(now - globalTime) < CleveRoids.EQUIP_GLOBAL_COOLDOWN
|
||
end
|
||
|
||
-- PERFORMANCE: Consolidated cursor handling, fewer CursorHasItem() calls
|
||
local function PerformEquipSwap(item, inventoryId, useQueueScript)
|
||
if not item or not inventoryId then return false end
|
||
|
||
-- Check if in combat and swapping weapons
|
||
-- Slot 18 (ranged) is only a weapon for Hunter/Warrior/Rogue/Mage/Warlock/Priest
|
||
-- For Druid/Paladin/Shaman, slot 18 is idol/libram/totem - can swap freely
|
||
local isWeapon = (inventoryId == 16 or inventoryId == 17)
|
||
if inventoryId == 18 then
|
||
-- Use cached playerClass for performance
|
||
local playerClass = CleveRoids.playerClass
|
||
-- Only treat slot 18 as weapon for classes that use ranged weapons
|
||
isWeapon = (playerClass == "HUNTER" or playerClass == "WARRIOR" or
|
||
playerClass == "ROGUE" or playerClass == "MAGE" or
|
||
playerClass == "WARLOCK" or playerClass == "PRIEST")
|
||
end
|
||
|
||
if isWeapon and UnitAffectingCombat("player") then
|
||
-- Don't swap while casting
|
||
if CleveRoids.CurrentSpell.type ~= "" then
|
||
return false
|
||
end
|
||
|
||
-- Check for on-swing spells if available (Nampower)
|
||
if GetCurrentCastingInfo then
|
||
local castId, _, _, casting, channeling, onswing = GetCurrentCastingInfo()
|
||
|
||
if onswing == 1 then
|
||
return false
|
||
end
|
||
|
||
-- If casting/channeling and QueueScript available, queue the swap for after cast
|
||
if (casting == 1 or channeling == 1) and QueueScript and useQueueScript and item.name then
|
||
local script = string.format('EquipItemByName("%s",%d)', item.name, inventoryId)
|
||
QueueScript(script, 3) -- Priority 3 = after queued spells
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Try EquipItemByName first if available (handles everything internally)
|
||
if item.name and EquipItemByName then
|
||
local ok = pcall(EquipItemByName, item.name, inventoryId)
|
||
if ok then return true end
|
||
end
|
||
|
||
-- Fallback: Manual pickup and equip
|
||
if item.bagID and item.slot then
|
||
PickupContainerItem(item.bagID, item.slot)
|
||
elseif item.inventoryID then
|
||
PickupInventoryItem(item.inventoryID)
|
||
else
|
||
return false
|
||
end
|
||
|
||
-- Single cursor check after pickup attempt
|
||
if not CursorHasItem() then
|
||
return false
|
||
end
|
||
|
||
EquipCursorItem(inventoryId)
|
||
local success = not CursorHasItem()
|
||
ClearCursor()
|
||
return success
|
||
end
|
||
|
||
-- PERFORMANCE: Get a queue entry from pool or create new
|
||
local function GetQueueEntry()
|
||
local pool = CleveRoids.queueEntryPool
|
||
local n = pool and table_getn(pool) or 0
|
||
if n > 0 then
|
||
local entry = pool[n]
|
||
pool[n] = nil
|
||
return entry
|
||
end
|
||
return {}
|
||
end
|
||
|
||
-- PERFORMANCE: Return entry to pool for reuse
|
||
local function ReleaseQueueEntry(entry)
|
||
if not entry then return end
|
||
-- Clear the entry (set to nil, don't create new table)
|
||
entry.item = nil
|
||
entry.slotName = nil
|
||
entry.inventoryId = nil
|
||
entry.queueTime = nil
|
||
entry.retries = nil
|
||
entry.maxRetries = nil
|
||
entry.itemId = nil
|
||
-- Add to pool (max 10 pooled entries)
|
||
local pool = CleveRoids.queueEntryPool
|
||
if table_getn(pool) < 10 then
|
||
table_insert(pool, entry)
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Swap-and-pop removal (O(1) instead of O(n))
|
||
local function RemoveQueueEntry(i)
|
||
local queue = CleveRoids.equipmentQueue
|
||
local n = CleveRoids.equipmentQueueLen
|
||
local entry = queue[i]
|
||
|
||
if i < n then
|
||
queue[i] = queue[n]
|
||
end
|
||
queue[n] = nil
|
||
CleveRoids.equipmentQueueLen = n - 1
|
||
|
||
ReleaseQueueEntry(entry)
|
||
end
|
||
|
||
-- Queue equipment swap function
|
||
function CleveRoids.QueueEquipItem(item, slotName)
|
||
if not item or not slotName then return false end
|
||
|
||
local inventoryId = GetInventoryIdFromSlot(slotName)
|
||
if not inventoryId then return false end
|
||
|
||
local now = GetTime()
|
||
local itemId = item.id
|
||
|
||
-- PERFORMANCE: Check for duplicate queue entries before adding
|
||
local queue = CleveRoids.equipmentQueue
|
||
local queueLen = CleveRoids.equipmentQueueLen
|
||
for i = 1, queueLen do
|
||
local q = queue[i]
|
||
if q and q.inventoryId == inventoryId and q.itemId == itemId then
|
||
return false -- Already queued
|
||
end
|
||
end
|
||
|
||
-- Try immediate equip if not on cooldown (use QueueScript for smoother mid-cast swaps)
|
||
if not IsSlotOnCooldown(inventoryId, now) then
|
||
local success = PerformEquipSwap(item, inventoryId, true)
|
||
|
||
if success then
|
||
-- Don't set cooldowns for relic slots (no GCD)
|
||
if not IsRelicSlot(inventoryId) then
|
||
CleveRoids.lastEquipTime[inventoryId] = now
|
||
CleveRoids.lastGlobalEquipTime = now
|
||
end
|
||
return true
|
||
end
|
||
end
|
||
|
||
-- Queue for later using pooled entry
|
||
local entry = GetQueueEntry()
|
||
entry.item = item
|
||
entry.slotName = slotName
|
||
entry.inventoryId = inventoryId
|
||
entry.queueTime = now
|
||
entry.retries = 0
|
||
entry.maxRetries = 5
|
||
entry.itemId = itemId
|
||
|
||
CleveRoids.equipmentQueueLen = queueLen + 1
|
||
queue[CleveRoids.equipmentQueueLen] = entry
|
||
|
||
-- PERFORMANCE: Start the queue processing frame
|
||
if CleveRoids.equipQueueFrame then
|
||
CleveRoids.equipQueueFrame:Show()
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
-- PERFORMANCE: Process equipment queue (called from self-disabling frame)
|
||
function CleveRoids.ProcessEquipmentQueue()
|
||
local queueLen = CleveRoids.equipmentQueueLen
|
||
if queueLen == 0 then return end
|
||
|
||
local now = GetTime()
|
||
local queue = CleveRoids.equipmentQueue
|
||
local i = 1
|
||
|
||
while i <= CleveRoids.equipmentQueueLen do
|
||
local queued = queue[i]
|
||
|
||
-- Remove expired entries first (>10 seconds old)
|
||
if (now - queued.queueTime) > 10 then
|
||
RemoveQueueEntry(i)
|
||
-- Don't increment i, we swapped in the last element
|
||
elseif not IsSlotOnCooldown(queued.inventoryId, now) then
|
||
-- Try to equip (use QueueScript for smoother mid-cast swaps)
|
||
local success = PerformEquipSwap(queued.item, queued.inventoryId, true)
|
||
|
||
if success then
|
||
-- Don't set cooldowns for relic slots (no GCD)
|
||
if not IsRelicSlot(queued.inventoryId) then
|
||
CleveRoids.lastEquipTime[queued.inventoryId] = now
|
||
CleveRoids.lastGlobalEquipTime = now
|
||
end
|
||
RemoveQueueEntry(i)
|
||
else
|
||
queued.retries = queued.retries + 1
|
||
if queued.retries >= queued.maxRetries then
|
||
RemoveQueueEntry(i)
|
||
else
|
||
i = i + 1
|
||
end
|
||
end
|
||
else
|
||
i = i + 1
|
||
end
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Self-disabling frame for queue processing with throttling
|
||
-- Only processes every EQUIP_QUEUE_INTERVAL instead of every frame
|
||
CleveRoids.EQUIP_QUEUE_INTERVAL = 0.1 -- 100ms between queue checks (was 50ms)
|
||
CleveRoids.equipQueueLastUpdate = 0
|
||
CleveRoids.equipQueueFrame = CreateFrame("Frame")
|
||
CleveRoids.equipQueueFrame:Hide()
|
||
|
||
-- PERFORMANCE: Upvalue for faster access in OnUpdate
|
||
local equipQueueFrame = CleveRoids.equipQueueFrame
|
||
local equipQueueInterval = CleveRoids.EQUIP_QUEUE_INTERVAL
|
||
|
||
equipQueueFrame:SetScript("OnUpdate", function()
|
||
-- PERFORMANCE: Use arg1 (elapsed time) if available, otherwise GetTime()
|
||
local elapsed = arg1
|
||
if elapsed then
|
||
CleveRoids.equipQueueLastUpdate = (CleveRoids.equipQueueLastUpdate or 0) + elapsed
|
||
if CleveRoids.equipQueueLastUpdate < equipQueueInterval then
|
||
return -- Throttle: skip this frame
|
||
end
|
||
CleveRoids.equipQueueLastUpdate = 0
|
||
else
|
||
local now = GetTime()
|
||
if (now - (CleveRoids.equipQueueLastUpdate or 0)) < equipQueueInterval then
|
||
return
|
||
end
|
||
CleveRoids.equipQueueLastUpdate = now
|
||
end
|
||
|
||
CleveRoids.ProcessEquipmentQueue()
|
||
if CleveRoids.equipmentQueueLen == 0 then
|
||
this:Hide()
|
||
end
|
||
end)
|
||
|
||
-- Improved DisableAddon function
|
||
function CleveRoids.DisableAddon(reason)
|
||
-- Prevent multiple disable calls
|
||
if CleveRoids.disabled then return end
|
||
|
||
-- Mark state
|
||
CleveRoids.disabled = true
|
||
|
||
-- Stop main frame activity
|
||
if CleveRoids.Frame then
|
||
if CleveRoids.Frame.UnregisterAllEvents then
|
||
CleveRoids.Frame:UnregisterAllEvents()
|
||
end
|
||
if CleveRoids.Frame.SetScript then
|
||
CleveRoids.Frame:SetScript("OnEvent", nil)
|
||
CleveRoids.Frame:SetScript("OnUpdate", nil)
|
||
end
|
||
end
|
||
|
||
-- Neuter all slash commands
|
||
if SlashCmdList then
|
||
local disabledMsg = function()
|
||
CleveRoids.Print("|cffff0000SuperCleveRoidMacros is disabled|r" ..
|
||
(reason and (": " .. tostring(reason)) or ""))
|
||
end
|
||
|
||
SlashCmdList.CLEVEROID = disabledMsg
|
||
SlashCmdList.CAST = disabledMsg
|
||
SlashCmdList.USE = disabledMsg
|
||
SlashCmdList.EQUIP = disabledMsg
|
||
SlashCmdList.EQUIPMH = disabledMsg
|
||
SlashCmdList.EQUIPOH = disabledMsg
|
||
end
|
||
|
||
-- Try to disable for next login
|
||
local addonName = "SuperCleveRoidMacros"
|
||
if type(DisableAddOn) == "function" then
|
||
pcall(DisableAddOn, addonName)
|
||
end
|
||
|
||
-- Final notice
|
||
CleveRoids.Print("|cffff0000Disabled|r - " .. (reason or "Unknown reason"))
|
||
CleveRoids.Print("Please install required dependencies and /reload")
|
||
end
|
||
|
||
local frame = CreateFrame("Frame")
|
||
frame:RegisterEvent("VARIABLES_LOADED")
|
||
frame:SetScript("OnEvent", function()
|
||
if not CleveRoidMacros then CleveRoidMacros = {} end
|
||
|
||
if type(CleveRoidMacros.realtime) ~= "number" then
|
||
CleveRoidMacros.realtime = 0
|
||
end
|
||
|
||
if type(CleveRoidMacros.refresh) ~= "number" then
|
||
CleveRoidMacros.refresh = 5
|
||
end
|
||
end)
|
||
|
||
-- Queues a full update of all action bars.
|
||
-- This is called by game event handlers to avoid running heavy logic inside the event itself.
|
||
function CleveRoids.QueueActionUpdate()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.isActionUpdateQueued = true
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cffff00ff[QueueActionUpdate]|r Queued, isActionUpdateQueued = %s",
|
||
tostring(CleveRoids.isActionUpdateQueued))
|
||
)
|
||
end
|
||
end
|
||
end
|
||
|
||
local function _StripColor(s)
|
||
if not s then return s end
|
||
if string.sub(s,1,2) == "|c" then return string.sub(s,11,-3) end
|
||
return s
|
||
end
|
||
|
||
local _ReagentBySpell = {
|
||
["Vanish"] = "Flash Powder", -- 5140
|
||
["Blind"] = "Blinding Powder", -- 5530
|
||
}
|
||
|
||
-- Expose for use in Generic.lua (IndexSpells fallback)
|
||
CleveRoids.ReagentBySpell = _ReagentBySpell
|
||
|
||
-- Minimal map for rogue reagents; extend as needed
|
||
local _ReagentIdByName = {
|
||
["Flash Powder"] = 5140, -- Vanish
|
||
["Blinding Powder"] = 5530, -- Blind
|
||
}
|
||
|
||
-- Lazy bag-scan tooltip (only if we need to scan a bag slot by name)
|
||
local function CRM_GetBagScanTip()
|
||
local tip = _G.CleveRoidsBagScanTip
|
||
if tip then return tip end
|
||
local ok, created = pcall(CreateFrame, "GameTooltip", "CleveRoidsBagScanTip", UIParent, "GameTooltipTemplate")
|
||
if ok and created then
|
||
tip = created
|
||
else
|
||
tip = CreateFrame("GameTooltip", "CleveRoidsBagScanTip", UIParent)
|
||
local L1 = tip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
|
||
local R1 = tip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
||
tip:AddFontStrings(L1, R1)
|
||
for i=2,10 do
|
||
tip:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
|
||
tip:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
|
||
end
|
||
end
|
||
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||
_G.CleveRoidsBagScanTip = tip
|
||
return tip
|
||
end
|
||
|
||
-- Return total in *bags only* (not bank), by id when possible; fallback to name
|
||
-- PERFORMANCE: Uses CleveRoids.Items cache for O(1) lookup when available
|
||
function CleveRoids.GetReagentCount(reagentName)
|
||
if not reagentName or reagentName == "" then return 0 end
|
||
|
||
-- Fast path: check cache first
|
||
local Items = CleveRoids.Items
|
||
if Items then
|
||
local wantId = _ReagentIdByName[reagentName]
|
||
|
||
-- Try by ID first if we have a mapping
|
||
if wantId then
|
||
local itemName = Items[wantId]
|
||
if itemName then
|
||
local itemData = Items[itemName]
|
||
if itemData and itemData.count and not itemData.inventoryID then
|
||
-- Item is in bags (not equipped), return cached count
|
||
return itemData.count
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Try by name
|
||
local itemData = Items[reagentName]
|
||
if type(itemData) == "string" then
|
||
itemData = Items[itemData] -- Resolve indirection
|
||
end
|
||
if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then
|
||
return itemData.count
|
||
end
|
||
|
||
-- Try lowercase
|
||
local lowerName = string.lower(reagentName)
|
||
local resolved = Items[lowerName]
|
||
if type(resolved) == "string" then
|
||
itemData = Items[resolved]
|
||
elseif type(resolved) == "table" then
|
||
itemData = resolved
|
||
end
|
||
if itemData and type(itemData) == "table" and itemData.count and not itemData.inventoryID then
|
||
return itemData.count
|
||
end
|
||
end
|
||
|
||
-- Slow path fallback: full bag scan (only when cache miss)
|
||
local wantId = _ReagentIdByName[reagentName]
|
||
local total = 0
|
||
|
||
for bag = 0, 4 do
|
||
local slots = GetContainerNumSlots(bag) or 0
|
||
for slot = 1, slots do
|
||
local _, count = GetContainerItemInfo(bag, slot)
|
||
count = count or 0
|
||
local link = GetContainerItemLink and GetContainerItemLink(bag, slot)
|
||
if link then
|
||
local _, _, idstr = string.find(link, "item:(%d+)")
|
||
local id = idstr and tonumber(idstr) or nil
|
||
if (wantId and id == wantId) or (not wantId and string.find(link, "%["..reagentName.."%]")) then
|
||
total = total + count
|
||
end
|
||
else
|
||
-- Fallback: scan bag slot tooltip for the name (expensive, only when no link)
|
||
local tip = CRM_GetBagScanTip()
|
||
tip:ClearLines()
|
||
tip:SetBagItem(bag, slot)
|
||
local left1 = _G[tip:GetName().."TextLeft1"]
|
||
local name = left1 and left1:GetText()
|
||
if name and name == reagentName then
|
||
total = total + count
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
return total
|
||
end
|
||
|
||
function CleveRoids.GetSpellCost(spellSlot, bookType)
|
||
-- Fast path: existing fixed-slot read
|
||
CleveRoids.Frame:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||
CleveRoids.Frame:SetSpell(spellSlot, bookType)
|
||
|
||
local cost, reagent
|
||
local costText = CleveRoids.Frame.costFontString:GetText()
|
||
if costText then
|
||
_, _, cost = string.find(costText, "^(%d+)%s+[^yYsS]")
|
||
end
|
||
|
||
local reagentText = CleveRoids.Frame.reagentFontString:GetText()
|
||
if reagentText then
|
||
_, _, reagent = string.find(reagentText, "^Reagents?%s*:%s*(.*)")
|
||
end
|
||
reagent = _StripColor(reagent)
|
||
|
||
-- Fallback: scan all lines on a named tooltip (handles Vanish layout)
|
||
if not reagent or not cost then
|
||
local tip = CleveRoidsTooltipScan
|
||
if not tip then
|
||
-- belt & suspenders: create if somehow missing
|
||
local ok,_ = pcall(CreateFrame, "GameTooltip", "CleveRoidsTooltipScan", UIParent, "GameTooltipTemplate")
|
||
if not ok or not CleveRoidsTooltipScan then
|
||
CleveRoidsTooltipScan = CreateFrame("GameTooltip", "CleveRoidsTooltipScan", UIParent)
|
||
local L1 = CleveRoidsTooltipScan:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
|
||
local R1 = CleveRoidsTooltipScan:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
||
CleveRoidsTooltipScan:AddFontStrings(L1, R1)
|
||
for i=2,32 do
|
||
CleveRoidsTooltipScan:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
|
||
CleveRoidsTooltipScan:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
|
||
end
|
||
end
|
||
CleveRoidsTooltipScan:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||
tip = CleveRoidsTooltipScan
|
||
end
|
||
|
||
tip:ClearLines()
|
||
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||
tip:SetSpell(spellSlot, bookType)
|
||
|
||
local base = tip:GetName() or "CleveRoidsTooltipScan"
|
||
local maxLines = (tip.NumLines and tip:NumLines()) or 32
|
||
|
||
for i = 1, maxLines do
|
||
local L = _G[base.."TextLeft"..i]
|
||
local R = _G[base.."TextRight"..i]
|
||
local lt = L and L:GetText() or ""
|
||
local rt = R and R:GetText() or ""
|
||
|
||
if not reagent then
|
||
if string.find(lt, "^[Rr]eagents?%s*:") then
|
||
reagent = _StripColor((rt ~= "" and rt) or (string.gsub(lt, "^[Rr]eagents?%s*:%s*", "")))
|
||
elseif string.find(rt, "^[Rr]eagents?%s*:") then
|
||
reagent = _StripColor((lt ~= "" and lt) or (string.gsub(rt, "^[Rr]eagents?%s*:%s*", "")))
|
||
end
|
||
end
|
||
|
||
if not cost and rt ~= "" then
|
||
local _, _, num = string.find(rt, "^(%d+)%s+(Mana|Energy|Rage|Focus)")
|
||
if num then cost = tonumber(num) end
|
||
end
|
||
|
||
if reagent and cost then break end
|
||
end
|
||
end
|
||
|
||
if not reagent then
|
||
local name = GetSpellName(spellSlot, bookType)
|
||
if name then
|
||
name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)%s*$", "") -- strip "(Rank X)" only
|
||
reagent = _ReagentBySpell[name]
|
||
end
|
||
end
|
||
|
||
return (cost and tonumber(cost) or 0), (reagent and tostring(reagent) or nil)
|
||
end
|
||
|
||
function CleveRoids.GetProxyActionSlot(slot)
|
||
if not slot then return end
|
||
return CleveRoids.actionSlots[slot] or CleveRoids.actionSlots[slot.."()"]
|
||
end
|
||
|
||
-- Resolves nested macro references for #showtooltip propagation
|
||
-- If an action is a {MacroName} reference and that macro has #showtooltip,
|
||
-- returns the inner macro's active action; otherwise returns nil
|
||
-- depth parameter prevents infinite recursion
|
||
function CleveRoids.ResolveNestedMacroActive(action, depth)
|
||
if not action or not action.action then return nil end
|
||
depth = depth or 0
|
||
if depth > 5 then return nil end -- prevent infinite recursion
|
||
|
||
-- Check if this action is a macro reference {MacroName}
|
||
local macroName = CleveRoids.GetMacroNameFromAction(action.action)
|
||
if not macroName then return nil end
|
||
|
||
-- Get or parse the inner macro
|
||
local innerMacro = CleveRoids.GetMacro(macroName)
|
||
if not innerMacro then
|
||
innerMacro = CleveRoids.ParseMacro(macroName)
|
||
end
|
||
if not innerMacro or not innerMacro.actions then return nil end
|
||
|
||
-- Check if inner macro has #showtooltip (indicated by having a tooltip or action list)
|
||
if not innerMacro.actions.tooltip and table.getn(innerMacro.actions.list or {}) == 0 then
|
||
return nil
|
||
end
|
||
|
||
-- Run TestForActiveAction on inner macro to get its current active
|
||
CleveRoids.TestForActiveAction(innerMacro.actions)
|
||
|
||
-- If inner macro has an active action, check if it's also a nested macro
|
||
if innerMacro.actions.active then
|
||
local deeperActive = CleveRoids.ResolveNestedMacroActive(innerMacro.actions.active, depth + 1)
|
||
if deeperActive then
|
||
return deeperActive
|
||
end
|
||
return innerMacro.actions.active
|
||
end
|
||
|
||
return nil
|
||
end
|
||
|
||
function CleveRoids.TestForActiveAction(actions)
|
||
if not actions then return end
|
||
local currentActive = actions.active
|
||
local currentSequence = actions.sequence
|
||
local hasActive = false
|
||
local newActiveAction = nil
|
||
local newSequence = nil
|
||
local firstUnconditional = nil
|
||
|
||
if actions.tooltip and table.getn(actions.list) == 0 then
|
||
if CleveRoids.TestAction(actions.cmd or "", actions.args or "") then
|
||
hasActive = true
|
||
newActiveAction = actions.tooltip
|
||
-- Resolve nested macro references for #showtooltip propagation
|
||
local macroName = CleveRoids.GetMacroNameFromAction(actions.tooltip.action)
|
||
if macroName then
|
||
local resolved = CleveRoids.ResolveNestedMacroActive(actions.tooltip, 0)
|
||
if resolved then
|
||
newActiveAction = resolved
|
||
else
|
||
-- Inner macro didn't resolve - no active action
|
||
hasActive = false
|
||
newActiveAction = nil
|
||
end
|
||
end
|
||
end
|
||
else
|
||
-- First pass: find first action with conditionals that passes
|
||
for _, action in ipairs(actions.list) do
|
||
local result = CleveRoids.TestAction(action.cmd, action.args)
|
||
|
||
-- Check if action has conditionals
|
||
local _, conditionals = CleveRoids.GetParsedMsg(action.args)
|
||
|
||
-- Skip this action for icon display if it has the '?' prefix
|
||
-- Note: CleveRoids._ignoretooltip is a side-effect flag set by GetParsedMsg
|
||
local shouldSkipForIcon = CleveRoids._ignoretooltip == 1
|
||
|
||
-- break on first action that passes tests (unless it should be skipped for icon)
|
||
if result and not shouldSkipForIcon then
|
||
hasActive = true
|
||
if action.sequence then
|
||
newSequence = action.sequence
|
||
newActiveAction = CleveRoids.GetCurrentSequenceAction(newSequence)
|
||
if not newActiveAction then hasActive = false end
|
||
else
|
||
newActiveAction = action
|
||
-- Resolve nested macro references for #showtooltip propagation
|
||
-- If inner macro doesn't resolve, continue to next action
|
||
local macroName = CleveRoids.GetMacroNameFromAction(action.action)
|
||
if macroName then
|
||
local resolved = CleveRoids.ResolveNestedMacroActive(action, 0)
|
||
if resolved then
|
||
newActiveAction = resolved
|
||
else
|
||
-- Inner macro didn't resolve - try next action
|
||
hasActive = false
|
||
newActiveAction = nil
|
||
end
|
||
end
|
||
end
|
||
if hasActive then break end
|
||
end
|
||
|
||
-- Track first unconditional non-macro action as fallback
|
||
if not conditionals and not firstUnconditional and not shouldSkipForIcon then
|
||
-- Don't use unresolvable macro refs as fallback
|
||
local macroName = CleveRoids.GetMacroNameFromAction(action.action)
|
||
if not macroName then
|
||
firstUnconditional = action
|
||
end
|
||
end
|
||
end
|
||
|
||
-- If no conditional action passed, use first unconditional action
|
||
if not hasActive and firstUnconditional then
|
||
hasActive = true
|
||
newActiveAction = firstUnconditional
|
||
end
|
||
end
|
||
|
||
local changed = false
|
||
if currentActive ~= newActiveAction or currentSequence ~= newSequence then
|
||
actions.active = newActiveAction
|
||
actions.sequence = newSequence
|
||
changed = true
|
||
end
|
||
|
||
if not hasActive then
|
||
if actions.active ~= nil or actions.sequence ~= nil then
|
||
actions.active = nil
|
||
actions.sequence = nil
|
||
changed = true
|
||
end
|
||
return changed
|
||
end
|
||
|
||
if actions.active then
|
||
local previousUsable = actions.active.usable
|
||
local previousOom = actions.active.oom
|
||
local previousInRange = actions.active.inRange
|
||
|
||
if actions.active.spell then
|
||
-- Default to -1 (unknown) so we fall through to proxy slot or original function
|
||
-- Only set to 1/0 when we have a definitive answer from IsSpellInRange
|
||
actions.active.inRange = -1
|
||
|
||
-- Enhanced nampower range check with spell ID support
|
||
if IsSpellInRange then
|
||
local unit = actions.active.conditionals and actions.active.conditionals.target or "target"
|
||
if unit == "focus" and pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
|
||
unit = pfUI.uf.focus.label .. pfUI.uf.focus.id
|
||
end
|
||
|
||
-- PERFORMANCE: Cache spell name construction using two-level cache (no string concat for lookup)
|
||
local castName = actions.active.action
|
||
if actions.active.spell and actions.active.spell.name then
|
||
local spellName = actions.active.spell.name
|
||
local rank = actions.active.spell.rank
|
||
or (actions.active.spell.highest and actions.active.spell.highest.rank)
|
||
if rank and rank ~= "" then
|
||
-- Two-level cache: spellNameCache[spellName][rank] = "SpellName(Rank X)"
|
||
local nameCache = CleveRoids.spellNameCache[spellName]
|
||
if not nameCache then
|
||
nameCache = {}
|
||
CleveRoids.spellNameCache[spellName] = nameCache
|
||
end
|
||
castName = nameCache[rank]
|
||
if not castName then
|
||
castName = spellName .. "(" .. rank .. ")"
|
||
nameCache[rank] = castName
|
||
end
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Try to get spell ID with caching
|
||
local spellId = nil
|
||
if GetSpellIdForName then
|
||
-- Check cache first
|
||
local cachedId = CleveRoids.spellIdCache[castName]
|
||
if cachedId then
|
||
spellId = cachedId
|
||
else
|
||
spellId = GetSpellIdForName(castName)
|
||
if spellId and spellId > 0 then
|
||
CleveRoids.spellIdCache[castName] = spellId
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Check range using API wrapper (handles all cases properly)
|
||
local API = CleveRoids.NampowerAPI
|
||
local checkValue = spellId and spellId > 0 and spellId or castName
|
||
|
||
if UnitExists(unit) then
|
||
-- We have a target - use API wrapper for proper range checking
|
||
-- API.IsSpellInRange handles: native check, -1 for self-cast, UnitXP fallback
|
||
if API then
|
||
local r = API.IsSpellInRange(checkValue, unit)
|
||
if r ~= nil then
|
||
actions.active.inRange = r
|
||
end
|
||
elseif IsSpellInRange then
|
||
-- No API, use native directly
|
||
local nativeResult = IsSpellInRange(checkValue, unit)
|
||
if nativeResult == 0 or nativeResult == 1 then
|
||
actions.active.inRange = nativeResult
|
||
elseif nativeResult == -1 then
|
||
-- Self-cast/ground-targeted spell, always in range
|
||
actions.active.inRange = 1
|
||
end
|
||
end
|
||
end
|
||
-- No target: don't set inRange, let proxy slot / original behavior handle it
|
||
end
|
||
|
||
-- Check if spell is usable first (handles forms, stances, and power type correctly)
|
||
local isUsableBySpell, notEnoughPower = nil, nil
|
||
if IsSpellUsable then
|
||
-- pcall to handle spells not in spellbook (Nampower throws error)
|
||
local ok, usable, oom = pcall(IsSpellUsable, actions.active.action)
|
||
if ok then
|
||
isUsableBySpell, notEnoughPower = usable, oom
|
||
end
|
||
end
|
||
|
||
-- For OOM check, use proper mana source
|
||
if notEnoughPower ~= nil then
|
||
-- Prefer IsSpellUsable result if available (Nampower)
|
||
actions.active.oom = (notEnoughPower == 1)
|
||
else
|
||
-- SuperWoW: UnitMana returns (current power, caster mana) for druids
|
||
local currentPower, casterMana = UnitMana("player")
|
||
|
||
-- For druids with SuperWoW, use caster mana for spell cost checks
|
||
local manaToCheck = currentPower
|
||
if CleveRoids.playerClass == "DRUID" and type(casterMana) == "number" then
|
||
manaToCheck = casterMana
|
||
end
|
||
|
||
actions.active.oom = (manaToCheck < actions.active.spell.cost)
|
||
end
|
||
|
||
local start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType)
|
||
local onCooldown = (start > 0 and duration > 0)
|
||
|
||
if actions.active.isReactive then
|
||
-- For Overpower, Revenge, Riposte: ONLY use combat log tracking
|
||
local spellName = actions.active.action
|
||
local useCombatLogOnly = (spellName == "Overpower" or spellName == "Revenge" or spellName == "Riposte")
|
||
|
||
if useCombatLogOnly then
|
||
-- Only trust HasReactiveProc for these spells
|
||
local hasProc = CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(spellName)
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cff00ff00[UPDATE USABLE]|r %s: hasProc=%s, previousUsable=%s, inRange=%s, oom=%s",
|
||
spellName, tostring(hasProc), tostring(previousUsable), tostring(actions.active.inRange), tostring(actions.active.oom))
|
||
)
|
||
end
|
||
if hasProc then
|
||
-- Proc is active, show as usable if in range and have enough rage/mana
|
||
if actions.active.inRange ~= 0 and not actions.active.oom then
|
||
actions.active.usable = 1
|
||
elseif pfUI and pfUI.bars and actions.active.oom then
|
||
actions.active.usable = 2 -- pfUI: out of mana/rage
|
||
else
|
||
actions.active.usable = nil
|
||
end
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cff00ff00[UPDATE USABLE]|r %s: SET usable=%s (proc active)",
|
||
spellName, tostring(actions.active.usable))
|
||
)
|
||
end
|
||
else
|
||
-- No proc = not usable
|
||
actions.active.usable = nil
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cff00ff00[UPDATE USABLE]|r %s: SET usable=nil (no proc)", spellName)
|
||
)
|
||
end
|
||
end
|
||
else
|
||
-- For other reactive spells, use the original fallback logic
|
||
-- Check combat log-based proc tracking first (stance-independent)
|
||
if CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(actions.active.action) then
|
||
-- Proc is active, show as usable if in range and have enough rage/mana
|
||
if actions.active.inRange ~= 0 and not actions.active.oom then
|
||
actions.active.usable = 1
|
||
elseif pfUI and pfUI.bars and actions.active.oom then
|
||
actions.active.usable = 2 -- pfUI: out of mana/rage
|
||
else
|
||
actions.active.usable = nil
|
||
end
|
||
-- Use Nampower's IsSpellUsable if available (stance-aware fallback)
|
||
elseif IsSpellUsable then
|
||
-- pcall to handle spells not in spellbook (Nampower throws error)
|
||
local ok, usable, oom = pcall(IsSpellUsable, actions.active.action)
|
||
if ok and usable == 1 and oom ~= 1 then
|
||
actions.active.usable = (pfUI and pfUI.bars) and nil or 1
|
||
else
|
||
actions.active.usable = nil
|
||
end
|
||
actions.active.oom = false
|
||
elseif not CleveRoids.IsReactiveUsable(actions.active.action) then
|
||
actions.active.oom = false
|
||
actions.active.usable = nil
|
||
else
|
||
actions.active.usable = (pfUI and pfUI.bars) and nil or 1
|
||
end
|
||
end
|
||
elseif isUsableBySpell == 1 and actions.active.inRange ~= 0 then
|
||
-- Use IsUsableSpell result if available (handles forms/stances correctly)
|
||
actions.active.usable = 1
|
||
elseif isUsableBySpell == 0 then
|
||
-- IsSpellUsable returned 0 = wrong stance/form, not enough power type, etc.
|
||
actions.active.usable = nil
|
||
elseif actions.active.inRange ~= 0 and not actions.active.oom then
|
||
-- Fallback to mana check ONLY if IsSpellUsable not available
|
||
actions.active.usable = 1
|
||
|
||
-- pfUI:actionbar.lua -- update usable [out-of-range = 1, oom = 2, not-usable = 3, default = 0]
|
||
elseif pfUI and pfUI.bars and actions.active.oom then
|
||
actions.active.usable = 2
|
||
else
|
||
actions.active.usable = nil
|
||
end
|
||
|
||
-- Check if this is a toggled buff ability (Prowl, Shadowmeld) and darken if buff is active
|
||
-- This must come AFTER all other usability checks to have final say
|
||
-- PERFORMANCE: Cache normalized spell name on the action object to avoid gsub per-frame
|
||
local spellName = actions.active._normalizedName
|
||
if not spellName then
|
||
spellName = string.gsub(actions.active.action, "%s*%(.-%)%s*$", "")
|
||
spellName = string.gsub(spellName, "_", " ")
|
||
actions.active._normalizedName = spellName
|
||
end
|
||
|
||
-- PERFORMANCE: Use cached lookup instead of creating table per-call
|
||
if CleveRoids.IsToggledBuffAbility(spellName) then
|
||
if CleveRoids.ValidatePlayerBuff(spellName) then
|
||
-- Buff is active, darken the icon like "wrong stance" (grayed out, not red)
|
||
actions.active.usable = nil
|
||
actions.active.oom = false -- Make sure we don't show red tint
|
||
end
|
||
end
|
||
else
|
||
actions.active.inRange = 1
|
||
actions.active.usable = 1
|
||
end
|
||
if actions.active.usable ~= previousUsable or
|
||
actions.active.oom ~= previousOom or
|
||
actions.active.inRange ~= previousInRange then
|
||
changed = true
|
||
if CleveRoids.debug and actions.active.isReactive then
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cffff00ff[STATE CHANGED]|r %s: usable %s->%s, will send ACTIONBAR_SLOT_CHANGED",
|
||
actions.active.action, tostring(previousUsable), tostring(actions.active.usable))
|
||
)
|
||
end
|
||
end
|
||
end
|
||
return changed
|
||
end
|
||
|
||
-- PERFORMANCE: Static buffer references for hot path
|
||
local _actionsToSlotsBuffer = CleveRoids._actionsToSlotsBuffer
|
||
local _slotsBuffer = CleveRoids._slotsBuffer
|
||
local _actionsListBuffer = CleveRoids._actionsListBuffer
|
||
|
||
function CleveRoids.TestForAllActiveActions()
|
||
-- PERFORMANCE: Reuse static buffers instead of creating new tables each call
|
||
-- Group slots by their actions object to handle shared macro references
|
||
local actionsToSlots = _actionsToSlotsBuffer
|
||
local actionsList = _actionsListBuffer
|
||
local actionsCount = 0
|
||
|
||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||
local Actions = CleveRoids.Actions
|
||
local slot, actions = next(Actions)
|
||
while slot do
|
||
if not actionsToSlots[actions] then
|
||
-- Reuse or create slots array from pool
|
||
local slots = _slotsBuffer[actions]
|
||
if not slots then
|
||
slots = {}
|
||
_slotsBuffer[actions] = slots
|
||
end
|
||
slots[1] = slot
|
||
slots._count = 1
|
||
actionsToSlots[actions] = slots
|
||
actionsCount = actionsCount + 1
|
||
actionsList[actionsCount] = actions
|
||
else
|
||
local slots = actionsToSlots[actions]
|
||
local count = slots._count + 1
|
||
slots[count] = slot
|
||
slots._count = count
|
||
end
|
||
slot, actions = next(Actions, slot)
|
||
end
|
||
|
||
-- Test each unique actions object once and send events to ALL slots that share it
|
||
for i = 1, actionsCount do
|
||
local actions = actionsList[i]
|
||
local slots = actionsToSlots[actions]
|
||
local stateChanged = CleveRoids.TestForActiveAction(actions)
|
||
if stateChanged then
|
||
-- Send event to ALL slots that use this macro
|
||
local count = slots._count
|
||
for j = 1, count do
|
||
CleveRoids.SendEventForAction(slots[j], "ACTIONBAR_SLOT_CHANGED", slots[j])
|
||
end
|
||
end
|
||
-- Clear for reuse (reset count and clear buffer reference)
|
||
for j = 1, slots._count do
|
||
slots[j] = nil
|
||
end
|
||
slots._count = 0
|
||
actionsToSlots[actions] = nil
|
||
actionsList[i] = nil -- Clear actionsList entry for reuse
|
||
end
|
||
end
|
||
|
||
function CleveRoids.ClearAction(slot)
|
||
if not CleveRoids.Actions[slot] then return end
|
||
CleveRoids.Actions[slot].active = nil
|
||
CleveRoids.Actions[slot] = nil
|
||
end
|
||
|
||
function CleveRoids.GetAction(slot)
|
||
if not slot or not CleveRoids.ready then return end
|
||
|
||
local actions = CleveRoids.Actions[slot]
|
||
if actions then return actions end
|
||
|
||
local text = GetActionText(slot)
|
||
|
||
if text then
|
||
local macro = CleveRoids.GetMacro(text)
|
||
if macro then
|
||
actions = macro.actions
|
||
|
||
CleveRoids.TestForActiveAction(actions)
|
||
CleveRoids.Actions[slot] = actions
|
||
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
|
||
return actions
|
||
end
|
||
end
|
||
end
|
||
|
||
function CleveRoids.GetActiveAction(slot)
|
||
local action = CleveRoids.GetAction(slot)
|
||
return action and action.active
|
||
end
|
||
|
||
-- PERFORMANCE: Static buffer for arg backup
|
||
local _originalArgsBuffer = CleveRoids._originalArgsBuffer
|
||
|
||
function CleveRoids.SendEventForAction(slot, event, ...)
|
||
local old_this = this
|
||
|
||
-- PERFORMANCE: Reuse static buffer instead of creating table each call
|
||
local original_global_args = _originalArgsBuffer
|
||
for i = 1, 10 do
|
||
original_global_args[i] = _G["arg" .. i]
|
||
end
|
||
|
||
if type(arg) == "table" then
|
||
|
||
local n_varargs_from_arg_table = arg.n or 0
|
||
for i = 1, 10 do
|
||
if i <= n_varargs_from_arg_table then
|
||
_G["arg" .. i] = arg[i]
|
||
else
|
||
_G["arg" .. i] = nil
|
||
end
|
||
end
|
||
else
|
||
for i = 1, 10 do
|
||
_G["arg" .. i] = nil
|
||
end
|
||
end
|
||
|
||
local button_to_call_event_on
|
||
local page = floor((slot - 1) / NUM_ACTIONBAR_BUTTONS) + 1
|
||
local pageSlot = slot - (page - 1) * NUM_ACTIONBAR_BUTTONS
|
||
|
||
if slot >= 73 then
|
||
button_to_call_event_on = _G["BonusActionButton" .. pageSlot]
|
||
elseif slot >= 61 then
|
||
button_to_call_event_on = _G["MultiBarBottomLeftButton" .. pageSlot]
|
||
elseif slot >= 49 then
|
||
button_to_call_event_on = _G["MultiBarBottomRightButton" .. pageSlot]
|
||
elseif slot >= 37 then
|
||
button_to_call_event_on = _G["MultiBarLeftButton" .. pageSlot]
|
||
elseif slot >= 25 then
|
||
button_to_call_event_on = _G["MultiBarRightButton" .. pageSlot]
|
||
end
|
||
|
||
if button_to_call_event_on then
|
||
this = button_to_call_event_on
|
||
ActionButton_OnEvent(event)
|
||
end
|
||
|
||
if page == CURRENT_ACTIONBAR_PAGE then
|
||
local main_bar_button = _G["ActionButton" .. pageSlot]
|
||
if main_bar_button and main_bar_button ~= button_to_call_event_on then
|
||
this = main_bar_button
|
||
ActionButton_OnEvent(event)
|
||
elseif not button_to_call_event_on and main_bar_button then
|
||
this = main_bar_button
|
||
ActionButton_OnEvent(event)
|
||
end
|
||
end
|
||
|
||
this = old_this
|
||
|
||
for i = 1, 10 do
|
||
_G["arg" .. i] = original_global_args[i]
|
||
end
|
||
|
||
if type(arg) == "table" and arg.n then
|
||
|
||
if arg.n == 0 then
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event) end
|
||
elseif arg.n == 1 then
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1]) end
|
||
elseif arg.n == 2 then
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2]) end
|
||
elseif arg.n == 3 then
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3]) end
|
||
elseif arg.n == 4 then
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4]) end
|
||
elseif arg.n == 5 then
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5]) end
|
||
elseif arg.n == 6 then
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5], arg[6]) end
|
||
else
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5], arg[6], arg[7]) end
|
||
end
|
||
else
|
||
|
||
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do
|
||
fn_h(slot, event)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Executes the given Macro's body
|
||
-- body: The Macro's body
|
||
function CleveRoids.ExecuteMacroBody(body,inline)
|
||
local lines = CleveRoids.splitString(body, "\n")
|
||
if inline then lines = CleveRoids.splitString(body, "\\n"); end
|
||
|
||
-- Clear stopmacro flag at start of macro execution
|
||
CleveRoids.stopMacroFlag = false
|
||
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cff00ffff[MacroRef]|r ExecuteMacroBody called with " .. table.getn(lines) .. " lines")
|
||
end
|
||
|
||
for k,v in pairs(lines) do
|
||
-- Check stopmacro flag before each line
|
||
if CleveRoids.stopMacroFlag then
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cffff8800[MacroRef]|r Stopped at line " .. k .. " due to /stopmacro")
|
||
end
|
||
break
|
||
end
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cff88ff88[MacroRef]|r Executing line " .. k .. ": " .. string.sub(v, 1, 60))
|
||
end
|
||
ChatFrameEditBox:SetText(v)
|
||
ChatEdit_SendText(ChatFrameEditBox)
|
||
end
|
||
return true
|
||
end
|
||
|
||
-- Gets the body of the Macro with the given name
|
||
-- name: The name of the Macro
|
||
-- returns: The body of the macro
|
||
function CleveRoids.GetMacroBody(name)
|
||
local macro = CleveRoids.GetMacro(name)
|
||
return macro and macro.body
|
||
end
|
||
|
||
-- Attempts to execute a macro by the given name or index (Blizzard or Super tab)
|
||
-- Returns: true if something was executed, false otherwise
|
||
-- Supports: macro name (string), macro index (number or numeric string like "19")
|
||
-- Uses CleveRoids' own ExecuteMacroBody for processing (supports enhanced syntax)
|
||
function CleveRoids.ExecuteMacroByName(name)
|
||
if not name or name == "" then
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cffff0000[MacroRef]|r ExecuteMacroByName called with empty name")
|
||
end
|
||
return false
|
||
end
|
||
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cff00ffff[MacroRef]|r ExecuteMacroByName called with: '" .. name .. "'")
|
||
end
|
||
|
||
local body
|
||
local source = nil
|
||
|
||
-- Check if input is a numeric index (e.g., "19" or 19)
|
||
local numericIndex = tonumber(name)
|
||
|
||
-- 1) Try Blizzard macro (by index or name)
|
||
if numericIndex then
|
||
-- Numeric index - get body directly (supports character macros 19-36)
|
||
local _n, _tex, b = GetMacroInfo(numericIndex)
|
||
if b and b ~= "" then
|
||
body = b
|
||
source = "Blizzard (index " .. numericIndex .. ")"
|
||
end
|
||
else
|
||
-- String name - look up by name
|
||
local id = GetMacroIndexByName(name)
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cff888888[MacroRef]|r GetMacroIndexByName('" .. name .. "') = " .. tostring(id))
|
||
end
|
||
if id and id ~= 0 then
|
||
local _n, _tex, b = GetMacroInfo(id)
|
||
if b and b ~= "" then
|
||
body = b
|
||
source = "Blizzard (slot " .. id .. ")"
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 2) SuperMacro's Super macros (by name only, not index)
|
||
-- These are the 7000-char extended macros stored in SM_SUPER
|
||
if not body and not numericIndex then
|
||
if type(GetSuperMacroInfo) == "function" then
|
||
local _n2, _t2, b2 = GetSuperMacroInfo(name)
|
||
if b2 and b2 ~= "" then
|
||
body = b2
|
||
source = "SuperMacro"
|
||
end
|
||
end
|
||
end
|
||
|
||
-- 3) CRM cache fallback (by name only)
|
||
if not body and not numericIndex then
|
||
if type(CleveRoids.GetMacro) == "function" then
|
||
local m = CleveRoids.GetMacro(name)
|
||
if m and m.body and m.body ~= "" then
|
||
body = m.body
|
||
source = "CRM cache"
|
||
end
|
||
end
|
||
end
|
||
|
||
if not body or body == "" then
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cffff0000[MacroRef]|r Macro '" .. name .. "' not found in any source")
|
||
end
|
||
return false
|
||
end
|
||
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cff00ff00[MacroRef]|r Found macro '" .. name .. "' from " .. source)
|
||
end
|
||
|
||
-- Execute using CleveRoids' processor (supports conditionals, extended syntax)
|
||
return CleveRoids.ExecuteMacroBody(body)
|
||
end
|
||
|
||
function CleveRoids.SetHelp(conditionals)
|
||
if conditionals.harm then
|
||
conditionals.help = false
|
||
end
|
||
end
|
||
|
||
function CleveRoids.FixEmptyTarget(conditionals)
|
||
if not conditionals.target then
|
||
if UnitExists("target") then
|
||
conditionals.target = "target"
|
||
elseif GetCVar("autoSelfCast") == "1" and not conditionals.target == "help" then
|
||
conditionals.target = "player"
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Fixes the conditionals' target by targeting the target with the given name
|
||
-- conditionals: The conditionals containing the current target
|
||
-- name: The name of the player to target
|
||
-- hook: The target hook
|
||
-- returns: Whether or not we've changed the player's current target
|
||
function CleveRoids.FixEmptyTargetSetTarget(conditionals, name, hook)
|
||
if not conditionals.target then
|
||
hook(name)
|
||
conditionals.target = "target"
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Returns the name of the focus target or nil
|
||
function CleveRoids.GetFocusName()
|
||
-- 1. Add specific compatibility for pfUI.
|
||
-- pfUI stores its focus unit information in a table.
|
||
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.unitname then
|
||
return pfUI.uf.focus.unitname
|
||
end
|
||
|
||
-- Fallback for other focus addons
|
||
if ClassicFocus_CurrentFocus then
|
||
return ClassicFocus_CurrentFocus
|
||
elseif CURR_FOCUS_TARGET then
|
||
return CURR_FOCUS_TARGET
|
||
end
|
||
|
||
return nil
|
||
end
|
||
|
||
-- Attempts to target the focus target.
|
||
-- returns: Whether or not it succeeded
|
||
function CleveRoids.TryTargetFocus()
|
||
local name = CleveRoids.GetFocusName()
|
||
|
||
if not name then
|
||
return false
|
||
end
|
||
|
||
TargetByName(name, true)
|
||
|
||
if not UnitExists("target") or (string.lower(UnitName("target")) ~= name) then
|
||
-- The target switch failed (out of range, LoS, etc.)
|
||
return false
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
function CleveRoids.GetMacroNameFromAction(text)
|
||
if string.sub(text, 1, 1) == "{" and string.sub(text, -1) == "}" then
|
||
local name
|
||
if string.sub(text, 2, 2) == "\"" and string.sub(text, -2, -2) == "\"" then
|
||
return string.sub(text, 3, -3)
|
||
else
|
||
return string.sub(text, 2, -2)
|
||
end
|
||
end
|
||
end
|
||
|
||
function CleveRoids.CreateActionInfo(action, conditionals)
|
||
local _, _, text = string.find(action, "!?%??~?(.*)")
|
||
local spell = CleveRoids.GetSpell(text)
|
||
local petSpell -- Add this line
|
||
local item, macroName, macro, macroTooltip, actionType, texture
|
||
|
||
-- NEW: Check if the action is a slot number
|
||
local slotId = tonumber(text)
|
||
if slotId and slotId >= 1 and slotId <= 19 then
|
||
actionType = "item"
|
||
local itemTexture = GetInventoryItemTexture("player", slotId)
|
||
if itemTexture then
|
||
texture = itemTexture
|
||
else
|
||
texture = CleveRoids.unknownTexture
|
||
end
|
||
else
|
||
-- Original logic for named items and spells
|
||
if not spell then
|
||
petSpell = CleveRoids.GetPetSpell(text) -- Add this line
|
||
end
|
||
if not spell and not petSpell then -- Modify this line
|
||
item = CleveRoids.GetItem(text)
|
||
end
|
||
if not item and not petSpell then -- Modify this line
|
||
macroName = CleveRoids.GetMacroNameFromAction(text)
|
||
macro = CleveRoids.GetMacro(macroName)
|
||
macroTooltip = (macro and macro.actions) and macro.actions.tooltip
|
||
end
|
||
|
||
if spell then
|
||
actionType = "spell"
|
||
texture = spell.texture or CleveRoids.unknownTexture
|
||
elseif petSpell then -- Add this block
|
||
actionType = "petspell"
|
||
texture = petSpell.texture or CleveRoids.unknownTexture
|
||
elseif item then
|
||
actionType = "item"
|
||
texture = (item and item.texture) or CleveRoids.unknownTexture
|
||
elseif macro then
|
||
actionType = "macro"
|
||
texture = (macro.actions and macro.actions.tooltip and macro.actions.tooltip.texture)
|
||
or (macro and macro.texture)
|
||
or CleveRoids.unknownTexture
|
||
end
|
||
end
|
||
|
||
local info = {
|
||
action = text,
|
||
item = item,
|
||
spell = spell,
|
||
petSpell = petSpell, -- Add this line
|
||
macro = macroTooltip,
|
||
type = actionType,
|
||
texture = texture,
|
||
conditionals = conditionals,
|
||
}
|
||
|
||
return info
|
||
end
|
||
|
||
function CleveRoids.SplitCommandAndArgs(text)
|
||
local _, _, cmd, args = string.find(text, "(/%w+%s?)(.*)")
|
||
if cmd and args then
|
||
cmd = CleveRoids.Trim(cmd)
|
||
text = CleveRoids.Trim(args)
|
||
end
|
||
return cmd, args
|
||
end
|
||
|
||
function CleveRoids.ParseSequence(text)
|
||
if not text or text == "" then return end
|
||
|
||
-- normalize commas
|
||
local args = string.gsub(text, "(%s*,%s*)", ",")
|
||
|
||
-- optional [conditionals] block
|
||
local _, condEnd, condBlock = string.find(args, "(%[.*%])")
|
||
|
||
-- accept reset= anywhere; no trailing space required; strip it out once
|
||
local _, _, resetVal = string.find(args, "[Rr][Ee][Ss][Ee][Tt]=([%w/]+)")
|
||
if resetVal then
|
||
args = string.gsub(args, "%s*[Rr][Ee][Ss][Ee][Tt]=[%w/]+%s*", " ", 1)
|
||
end
|
||
|
||
-- actions are what's left after any ] (if present)
|
||
local actionSeq = CleveRoids.Trim((condEnd and string.sub(args, condEnd + 1)) or args)
|
||
args = (condBlock or "") .. actionSeq
|
||
if actionSeq == "" then return end
|
||
|
||
local sequence = {
|
||
index = 1,
|
||
reset = {},
|
||
status = 0,
|
||
lastUpdate = 0,
|
||
args = args,
|
||
list = {},
|
||
}
|
||
|
||
-- fill reset rules: seconds or flags (target/combat/alt/ctrl/shift)
|
||
if resetVal then
|
||
for _, rule in pairs(CleveRoids.Split(resetVal, "/")) do
|
||
rule = string.lower(CleveRoids.Trim(rule))
|
||
local secs = tonumber(rule)
|
||
if secs and secs > 0 then
|
||
sequence.reset.secs = secs
|
||
else
|
||
sequence.reset[rule] = true
|
||
end
|
||
end
|
||
end
|
||
|
||
-- build steps
|
||
for _, a in ipairs(CleveRoids.Split(actionSeq, ",")) do
|
||
local sa = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(a))
|
||
table.insert(sequence.list, sa)
|
||
end
|
||
|
||
CleveRoids.Sequences[text] = sequence
|
||
return sequence
|
||
end
|
||
|
||
function CleveRoids.ParseMacro(name)
|
||
if not name then return end
|
||
|
||
local macroID = GetMacroIndexByName(name)
|
||
|
||
local _, texture, body
|
||
if macroID and macroID ~= 0 then
|
||
_, texture, body = GetMacroInfo(macroID)
|
||
end
|
||
|
||
if (not body) and GetSuperMacroInfo then
|
||
_, texture, body = GetSuperMacroInfo(name)
|
||
end
|
||
|
||
if not texture or not body then return end
|
||
|
||
local macro = {
|
||
id = macroID,
|
||
name = name,
|
||
texture = texture,
|
||
body = body,
|
||
actions = {},
|
||
}
|
||
macro.actions.list = {}
|
||
|
||
-- build a list of testable actions for the macro
|
||
local hasShowTooltip = false
|
||
local showTooltipHasArg = false
|
||
|
||
for i, line in ipairs(CleveRoids.splitString(body, "\n")) do
|
||
line = CleveRoids.Trim(line)
|
||
local cmd, args = CleveRoids.SplitCommandAndArgs(line)
|
||
|
||
-- check for #showtooltip
|
||
if i == 1 then
|
||
local _, _, st, _, tt = string.find(line, "(#showtooltip)(%s?(.*))")
|
||
|
||
-- if no #showtooltip, nothing to keep track of
|
||
if not st then
|
||
break
|
||
end
|
||
|
||
hasShowTooltip = true
|
||
tt = CleveRoids.Trim(tt)
|
||
|
||
-- #showtooltip and item/spell/macro specified, only use this tooltip
|
||
if st and tt ~= "" then
|
||
showTooltipHasArg = true
|
||
for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(tt)) do
|
||
-- Parse the arg to extract the spell name from any conditionals
|
||
local parsedArg = CleveRoids.GetParsedMsg(arg)
|
||
macro.actions.tooltip = CleveRoids.CreateActionInfo(parsedArg)
|
||
local action = CleveRoids.CreateActionInfo(parsedArg)
|
||
action.cmd = "/cast"
|
||
action.args = arg
|
||
action.isReactive = CleveRoids.reactiveSpells[action.action]
|
||
table.insert(macro.actions.list, action)
|
||
end
|
||
break
|
||
end
|
||
else
|
||
-- make sure we have a testable action
|
||
if line ~= "" and args ~= "" and CleveRoids.dynamicCmds[cmd] then
|
||
for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(args)) do
|
||
local action = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(arg))
|
||
|
||
if cmd == "/castsequence" then
|
||
local sequence = CleveRoids.GetSequence(args)
|
||
if sequence then
|
||
action.sequence = sequence
|
||
end
|
||
end
|
||
action.cmd = cmd
|
||
action.args = arg
|
||
action.isReactive = CleveRoids.reactiveSpells[action.action]
|
||
table.insert(macro.actions.list, action)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- If #showtooltip was present but had no argument, use the first action as the tooltip
|
||
if hasShowTooltip and not showTooltipHasArg and table.getn(macro.actions.list) > 0 then
|
||
macro.actions.tooltip = macro.actions.list[1]
|
||
end
|
||
|
||
-- Store whether #showtooltip had an explicit argument (for icon fallback logic)
|
||
macro.actions.explicitTooltip = showTooltipHasArg
|
||
|
||
CleveRoids.Macros[name] = macro
|
||
return macro
|
||
end
|
||
|
||
function CleveRoids.ParseMsg(msg)
|
||
if not msg then return end
|
||
local conditionals = {}
|
||
|
||
-- reset side flag for this parse
|
||
CleveRoids._ignoretooltip = 0
|
||
|
||
-- strip optional leading '?' and remember how many we stripped
|
||
local ignorecount
|
||
msg, ignorecount = string.gsub(CleveRoids.Trim(msg), "^%?", "")
|
||
conditionals.ignoretooltip = ignorecount
|
||
CleveRoids._ignoretooltip = ignorecount
|
||
|
||
-- capture a single [...] conditional block if present
|
||
local _, cbEnd, conditionBlock = string.find(msg, "%[(.+)%]")
|
||
|
||
-- split off flags/action after the condition block (or from start if none)
|
||
local _, _, noSpam, cancelAura, action = string.find(
|
||
string.sub(msg, (cbEnd or 0) + 1),
|
||
"^%s*(!?)(~?)([^!~]+.*)"
|
||
)
|
||
action = CleveRoids.Trim(action or "")
|
||
|
||
-- store the raw action for callers and strip trailing "(Rank X)" for comparisons
|
||
conditionals.action = action
|
||
action = string.gsub(action, "%s*%(%s*Rank%s+%d+%s*%)%s*$", "")
|
||
|
||
-- IMPORTANT: if there's NO conditional block, return nil conditionals so
|
||
-- DoWithConditionals will hit the {macroName} execution branch.
|
||
if not conditionBlock then
|
||
local hasFlag = (noSpam and noSpam ~= "") or (cancelAura and cancelAura ~= "")
|
||
if hasFlag and action ~= "" then
|
||
if noSpam ~= "" then
|
||
local spamCond = CleveRoids.GetSpammableConditional(action)
|
||
if spamCond then
|
||
conditionals[spamCond] = { action }
|
||
-- Also create _groups entry for consistency with Multi()
|
||
if not conditionals._groups then
|
||
conditionals._groups = {}
|
||
end
|
||
conditionals._groups[spamCond] = { { values = { action }, operator = "OR" } }
|
||
end
|
||
end
|
||
if cancelAura ~= "" then
|
||
conditionals.cancelaura = action
|
||
end
|
||
return conditionals.action, conditionals
|
||
end
|
||
return conditionals.action, nil
|
||
end
|
||
|
||
-- With a condition block present, build out the conditionals table
|
||
|
||
-- optional spam/cancel flags (apply only when we actually have a [] block)
|
||
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
|
||
end
|
||
if cancelAura and cancelAura ~= "" then
|
||
conditionals.cancelaura = action
|
||
end
|
||
|
||
-- Set the action's target to @unitid if found (e.g., @mouseover)
|
||
local _, _, target = string.find(conditionBlock, "(@[^%s,]+)")
|
||
if target then
|
||
conditionBlock = CleveRoids.Trim(string.gsub(conditionBlock, target, ""))
|
||
conditionals.target = string.sub(target, 2)
|
||
end
|
||
|
||
if conditionBlock and conditionals.action then
|
||
-- Split the conditional block by comma or space
|
||
for _, conditionGroups in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do
|
||
if conditionGroups ~= "" then
|
||
-- Split conditional groups by colon
|
||
local conditionGroup = CleveRoids.splitStringIgnoringQuotes(conditionGroups, ":")
|
||
local condition, args = conditionGroup[1], conditionGroup[2]
|
||
|
||
-- No args → the action is the implicit argument
|
||
if not args or args == "" then
|
||
if not conditionals[condition] then
|
||
-- PERFORMANCE: Use module-level constant instead of creating table per-call
|
||
if BOOLEAN_CONDITIONALS[condition] then
|
||
conditionals[condition] = true
|
||
else
|
||
conditionals[condition] = conditionals.action
|
||
-- Create first group for non-boolean conditionals
|
||
if not conditionals._groups then
|
||
conditionals._groups = {}
|
||
end
|
||
conditionals._groups[condition] = { { values = { conditionals.action }, operator = "OR" } }
|
||
end
|
||
else
|
||
-- existing code for when conditionals[condition] already exists
|
||
if type(conditionals[condition]) ~= "table" then
|
||
conditionals[condition] = { conditionals[condition] }
|
||
end
|
||
table.insert(conditionals[condition], conditionals.action)
|
||
|
||
-- Multiple instances of same conditional = AND logic
|
||
if not conditionals._operators then
|
||
conditionals._operators = {}
|
||
end
|
||
conditionals._operators[condition] = "AND"
|
||
|
||
-- Add new group for this instance
|
||
if not conditionals._groups then
|
||
conditionals._groups = {}
|
||
end
|
||
if not conditionals._groups[condition] then
|
||
conditionals._groups[condition] = {}
|
||
end
|
||
table.insert(conditionals._groups[condition], { values = { conditionals.action }, operator = "OR" })
|
||
end
|
||
else
|
||
-- Has args. Ensure the key's value is a table and add new arguments.
|
||
-- Track if this conditional already existed (repeated via comma)
|
||
local conditionAlreadyExists = conditionals[condition] ~= nil
|
||
|
||
if not conditionals[condition] then
|
||
conditionals[condition] = {}
|
||
elseif type(conditionals[condition]) ~= "table" then
|
||
conditionals[condition] = { conditionals[condition] }
|
||
end
|
||
|
||
-- Detect which separator is used: / (OR) or & (AND)
|
||
-- Initialize metadata tables if needed
|
||
if not conditionals._operators then
|
||
conditionals._operators = {}
|
||
end
|
||
if not conditionals._groups then
|
||
conditionals._groups = {}
|
||
end
|
||
|
||
-- Check which separator is present in the CURRENT args
|
||
local hasSlash = string.find(args, "/")
|
||
local hasAmpersand = string.find(args, "&")
|
||
|
||
-- Detect if & is part of a multi-comparison pattern (e.g., >0&<10)
|
||
-- Pattern: operator+number followed by & (with optional whitespace) followed by operator
|
||
-- IMPORTANT: Only whitespace allowed around &, NOT letters (to distinguish from Rip>3&Rake>3)
|
||
local isMultiComparison = hasAmpersand and string.find(args, "[>~=<]+%d+%.?%d*%s*&%s*[>~=<]")
|
||
|
||
local separator = "/"
|
||
local operatorType = "OR"
|
||
|
||
if hasAmpersand and not hasSlash and not isMultiComparison then
|
||
separator = "&"
|
||
operatorType = "AND"
|
||
elseif hasAmpersand and hasSlash then
|
||
-- Both separators present - default to / (OR) and warn
|
||
-- Could add a warning here in the future
|
||
separator = "/"
|
||
operatorType = "OR"
|
||
end
|
||
|
||
-- Store the operator type for this conditional (for backwards compat)
|
||
-- Note: when groups exist, Multi/NegatedMulti will use per-group operators
|
||
conditionals._operators[condition] = operatorType
|
||
|
||
-- Create a new group for this conditional instance
|
||
-- Structure: { values = { ... }, operator = "OR" or "AND" }
|
||
if not conditionals._groups[condition] then
|
||
conditionals._groups[condition] = {}
|
||
end
|
||
local currentGroup = { values = {}, operator = operatorType }
|
||
table.insert(conditionals._groups[condition], currentGroup)
|
||
|
||
-- Split args by the determined separator
|
||
for _, arg_item in CleveRoids.splitString(args, separator) do
|
||
local processed_arg = CleveRoids.Trim(arg_item)
|
||
|
||
processed_arg = string.gsub(processed_arg, '"', "")
|
||
processed_arg = string.gsub(processed_arg, "_", " ")
|
||
processed_arg = CleveRoids.Trim(processed_arg)
|
||
|
||
-- normalize "name#N" → "name=#N" and "#N" → "=#N"
|
||
local arg_for_find = processed_arg
|
||
arg_for_find = string.gsub(arg_for_find, "^#(%d+)$", "=#%1")
|
||
arg_for_find = string.gsub(arg_for_find, "([^>~=<]+)#(%d+)", "%1=#%2")
|
||
|
||
-- accept decimals too; capture name/op/amount
|
||
local _, _, name, operator, amount = string.find(arg_for_find, "([^>~=<]*)([>~=<]+)(#?%d*%.?%d+)")
|
||
|
||
if not operator or not amount then
|
||
-- No operator found, treat as simple string argument
|
||
table.insert(conditionals[condition], processed_arg)
|
||
table.insert(currentGroup.values, processed_arg)
|
||
else
|
||
local name_to_use = (name and name ~= "") and name or conditionals.action
|
||
local final_amount_str, num_replacements = string.gsub(amount, "#", "")
|
||
local should_check_stacks = (num_replacements == 1)
|
||
|
||
-- SPECIAL HANDLING FOR CONDITIONALS WITH MULTIPLE COMPARISONS
|
||
-- Detect if this conditional has multiple operators
|
||
-- Example: "ap>1800/<2200" or "Recently_Bandaged>0&<10" or "health>50&<80"
|
||
-- Works with ANY conditional that supports numeric operators
|
||
-- Pattern: only whitespace or separators allowed between comparisons, NOT letters
|
||
if string.find(processed_arg, "[>~=<]+%d+%.?%d*%s*[/&]%s*[>~=<]") then
|
||
-- This arg has multiple comparisons, parse them all
|
||
local stat_name = name_to_use
|
||
local comparisons = {}
|
||
|
||
-- Extract all operator+number pairs
|
||
-- Pattern matches operator followed by optional decimal number
|
||
local gfind_func = string.gfind or string.gmatch
|
||
for op, num in gfind_func(processed_arg, "([>~=<]+)(#?%d*%.?%d+)") do
|
||
local clean_num = string.gsub(num, "#", "")
|
||
local check_stacks = (string.find(num, "#") ~= nil)
|
||
table.insert(comparisons, {
|
||
operator = op,
|
||
amount = tonumber(clean_num),
|
||
checkStacks = check_stacks
|
||
})
|
||
end
|
||
|
||
if table.getn(comparisons) > 0 then
|
||
local entry = {
|
||
name = CleveRoids.Trim(stat_name),
|
||
comparisons = comparisons -- Store all comparisons
|
||
}
|
||
table.insert(conditionals[condition], entry)
|
||
table.insert(currentGroup.values, entry)
|
||
else
|
||
-- Fallback to single comparison if parsing failed
|
||
local entry = {
|
||
name = CleveRoids.Trim(name_to_use),
|
||
operator = operator,
|
||
amount = tonumber(final_amount_str),
|
||
checkStacks = should_check_stacks
|
||
}
|
||
table.insert(conditionals[condition], entry)
|
||
table.insert(currentGroup.values, entry)
|
||
end
|
||
else
|
||
-- Normal single-comparison conditional (existing behavior)
|
||
local entry = {
|
||
name = CleveRoids.Trim(name_to_use),
|
||
operator = operator,
|
||
amount = tonumber(final_amount_str),
|
||
checkStacks = should_check_stacks
|
||
}
|
||
table.insert(conditionals[condition], entry)
|
||
table.insert(currentGroup.values, entry)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return conditionals.action, conditionals
|
||
end
|
||
end
|
||
|
||
|
||
-- Get previously parsed or parse, store and return
|
||
function CleveRoids.GetParsedMsg(msg)
|
||
if not msg then return end
|
||
|
||
-- PERFORMANCE: Check cache first before doing string operations
|
||
local cached = CleveRoids.ParsedMsg[msg]
|
||
if cached then
|
||
-- Use cached ignoretooltip value (already computed during first parse)
|
||
CleveRoids._ignoretooltip = cached.ignoretooltip or 0
|
||
return cached.action, cached.conditionals
|
||
end
|
||
|
||
-- Only compute ignoretooltip for new messages (not cache hits)
|
||
-- PERFORMANCE: Use string.sub for simple prefix check instead of gsub
|
||
local trimmed = CleveRoids.Trim(msg)
|
||
local ignorecount = (string.sub(trimmed, 1, 1) == "?") and 1 or 0
|
||
CleveRoids._ignoretooltip = ignorecount
|
||
|
||
local action, conditionals = CleveRoids.ParseMsg(msg)
|
||
CleveRoids.ParsedMsg[msg] = {
|
||
action = action,
|
||
conditionals = conditionals,
|
||
ignoretooltip = ignorecount,
|
||
}
|
||
return action, conditionals
|
||
end
|
||
|
||
|
||
function CleveRoids.GetMacro(name)
|
||
return CleveRoids.Macros[name] or CleveRoids.ParseMacro(name)
|
||
end
|
||
|
||
function CleveRoids.GetSequence(args)
|
||
return CleveRoids.Sequences[args] or CleveRoids.ParseSequence(args)
|
||
end
|
||
|
||
function CleveRoids.GetCurrentSequenceAction(sequence)
|
||
return sequence.list[sequence.index]
|
||
end
|
||
|
||
function CleveRoids.ResetSequence(sequence)
|
||
sequence.index = 1
|
||
end
|
||
|
||
function CleveRoids.AdvanceSequence(sequence)
|
||
if sequence.index < table.getn(sequence.list) then
|
||
-- Not at the end yet, just advance normally
|
||
sequence.index = sequence.index + 1
|
||
else
|
||
-- At the end of sequence - check if we should auto-reset or stay at last step
|
||
local hasNonModifierReset = false
|
||
|
||
if sequence.reset then
|
||
-- Check if there are any reset conditions besides modifier keys
|
||
for k, _ in pairs(sequence.reset) do
|
||
-- target, combat, secs are non-modifier resets
|
||
if k ~= "alt" and k ~= "ctrl" and k ~= "shift" then
|
||
hasNonModifierReset = true
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Only auto-reset if:
|
||
-- 1. No reset table exists at all, OR
|
||
-- 2. Reset table only contains modifier keys (alt/ctrl/shift)
|
||
-- Otherwise, stay on the last step and keep casting it until reset fires
|
||
if not hasNonModifierReset then
|
||
CleveRoids.ResetSequence(sequence)
|
||
end
|
||
-- If hasNonModifierReset is true, sequence.index stays at max,
|
||
-- so GetCurrentSequenceAction will keep returning the last spell
|
||
end
|
||
end
|
||
|
||
function CleveRoids.TestAction(cmd, args)
|
||
local msg, conditionals = CleveRoids.GetParsedMsg(args)
|
||
|
||
-- Nil-safe guards
|
||
local hasShowTooltip = (type(msg) == "string") and string.find(msg, "#showtooltip")
|
||
local ignoreTooltip = ((type(conditionals) == "table") and (conditionals.ignoretooltip == 1))
|
||
or (CleveRoids._ignoretooltip == 1)
|
||
|
||
-- If the line is explicitly ignored for tooltip (via '?'),
|
||
-- or it is a '#showtooltip' token, do not contribute icon/tooltip.
|
||
if hasShowTooltip or ignoreTooltip then
|
||
CleveRoids._ignoretooltip = 0 -- clear for next parse
|
||
return
|
||
end
|
||
|
||
-- No [] block → return a testable token so the UI can pick a texture
|
||
if not conditionals then
|
||
if not msg or msg == "" then
|
||
return
|
||
else
|
||
return CleveRoids.GetMacroNameFromAction(msg) or msg
|
||
end
|
||
end
|
||
|
||
local origTarget = conditionals.target
|
||
if cmd == "" or not CleveRoids.dynamicCmds[cmd] then
|
||
return
|
||
end
|
||
|
||
if conditionals.target == "focus" then
|
||
local focusUnitId = nil
|
||
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id
|
||
and UnitExists(pfUI.uf.focus.label .. pfUI.uf.focus.id) then
|
||
focusUnitId = pfUI.uf.focus.label .. pfUI.uf.focus.id
|
||
end
|
||
if focusUnitId then
|
||
conditionals.target = focusUnitId
|
||
else
|
||
if not CleveRoids.GetFocusName() then
|
||
return
|
||
end
|
||
conditionals.target = "target"
|
||
end
|
||
end
|
||
|
||
if conditionals.target == "mouseover" then
|
||
if not CleveRoids.IsValidTarget("mouseover", conditionals.help) then
|
||
return false
|
||
end
|
||
end
|
||
|
||
CleveRoids.FixEmptyTarget(conditionals)
|
||
|
||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||
local k, v = next(conditionals)
|
||
while k do
|
||
if not CleveRoids.ignoreKeywords[k] then
|
||
if not CleveRoids.Keywords[k] or not CleveRoids.Keywords[k](conditionals) then
|
||
conditionals.target = origTarget
|
||
return
|
||
end
|
||
end
|
||
k, v = next(conditionals, k)
|
||
end
|
||
|
||
conditionals.target = origTarget
|
||
return CleveRoids.GetMacroNameFromAction(msg) or msg
|
||
end
|
||
|
||
function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBeforeAction, action)
|
||
local msg, conditionals = CleveRoids.GetParsedMsg(msg)
|
||
|
||
-- Debug: Log parsed msg and action type
|
||
if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then
|
||
CleveRoids.Print("|cff888888[EquipLog] DoWithConditionals: parsed msg='" .. tostring(msg) .. "' action=" .. tostring(action) .. "|r")
|
||
end
|
||
|
||
-- No conditionals. Just exit.
|
||
if not conditionals then
|
||
if not msg then -- if not even an empty string
|
||
return false
|
||
else
|
||
if string.sub(msg, 1, 1) == "{" and string.sub(msg, -1) == "}" then
|
||
if string.sub(msg, 2, 2) == "\"" and string.sub(msg, -2, -2) == "\"" then
|
||
return CleveRoids.ExecuteMacroBody(string.sub(msg, 3, -3), true)
|
||
else
|
||
return CleveRoids.ExecuteMacroByName(string.sub(msg, 2, -2))
|
||
end
|
||
end
|
||
|
||
if hook then
|
||
hook(msg)
|
||
end
|
||
return true
|
||
end
|
||
end
|
||
|
||
if conditionals.cancelaura then
|
||
if CleveRoids.CancelAura(conditionals.cancelaura) then
|
||
return true
|
||
end
|
||
end
|
||
|
||
local origTarget = conditionals.target
|
||
if conditionals.target == "mouseover" then
|
||
if UnitExists("mouseover") then
|
||
conditionals.target = "mouseover"
|
||
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
|
||
conditionals.target = CleveRoids.mouseoverUnit
|
||
else
|
||
conditionals.target = "mouseover"
|
||
end
|
||
end
|
||
|
||
local needRetarget = false
|
||
if fixEmptyTargetFunc then
|
||
needRetarget = fixEmptyTargetFunc(conditionals, msg, hook)
|
||
end
|
||
|
||
-- CleveRoids.SetHelp(conditionals)
|
||
|
||
if conditionals.target == "focus" then
|
||
local focusUnitId = nil
|
||
|
||
-- Attempt to get the direct UnitID from pfUI's focus frame data. This is more reliable.
|
||
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id and UnitExists(pfUI.uf.focus.label .. pfUI.uf.focus.id) then
|
||
focusUnitId = pfUI.uf.focus.label .. pfUI.uf.focus.id
|
||
end
|
||
|
||
if focusUnitId then
|
||
-- If we found a valid UnitID, we will use it for all subsequent checks and the final cast.
|
||
-- This avoids changing the player's actual target.
|
||
conditionals.target = focusUnitId
|
||
needRetarget = false
|
||
else
|
||
-- return false if pfUI is installed and no focus is set instead of "invalid target"
|
||
if pfUI and (pfUI.uf.focus.label == nil or pfUI.uf.focus.label == "") then return false end
|
||
-- If the direct UnitID isn't found, fall back to the original (but likely failing) method of targeting by name.
|
||
if not CleveRoids.TryTargetFocus() then
|
||
UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1.0, 0.0, 0.0, 1.0)
|
||
conditionals.target = origTarget
|
||
return false
|
||
end
|
||
conditionals.target = "target"
|
||
needRetarget = true
|
||
end
|
||
end
|
||
|
||
-- Handle [multiscan:priority] - scan enemies and find best target
|
||
-- Must be processed BEFORE the Keywords loop since it sets conditionals.target
|
||
-- Pass origTarget so @unit syntax makes that unit exempt from combat check
|
||
if conditionals.multiscan then
|
||
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, origTarget)
|
||
if not scanResult then
|
||
-- No valid target found - fail this conditional line (try next)
|
||
conditionals.target = origTarget
|
||
return false
|
||
end
|
||
-- Set target to the found GUID for soft-casting via SuperWoW
|
||
conditionals.target = scanResult
|
||
-- Don't need to retarget since we're using GUID directly
|
||
needRetarget = false
|
||
end
|
||
|
||
for k, v in pairs(conditionals) do
|
||
if not CleveRoids.ignoreKeywords[k] then
|
||
local result = CleveRoids.Keywords[k] and CleveRoids.Keywords[k](conditionals)
|
||
-- Debug logging for equipped conditional when equipDebugLog is enabled
|
||
if CleveRoids.equipDebugLog and (k == "equipped" or k == "noequipped") then
|
||
local valStr = type(v) == "table" and table.concat(v, ", ") or tostring(v)
|
||
CleveRoids.Print("|cff888888[EquipLog] Conditional [" .. k .. ":" .. valStr .. "] = " ..
|
||
(result and "|cff00ff00PASS|r" or "|cffff0000FAIL|r") .. "|r")
|
||
end
|
||
-- Debug logging for macro reference debug
|
||
if CleveRoids.macroRefDebug then
|
||
local valStr = (v == true) and "" or (type(v) == "table" and table.concat(v, ", ") or tostring(v))
|
||
CleveRoids.Print("|cff888888[MacroRef]|r [" .. k .. (valStr ~= "" and (":" .. valStr) or "") .. "] = " ..
|
||
(result and "|cff00ff00PASS|r" or "|cffff0000FAIL|r"))
|
||
end
|
||
if not result then
|
||
if needRetarget then
|
||
TargetLastTarget()
|
||
needRetarget = false
|
||
end
|
||
conditionals.target = origTarget
|
||
return false
|
||
end
|
||
end
|
||
end
|
||
|
||
if conditionals.target ~= nil and targetBeforeAction and not (CleveRoids.hasSuperwow and action == CastSpellByName) then
|
||
if not UnitIsUnit("target", conditionals.target) then
|
||
if SpellIsTargeting() then
|
||
SpellStopCasting()
|
||
end
|
||
TargetUnit(conditionals.target)
|
||
needRetarget = true
|
||
else
|
||
if needRetarget then needRetarget = false end
|
||
end
|
||
elseif needRetarget then
|
||
TargetLastTarget()
|
||
needRetarget = false
|
||
end
|
||
|
||
if action == "STOPMACRO" then
|
||
-- Set flag to stop subsequent lines in current macro
|
||
CleveRoids.stopMacroFlag = true
|
||
return true
|
||
end
|
||
|
||
local result = true
|
||
if string.sub(msg, 1, 1) == "{" and string.sub(msg, -1) == "}" then
|
||
if CleveRoids.macroRefDebug then
|
||
CleveRoids.Print("|cff00ffff[MacroRef]|r Detected macro reference: " .. msg)
|
||
end
|
||
if string.sub(msg, 2, 2) == "\"" and string.sub(msg, -2,-2) == "\"" then
|
||
result = CleveRoids.ExecuteMacroBody(string.sub(msg, 3, -3), true)
|
||
else
|
||
result = CleveRoids.ExecuteMacroByName(string.sub(msg, 2, -2))
|
||
end
|
||
elseif msg == "" or msg == nil then
|
||
-- Empty action (conditionals passed but no spell to cast)
|
||
-- For non-spell actions (pet commands, etc.), still execute the action
|
||
if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then
|
||
CleveRoids.Print("|cffff8800[EquipLog] Empty msg branch - calling action() with no args|r")
|
||
end
|
||
if action and action ~= CastSpellByName then
|
||
action()
|
||
result = true
|
||
else
|
||
result = false
|
||
end
|
||
else
|
||
local castMsg = msg
|
||
-- FLEXIBLY check for any rank text like "(Rank 9)" before adding the highest rank
|
||
-- Use specific "(Rank" check instead of any parentheses, so spells like
|
||
-- "Faerie Fire (Feral)" still get their rank appended automatically
|
||
if action == CastSpellByName and not string.find(msg, "%(Rank") then
|
||
local sp = CleveRoids.GetSpell(msg)
|
||
local rank = sp and (sp.rank or (sp.highest and sp.highest.rank))
|
||
if rank and rank ~= "" then
|
||
castMsg = msg .. "(" .. rank .. ")"
|
||
end
|
||
end
|
||
if action == CastSpellByName then
|
||
-- Let Nampower DLL handle queuing natively via its CastSpellByName hook
|
||
if CleveRoids.hasSuperwow and conditionals.target then
|
||
CastSpellByName(castMsg, conditionals.target)
|
||
else
|
||
CastSpellByName(castMsg)
|
||
end
|
||
else
|
||
-- For other actions like UseContainerItem etc.
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff00ff00[EquipLog] Calling action('" .. tostring(msg) .. "')|r")
|
||
end
|
||
action(msg)
|
||
end
|
||
end
|
||
|
||
if needRetarget then
|
||
TargetLastTarget()
|
||
end
|
||
|
||
conditionals.target = origTarget
|
||
return result
|
||
end
|
||
|
||
function CleveRoids.DoCast(msg)
|
||
if CleveRoids.ChannelTimeDebug then
|
||
if msg and string.find(msg, "Arcane") then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[/cast BUTTON PRESS]|r " .. msg)
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = parts[i]
|
||
if CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.CAST_SlashCmd, CleveRoids.FixEmptyTarget, not CleveRoids.hasSuperwow, CastSpellByName) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- PERFORMANCE: Module-level pet cast action to avoid closure allocation per call
|
||
local function _petCastAction(spellName)
|
||
local petSpell = CleveRoids.GetPetSpell(spellName)
|
||
if petSpell and petSpell.slot then
|
||
CastPetAction(petSpell.slot)
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoCastPet(msg)
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = parts[i]
|
||
if CleveRoids.DoWithConditionals(v, _petCastAction, CleveRoids.FixEmptyTarget, false, _petCastAction) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoTarget(msg)
|
||
local action, conditionals = CleveRoids.GetParsedMsg(msg)
|
||
|
||
if action ~= "" or type(conditionals) ~= "table" or not next(conditionals) then
|
||
CleveRoids.Hooks.TARGET_SlashCmd(msg)
|
||
return true
|
||
end
|
||
|
||
local function IsGuidValid(unitTok, conds)
|
||
if not unitTok or not UnitExists(unitTok) or UnitIsDeadOrGhost(unitTok) then
|
||
return false
|
||
end
|
||
local orig = conds.target
|
||
conds.target = unitTok
|
||
local ok = true
|
||
for k, _ in pairs(conds) do
|
||
if not CleveRoids.ignoreKeywords[k] then
|
||
local fn = CleveRoids.Keywords[k]
|
||
if not fn or not fn(conds) then ok = false; break end
|
||
end
|
||
end
|
||
conds.target = orig
|
||
return ok
|
||
end
|
||
|
||
-- Save original target GUID for potential restoration (SuperWoW returns GUID as 2nd value)
|
||
local _, originalTargetGuid = UnitExists("target")
|
||
|
||
-- Track if an explicit target was specified via @unit syntax
|
||
local explicitTarget = conditionals.target
|
||
|
||
do
|
||
local unitTok = conditionals.target
|
||
|
||
if unitTok == "mouseover" then
|
||
if UnitExists("mouseover") then
|
||
unitTok = "mouseover"
|
||
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
|
||
unitTok = CleveRoids.mouseoverUnit
|
||
elseif pfUI and pfUI.uf and pfUI.uf.mouseover and pfUI.uf.mouseover.unit
|
||
and UnitExists(pfUI.uf.mouseover.unit) then
|
||
unitTok = pfUI.uf.mouseover.unit
|
||
else
|
||
unitTok = nil
|
||
end
|
||
end
|
||
|
||
if unitTok == "focus" and pfUI and pfUI.uf and pfUI.uf.focus
|
||
and pfUI.uf.focus.label and pfUI.uf.focus.id then
|
||
local fTok = pfUI.uf.focus.label .. pfUI.uf.focus.id
|
||
if UnitExists(fTok) then unitTok = fTok else unitTok = nil end
|
||
end
|
||
|
||
if unitTok and UnitExists(unitTok) and IsGuidValid(unitTok, conditionals) then
|
||
TargetUnit(unitTok)
|
||
return true
|
||
end
|
||
|
||
-- If an explicit target was specified via @unit syntax but doesn't exist or isn't valid,
|
||
-- return false instead of falling through to candidate search.
|
||
-- This ensures "/target [@mouseover,harm,alive]" does nothing when mouse is over empty ground.
|
||
if explicitTarget then
|
||
return false
|
||
end
|
||
end
|
||
|
||
if UnitExists("target") and IsGuidValid("target", conditionals) then
|
||
return true
|
||
end
|
||
|
||
local candidates = {}
|
||
local wantsHelp = conditionals.help
|
||
local wantsHarm = conditionals.harm
|
||
|
||
local function addCandidate(unitId)
|
||
if not UnitExists(unitId) then return end
|
||
|
||
if wantsHelp and not UnitCanAssist("player", unitId) then return end
|
||
|
||
if wantsHarm and not UnitCanAttack("player", unitId) then return end
|
||
|
||
table.insert(candidates, { unitId = unitId })
|
||
end
|
||
|
||
addCandidate("mouseover")
|
||
|
||
addCandidate("target")
|
||
|
||
if not wantsHarm then
|
||
table.insert(candidates, { unitId = "player" })
|
||
end
|
||
|
||
addCandidate("pet")
|
||
|
||
for i = 1, 4 do
|
||
addCandidate("party"..i)
|
||
addCandidate("partypet"..i)
|
||
addCandidate("party"..i.."target")
|
||
end
|
||
|
||
for i = 1, 40 do
|
||
addCandidate("raid"..i)
|
||
addCandidate("raidpet"..i)
|
||
addCandidate("raid"..i.."target")
|
||
end
|
||
|
||
addCandidate("targettarget")
|
||
addCandidate("targettargettarget")
|
||
|
||
addCandidate("pettarget")
|
||
|
||
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.label and pfUI.uf.focus.id then
|
||
local focusTok = pfUI.uf.focus.label .. pfUI.uf.focus.id
|
||
addCandidate(focusTok)
|
||
end
|
||
|
||
if CleveRoids.hasSuperwow then
|
||
local numChildren = WorldFrame:GetNumChildren()
|
||
local children = { WorldFrame:GetChildren() }
|
||
|
||
for i = 1, numChildren do
|
||
local frame = children[i]
|
||
if frame and frame:IsVisible() then
|
||
local success, guid = pcall(frame.GetName, frame, 1)
|
||
if success and guid and type(guid) == "string" and string.len(guid) > 0 then
|
||
if UnitExists(guid) then
|
||
addCandidate(guid)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
for _, c in ipairs(candidates) do
|
||
if IsGuidValid(c.unitId, conditionals) then
|
||
TargetUnit(c.unitId)
|
||
return true
|
||
end
|
||
end
|
||
|
||
-- UnitXP 3D enemy scanning: cycles through enemies in world space (no nameplate required)
|
||
-- This is the most powerful scan - finds enemies by line of sight and distance
|
||
-- Always enabled unless explicitly looking for friendlies only ([help] without [harm])
|
||
-- UnitXP only finds enemies, so this is safe for any /target with conditionals
|
||
local wantsFriendlyOnly = wantsHelp and not wantsHarm
|
||
if not wantsFriendlyOnly and CleveRoids.hasUnitXP then
|
||
-- Try nearestEnemy first - most common case and most efficient
|
||
local found = UnitXP("target", "nearestEnemy")
|
||
if found and UnitExists("target") and IsGuidValid("target", conditionals) then
|
||
return true
|
||
end
|
||
|
||
-- Determine scan mode: use distance-priority for melee conditionals
|
||
local wantsMelee = conditionals.meleerange or conditionals.nomeleerange
|
||
local scanMode = wantsMelee and "nextEnemyConsideringDistance" or "nextEnemyInCycle"
|
||
|
||
local seenGuids = {}
|
||
local firstGuid = nil
|
||
local maxIterations = 50 -- Safety limit
|
||
|
||
for i = 1, maxIterations do
|
||
found = UnitXP("target", scanMode)
|
||
if not found then break end
|
||
|
||
local _, currentGuid = UnitExists("target")
|
||
if not currentGuid then break end
|
||
|
||
-- Check if we've cycled back to start
|
||
if firstGuid == nil then
|
||
firstGuid = currentGuid
|
||
elseif currentGuid == firstGuid then
|
||
break -- Completed full cycle
|
||
end
|
||
|
||
-- Skip already-seen targets
|
||
if not seenGuids[currentGuid] then
|
||
seenGuids[currentGuid] = true
|
||
|
||
-- Test this target against all conditionals
|
||
if IsGuidValid("target", conditionals) then
|
||
return true -- Found matching target
|
||
end
|
||
end
|
||
end
|
||
|
||
-- No match found via UnitXP scan - restore original target if we had one
|
||
if originalTargetGuid and UnitExists(originalTargetGuid) then
|
||
TargetUnit(originalTargetGuid)
|
||
elseif not originalTargetGuid then
|
||
ClearTarget()
|
||
end
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
-- Attempts to attack a unit by a set of conditionals
|
||
-- msg: The raw message intercepted from a /petattack command
|
||
function CleveRoids.DoPetAction(action, msg)
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
if CleveRoids.DoWithConditionals(parts[i], action, CleveRoids.FixEmptyTarget, true, action) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- PERFORMANCE: Module-level action to avoid closure allocation per call
|
||
local function _startAttackAction()
|
||
if not UnitExists("target") or UnitIsDead("target") then TargetNearestEnemy() end
|
||
if not CleveRoids.CurrentSpell.autoAttack and not CleveRoids.CurrentSpell.autoAttackLock and UnitExists("target") and UnitCanAttack("player", "target") then
|
||
CleveRoids.CurrentSpell.autoAttackLock = true
|
||
CleveRoids.autoAttackLockElapsed = GetTime()
|
||
AttackTarget()
|
||
end
|
||
end
|
||
|
||
-- Attempts to conditionally start an attack. Returns false if no conditionals are found.
|
||
function CleveRoids.DoConditionalStartAttack(msg)
|
||
if not string.find(msg, "%[") then return false end
|
||
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
-- We pass 'nil' for the hook, so DoWithConditionals does nothing if it fails to parse conditionals.
|
||
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _startAttackAction) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- PERFORMANCE: Module-level actions to avoid closure allocation per call
|
||
local function _stopAttackAction()
|
||
if CleveRoids.CurrentSpell.autoAttack and UnitExists("target") then
|
||
AttackTarget()
|
||
CleveRoids.CurrentSpell.autoAttack = false
|
||
end
|
||
end
|
||
|
||
local function _stopCastingAction()
|
||
SpellStopCasting()
|
||
end
|
||
|
||
local function _clearTargetAction()
|
||
ClearTarget()
|
||
end
|
||
|
||
-- Attempts to conditionally stop an attack. Returns false if no conditionals are found.
|
||
function CleveRoids.DoConditionalStopAttack(msg)
|
||
if not string.find(msg, "%[") then return false end
|
||
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _stopAttackAction) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Attempts to conditionally stop casting. Returns false if no conditionals are found.
|
||
function CleveRoids.DoConditionalStopCasting(msg)
|
||
if not string.find(msg, "%[") then return false end
|
||
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _stopCastingAction) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Attempts to conditionally clear target. Returns false if no conditionals are found.
|
||
function CleveRoids.DoConditionalClearTarget(msg)
|
||
if not string.find(msg, "%[") then return false end
|
||
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _clearTargetAction) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
|
||
-- Attempts to use or equip an item by a set of conditionals
|
||
-- Also checks if a condition is a spell so that you can mix item and spell use
|
||
-- msg: The raw message intercepted from a /use or /equip command
|
||
function CleveRoids.DoUse(msg)
|
||
local handled = false
|
||
|
||
local action = function(msg)
|
||
-- Defensive: make sure we are not in "split stack" mode and nothing is on the cursor
|
||
if type(CloseStackSplitFrame) == "function" then CloseStackSplitFrame() end
|
||
if CursorHasItem and CursorHasItem() then ClearCursor() end
|
||
|
||
-- Try to interpret the message as a direct inventory slot ID first.
|
||
local slotId = tonumber(msg)
|
||
if slotId and slotId >= 1 and slotId <= 19 then -- Character slots are 1-19
|
||
ClearCursor() -- extra safety before using equipped items
|
||
UseInventoryItem(slotId)
|
||
return
|
||
end
|
||
|
||
-- Try to interpret as item ID (numbers > 19)
|
||
-- v2.18+: Use FindPlayerItemSlot directly for item IDs (no name resolution needed)
|
||
if slotId and slotId > 19 then
|
||
local API = CleveRoids.NampowerAPI
|
||
-- v2.18+: Native lookup can find item directly by ID
|
||
if API and API.features and API.features.hasFindPlayerItemSlot then
|
||
local itemInfo = API.FindItemFast(slotId)
|
||
if itemInfo then
|
||
ClearCursor()
|
||
if itemInfo.inventoryID then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. slotId .. " via UseInventoryItem(" .. itemInfo.inventoryID .. ") [v2.18 ID lookup]|r")
|
||
end
|
||
UseInventoryItem(itemInfo.inventoryID)
|
||
return
|
||
elseif itemInfo.bagID and itemInfo.slot then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. slotId .. " via UseContainerItem(" .. itemInfo.bagID .. "," .. itemInfo.slot .. ") [v2.18 ID lookup]|r")
|
||
end
|
||
UseContainerItem(itemInfo.bagID, itemInfo.slot)
|
||
return
|
||
end
|
||
end
|
||
-- Item not found by ID - fail
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cffff8800[UseLog] Item ID " .. slotId .. " not found in inventory [v2.18]|r")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- Fallback: Resolve item ID to name for legacy lookup
|
||
local itemName = nil
|
||
if API and API.GetItemName then
|
||
itemName = API.GetItemName(slotId)
|
||
end
|
||
-- Fall back to GetItemInfo
|
||
if not itemName and GetItemInfo then
|
||
itemName = GetItemInfo(slotId)
|
||
end
|
||
if itemName then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] Resolved item ID " .. slotId .. " to '" .. itemName .. "'|r")
|
||
end
|
||
msg = itemName -- Replace ID with name for subsequent lookups
|
||
else
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cffff8800[UseLog] Could not resolve item ID " .. slotId .. " - item not in cache|r")
|
||
end
|
||
-- Item not in client cache - can't resolve without seeing it first
|
||
return
|
||
end
|
||
end
|
||
|
||
-- v2.18+: Use native fast lookup (much faster than Lua cache + scan)
|
||
local API = CleveRoids.NampowerAPI
|
||
if API and API.features and API.features.hasFindPlayerItemSlot then
|
||
local itemInfo = API.FindItemFast(msg)
|
||
if itemInfo then
|
||
ClearCursor()
|
||
if itemInfo.inventoryID then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. itemInfo.inventoryID .. ") [v2.18 native]|r")
|
||
end
|
||
UseInventoryItem(itemInfo.inventoryID)
|
||
return
|
||
elseif itemInfo.bagID and itemInfo.slot then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. itemInfo.bagID .. "," .. itemInfo.slot .. ") [v2.18 native]|r")
|
||
end
|
||
UseContainerItem(itemInfo.bagID, itemInfo.slot)
|
||
return
|
||
end
|
||
end
|
||
-- v2.18 lookup didn't find item - fall through to legacy path
|
||
-- (might be partial match or different case that native doesn't handle)
|
||
end
|
||
|
||
-- PERFORMANCE: Try cache lookup first (O(1) instead of O(n) scan)
|
||
-- IMPORTANT: Validate cache hits to prevent stale data during combat
|
||
-- (IndexItems() is skipped during combat, so cache may have old bag/slot locations)
|
||
local location = CleveRoids.FindItemLocation(msg)
|
||
if location then
|
||
local cacheValid = false
|
||
local qname = string_lower(msg)
|
||
|
||
if location.type == "inventory" then
|
||
-- Validate: check if this slot actually contains the item we want
|
||
local link = GetInventoryItemLink("player", location.inventoryID)
|
||
if link then
|
||
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
||
if nm and string_lower(nm) == qname then
|
||
cacheValid = true
|
||
end
|
||
end
|
||
else
|
||
-- Validate: check if this bag slot actually contains the item we want
|
||
local link = GetContainerItemLink(location.bag, location.slot)
|
||
if link then
|
||
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
||
if nm and string_lower(nm) == qname then
|
||
cacheValid = true
|
||
end
|
||
end
|
||
end
|
||
|
||
if cacheValid then
|
||
ClearCursor()
|
||
if location.type == "inventory" then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. location.inventoryID .. ") [cached]|r")
|
||
end
|
||
UseInventoryItem(location.inventoryID)
|
||
else
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. location.bag .. "," .. location.slot .. ") [cached]|r")
|
||
end
|
||
UseContainerItem(location.bag, location.slot)
|
||
end
|
||
return
|
||
elseif CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " - cache STALE, falling back to scan|r")
|
||
end
|
||
end
|
||
|
||
-- Slow path fallback: full scan for substring matches or cache miss
|
||
local qname = string_lower(msg)
|
||
|
||
-- Search equipped inventory slots first (for trinkets, etc.)
|
||
for slot = 0, 19 do
|
||
local link = GetInventoryItemLink("player", slot)
|
||
if link then
|
||
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
||
if nm and string_lower(nm) == qname then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. slot .. ")|r")
|
||
end
|
||
ClearCursor()
|
||
UseInventoryItem(slot)
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Then search bags
|
||
for bag = 0, 4 do
|
||
local numSlots = GetContainerNumSlots(bag) or 0
|
||
for bagSlot = 1, numSlots do
|
||
local link = GetContainerItemLink(bag, bagSlot)
|
||
if link then
|
||
local _, _, nm = string_find(link, "|h%[(.-)%]|h")
|
||
if nm and string_lower(nm) == qname then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseContainerItem(" .. bag .. "," .. bagSlot .. ")|r")
|
||
end
|
||
ClearCursor()
|
||
UseContainerItem(bag, bagSlot)
|
||
return
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " - not found in equipped slots or bags|r")
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
if CleveRoids.DoWithConditionals(parts[i], action, CleveRoids.FixEmptyTarget, false, action) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.EquipBagItem(msg, slotOrOffhand)
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' slot=" .. tostring(slotOrOffhand) .. "|r")
|
||
end
|
||
|
||
if CleveRoids.equipInProgress then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cffff0000[EquipLog] Equip already in progress, skipping|r")
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Accept slot number directly, or boolean for backward compatibility (false=16/MH, true=17/OH)
|
||
local invslot
|
||
if type(slotOrOffhand) == "number" then
|
||
invslot = slotOrOffhand
|
||
else
|
||
invslot = slotOrOffhand and 17 or 16
|
||
end
|
||
local API = CleveRoids.NampowerAPI
|
||
|
||
-- v2.18+: Use native fast lookup for item ID or name
|
||
-- Guard against API being a function instead of table (addon conflict protection)
|
||
if type(API) == "table" and type(API.features) == "table" and API.features.hasFindPlayerItemSlot then
|
||
local searchTerm = msg
|
||
local itemId = tonumber(msg)
|
||
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff888888[EquipLog] Searching for '" .. tostring(searchTerm) .. "' (slot " .. invslot .. ")|r")
|
||
end
|
||
|
||
-- Check if already equipped in target slot
|
||
if API.IsItemInSlot(searchTerm, invslot) then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff00ff00[EquipLog] '" .. tostring(searchTerm) .. "' already in slot " .. invslot .. "|r")
|
||
end
|
||
return true
|
||
end
|
||
|
||
-- Find the item (works with both ID and name)
|
||
local itemInfo = API.FindItemFast(searchTerm)
|
||
if CleveRoids.equipDebugLog then
|
||
if itemInfo then
|
||
CleveRoids.Print("|cff888888[EquipLog] FindItemFast found: invID=" .. tostring(itemInfo.inventoryID) .. " bag=" .. tostring(itemInfo.bagID) .. " slot=" .. tostring(itemInfo.slot) .. "|r")
|
||
else
|
||
CleveRoids.Print("|cffff8800[EquipLog] FindItemFast returned nil|r")
|
||
end
|
||
end
|
||
if itemInfo then
|
||
-- Already equipped in different slot - need to swap
|
||
if itemInfo.inventoryID then
|
||
if itemInfo.inventoryID == invslot then
|
||
return true -- Already in correct slot
|
||
end
|
||
-- Pick up from current slot and equip to target
|
||
ClearCursor()
|
||
PickupInventoryItem(itemInfo.inventoryID)
|
||
if CursorHasItem and CursorHasItem() then
|
||
EquipCursorItem(invslot)
|
||
ClearCursor()
|
||
return true
|
||
end
|
||
elseif itemInfo.bagID and itemInfo.slot then
|
||
-- In bag - equip to target slot
|
||
ClearCursor()
|
||
PickupContainerItem(itemInfo.bagID, itemInfo.slot)
|
||
if CursorHasItem and CursorHasItem() then
|
||
EquipCursorItem(invslot)
|
||
ClearCursor()
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff00ff00[EquipLog] Equipped '" .. tostring(msg) .. "' from bag " .. itemInfo.bagID .. " slot " .. itemInfo.slot .. "|r")
|
||
end
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Item not found via v2.18 lookup
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot|r")
|
||
-- Debug: Try direct FindPlayerItemSlot call
|
||
if FindPlayerItemSlot then
|
||
local bag, slot = FindPlayerItemSlot(msg)
|
||
CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r")
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Fallback for older Nampower versions
|
||
-- Try to interpret as item ID (numbers > 19)
|
||
local itemId = tonumber(msg)
|
||
if itemId and itemId > 19 then
|
||
local itemName = nil
|
||
if API and API.GetItemName then
|
||
itemName = API.GetItemName(itemId)
|
||
end
|
||
if not itemName and GetItemInfo then
|
||
itemName = GetItemInfo(itemId)
|
||
end
|
||
if itemName then
|
||
msg = itemName
|
||
else
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cffff8800[EquipLog] Can't resolve item ID " .. tostring(itemId) .. "|r")
|
||
end
|
||
return false -- Can't resolve item ID
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Fast check if already equipped (single function call)
|
||
if CleveRoids.IsItemEquipped and CleveRoids.IsItemEquipped(msg, invslot) then
|
||
return true
|
||
end
|
||
|
||
-- Note what's currently in the target slot so we can invalidate its cache
|
||
local oldSlotLink = GetInventoryItemLink("player", invslot)
|
||
local oldSlotName = nil
|
||
if oldSlotLink then
|
||
local _, _, name = string_find(oldSlotLink, "|h%[(.-)%]|h")
|
||
oldSlotName = name
|
||
end
|
||
|
||
-- Helper to invalidate displaced item's cache
|
||
local function InvalidateDisplacedItem()
|
||
if oldSlotName and CleveRoids.Items then
|
||
CleveRoids.Items[oldSlotName] = nil
|
||
CleveRoids.Items[string_lower(oldSlotName)] = nil
|
||
end
|
||
end
|
||
|
||
-- Check if item is already equipped in the paired slot (swap case)
|
||
-- EquipItemByName doesn't handle swapping equipped items, so we must do it manually
|
||
-- Paired slots: trinkets (13<->14), weapons (16<->17), rings (11<->12)
|
||
local pairedSlots = {[13] = 14, [14] = 13, [16] = 17, [17] = 16, [11] = 12, [12] = 11}
|
||
local checkSlot = pairedSlots[invslot]
|
||
if checkSlot then
|
||
local link = GetInventoryItemLink("player", checkSlot)
|
||
if link then
|
||
local _, _, slotItemName = string_find(link, "|h%[(.-)%]|h")
|
||
if slotItemName and string_lower(slotItemName) == string_lower(msg) then
|
||
-- Found item in paired slot - swap it manually
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cff00ffff[EquipLog] Swapping from slot " .. checkSlot .. " to slot " .. invslot .. "|r")
|
||
end
|
||
ClearCursor()
|
||
PickupInventoryItem(checkSlot)
|
||
if CursorHasItem and CursorHasItem() then
|
||
EquipCursorItem(invslot)
|
||
ClearCursor()
|
||
if CleveRoids.Items then
|
||
CleveRoids.Items[msg] = nil
|
||
CleveRoids.Items[string_lower(msg)] = nil
|
||
end
|
||
InvalidateDisplacedItem()
|
||
return true
|
||
end
|
||
ClearCursor()
|
||
end
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Try EquipItemByName for bag items (fast path)
|
||
-- This is the fastest path - no item lookup, no cursor operations
|
||
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
|
||
end
|
||
InvalidateDisplacedItem()
|
||
return true
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Use fast lookup first, fall back to full scan
|
||
-- Full scan is now optimized with GetNameFromLink() instead of GetItemInfo()
|
||
local item = CleveRoids.GetItemFast and CleveRoids.GetItemFast(msg)
|
||
if not item then
|
||
-- Try quick targeted scan (stops when found)
|
||
item = CleveRoids.FindItemQuick and CleveRoids.FindItemQuick(msg)
|
||
end
|
||
if not item then
|
||
-- Full scan fallback (now optimized, safe during combat)
|
||
item = CleveRoids.GetItem(msg)
|
||
end
|
||
|
||
if not item or not item.name then
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found in bags or equipped|r")
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Already equipped check (by item ID from lookup)
|
||
if item.inventoryID == invslot then
|
||
return true
|
||
end
|
||
|
||
if not item.bagID and not item.inventoryID then
|
||
return false
|
||
end
|
||
|
||
-- Try EquipItemByName with resolved name (in case msg was partial/different case)
|
||
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
|
||
end
|
||
InvalidateDisplacedItem()
|
||
return true
|
||
end
|
||
end
|
||
|
||
-- Fallback: Manual pickup and equip
|
||
CleveRoids.equipInProgress = true
|
||
|
||
-- PERFORMANCE: Single cursor check at start
|
||
if CursorHasItem and CursorHasItem() then
|
||
ClearCursor()
|
||
end
|
||
|
||
local pickupSuccess = false
|
||
if item.bagID and item.slot then
|
||
PickupContainerItem(item.bagID, item.slot)
|
||
pickupSuccess = CursorHasItem and CursorHasItem()
|
||
elseif item.inventoryID then
|
||
PickupInventoryItem(item.inventoryID)
|
||
pickupSuccess = CursorHasItem and CursorHasItem()
|
||
end
|
||
|
||
if not pickupSuccess then
|
||
ClearCursor()
|
||
CleveRoids.equipInProgress = false
|
||
return false
|
||
end
|
||
|
||
EquipCursorItem(invslot)
|
||
ClearCursor()
|
||
|
||
if CleveRoids.Items and item.name then
|
||
CleveRoids.Items[item.name] = nil
|
||
CleveRoids.Items[string_lower(item.name)] = nil
|
||
end
|
||
InvalidateDisplacedItem()
|
||
CleveRoids.equipInProgress = false
|
||
return true
|
||
end
|
||
|
||
-- PERFORMANCE: Module-level actions to avoid closure allocation per call
|
||
local function _equipMainhandAction(msg)
|
||
return CleveRoids.EquipBagItem(msg, false)
|
||
end
|
||
|
||
local function _equipOffhandAction(msg)
|
||
return CleveRoids.EquipBagItem(msg, true)
|
||
end
|
||
|
||
local function _equipTrinket1Action(msg)
|
||
return CleveRoids.EquipBagItem(msg, 13)
|
||
end
|
||
|
||
local function _equipTrinket2Action(msg)
|
||
return CleveRoids.EquipBagItem(msg, 14)
|
||
end
|
||
|
||
local function _equipRing1Action(msg)
|
||
return CleveRoids.EquipBagItem(msg, 11)
|
||
end
|
||
|
||
local function _equipRing2Action(msg)
|
||
return CleveRoids.EquipBagItem(msg, 12)
|
||
end
|
||
|
||
local function _unshiftAction()
|
||
local currentShapeshiftIndex = CleveRoids.GetCurrentShapeshiftIndex()
|
||
if currentShapeshiftIndex ~= 0 then
|
||
CastShapeshiftForm(currentShapeshiftIndex)
|
||
end
|
||
end
|
||
|
||
function CleveRoids.DoEquipMainhand(msg)
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = string.gsub(parts[i], "^%?", "")
|
||
if CleveRoids.DoWithConditionals(v, _equipMainhandAction, CleveRoids.FixEmptyTarget, false, _equipMainhandAction) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoEquipOffhand(msg)
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = string.gsub(parts[i], "^%?", "")
|
||
if CleveRoids.DoWithConditionals(v, _equipOffhandAction, CleveRoids.FixEmptyTarget, false, _equipOffhandAction) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoEquipTrinket1(msg)
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = string.gsub(parts[i], "^%?", "")
|
||
if CleveRoids.DoWithConditionals(v, _equipTrinket1Action, CleveRoids.FixEmptyTarget, false, _equipTrinket1Action) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoEquipTrinket2(msg)
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = string.gsub(parts[i], "^%?", "")
|
||
if CleveRoids.DoWithConditionals(v, _equipTrinket2Action, CleveRoids.FixEmptyTarget, false, _equipTrinket2Action) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoEquipRing1(msg)
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = string.gsub(parts[i], "^%?", "")
|
||
if CleveRoids.DoWithConditionals(v, _equipRing1Action, CleveRoids.FixEmptyTarget, false, _equipRing1Action) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoEquipRing2(msg)
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
local v = string.gsub(parts[i], "^%?", "")
|
||
if CleveRoids.DoWithConditionals(v, _equipRing2Action, CleveRoids.FixEmptyTarget, false, _equipRing2Action) then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoUnshift(msg)
|
||
local handled
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
|
||
for i = 1, table.getn(parts) do
|
||
handled = false
|
||
if CleveRoids.DoWithConditionals(parts[i], _unshiftAction, CleveRoids.FixEmptyTarget, false, _unshiftAction) then
|
||
handled = true
|
||
break
|
||
end
|
||
end
|
||
|
||
if handled == nil then
|
||
_unshiftAction()
|
||
end
|
||
|
||
return handled
|
||
end
|
||
|
||
function CleveRoids.DoRetarget()
|
||
if GetUnitName("target") == nil
|
||
or UnitHealth("target") == 0
|
||
or not UnitCanAttack("player", "target")
|
||
then
|
||
ClearTarget()
|
||
TargetNearestEnemy()
|
||
end
|
||
end
|
||
|
||
-- Attempts to stop macro
|
||
function CleveRoids.DoStopMacro(msg)
|
||
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
|
||
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
|
||
for i = 1, table.getn(parts) do
|
||
if CleveRoids.DoWithConditionals(msg, nil, nil, not CleveRoids.hasSuperwow, "STOPMACRO") then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
function CleveRoids.DoCastSequence(sequence)
|
||
if not CleveRoids.hasSuperwow then
|
||
CleveRoids.Print("|cFFFF0000/castsequence|r requires |cFF00FFFFSuperWoW|r.")
|
||
return
|
||
end
|
||
if type(sequence) == "string" then
|
||
sequence = CleveRoids.GetSequence(sequence)
|
||
if not sequence then return end
|
||
end
|
||
|
||
if CleveRoids.currentSequence and not CleveRoids.CheckSpellCast("player") then
|
||
CleveRoids.currentSequence = nil
|
||
elseif CleveRoids.currentSequence then
|
||
return
|
||
end
|
||
|
||
-- If sequence is complete, don't execute - let macro continue to next line
|
||
if sequence.complete then
|
||
return
|
||
end
|
||
|
||
if sequence.index > 1 and sequence.reset then
|
||
for k,_ in pairs(sequence.reset) do
|
||
if CleveRoids.kmods[k] and CleveRoids.kmods[k]() then
|
||
CleveRoids.ResetSequence(sequence)
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
local active = CleveRoids.GetCurrentSequenceAction(sequence)
|
||
if not (active and active.action) then return end
|
||
|
||
sequence.status = 0
|
||
sequence.lastUpdate = GetTime()
|
||
sequence.expires = 0
|
||
|
||
local prevSeq = CleveRoids.currentSequence
|
||
CleveRoids.currentSequence = sequence
|
||
|
||
local actionText = (sequence.cond or "") .. active.action
|
||
local resolvedText, conds = CleveRoids.GetParsedMsg(actionText)
|
||
|
||
-- Check if this is a macro execution {macroname}
|
||
local macroName = CleveRoids.GetMacroNameFromAction(active.action)
|
||
if macroName then
|
||
-- Execute the macro
|
||
local success = CleveRoids.ExecuteMacroByName(macroName)
|
||
if not success then
|
||
CleveRoids.currentSequence = prevSeq
|
||
end
|
||
return
|
||
end
|
||
|
||
local function cast_by_name(msg)
|
||
msg = msg or ""
|
||
-- Check specifically for "(Rank" to allow spells like "Faerie Fire (Feral)"
|
||
-- to still get their rank appended automatically
|
||
if not string.find(msg, "%(Rank") then
|
||
local sp = CleveRoids.GetSpell(msg)
|
||
local r = (sp and sp.rank) or (sp and sp.highest and sp.highest.rank)
|
||
if r and r ~= "" then msg = msg .. "(" .. r .. ")" end
|
||
end
|
||
-- Let Nampower DLL handle queuing natively via its CastSpellByName hook
|
||
CastSpellByName(msg)
|
||
return true
|
||
end
|
||
|
||
local attempted = false
|
||
if not conds then
|
||
attempted = cast_by_name(resolvedText or active.action)
|
||
else
|
||
local final = CleveRoids.DoWithConditionals(actionText, nil, CleveRoids.FixEmptyTarget, false, CastSpellByName)
|
||
if final then attempted = cast_by_name(final) end
|
||
end
|
||
|
||
if not attempted then
|
||
CleveRoids.currentSequence = prevSeq
|
||
end
|
||
end
|
||
|
||
CleveRoids.DoConditionalCancelAura = function(msg)
|
||
local s = CleveRoids.Trim(msg or "")
|
||
if s == "" then return false end
|
||
|
||
-- No conditionals? cancel immediately.
|
||
if not string.find(s, "%[") then
|
||
return CleveRoids.CancelAura(s)
|
||
end
|
||
|
||
-- Has conditionals? Let the framework evaluate them, then run CancelAura.
|
||
return CleveRoids.DoWithConditionals(s, nil, CleveRoids.FixEmptyTarget, false, CleveRoids.CancelAura) or false
|
||
end
|
||
|
||
-- PERFORMANCE: Separate periodic cleanup timer (runs every 5 seconds instead of every frame)
|
||
CleveRoids.lastCleanupTime = 0
|
||
CleveRoids.CLEANUP_INTERVAL = 5 -- Run cleanup every 5 seconds
|
||
|
||
-- PERFORMANCE: Upvalues for OnUpdate hot path
|
||
local GetTime = GetTime
|
||
local UnitAffectingCombat = UnitAffectingCombat
|
||
local pairs = pairs
|
||
|
||
function CleveRoids.OnUpdate(self)
|
||
-- PERFORMANCE: Single GetTime() call per frame
|
||
local time = GetTime()
|
||
|
||
-- PERFORMANCE: Early exit if not ready (before any other checks)
|
||
if not CleveRoids.ready then
|
||
-- Handle initialization timer only when not ready
|
||
if CleveRoids.initializationTimer and time >= CleveRoids.initializationTimer then
|
||
CleveRoids.IndexItems()
|
||
CleveRoids.IndexActionBars()
|
||
CleveRoids.ready = true
|
||
CleveRoids.initializationTimer = nil
|
||
CleveRoids.TestForAllActiveActions()
|
||
CleveRoids.lastUpdate = time
|
||
end
|
||
return
|
||
end
|
||
|
||
-- PERFORMANCE: Delayed WDB warmup after login (ensures GetItemInfo works after WDB clear)
|
||
if CleveRoids.wdbWarmupTime and time >= CleveRoids.wdbWarmupTime then
|
||
CleveRoids.wdbWarmupTime = nil
|
||
CleveRoids.DoWDBWarmup()
|
||
end
|
||
|
||
-- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division)
|
||
local refreshRate = CleveRoids.cachedRefreshRate
|
||
if not refreshRate then
|
||
refreshRate = 1 / (CleveRoidMacros.refresh or 5)
|
||
CleveRoids.cachedRefreshRate = refreshRate
|
||
end
|
||
|
||
-- PERFORMANCE: Throttle check FIRST - skip most work on non-throttled frames
|
||
local lastUpdate = CleveRoids.lastUpdate or 0
|
||
local bypassThrottle = CleveRoids.isActionUpdateQueued and CleveRoidMacros.realtime == 0
|
||
local shouldUpdate = bypassThrottle or (time - lastUpdate) >= refreshRate
|
||
|
||
if not shouldUpdate then
|
||
return -- Early exit for non-throttled frames
|
||
end
|
||
|
||
CleveRoids.lastUpdate = time
|
||
|
||
-- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED)
|
||
-- PERFORMANCE: Skip check entirely if no pending update
|
||
local pendingTime = CleveRoids.equipIndexPendingTime
|
||
if pendingTime and not UnitAffectingCombat("player") then
|
||
if (time - (CleveRoids.lastEquipIndexTime or 0)) >= 0.2 then
|
||
CleveRoids.lastEquipIndexTime = time
|
||
CleveRoids.equipIndexPendingTime = nil
|
||
CleveRoids.lastItemIndexTime = time
|
||
CleveRoids.IndexItems()
|
||
CleveRoids.Actions = {}
|
||
CleveRoids.Macros = {}
|
||
CleveRoids.IndexActionBars()
|
||
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
end
|
||
|
||
-- PERFORMANCE: Check for expired reactive procs only if we have any
|
||
-- Use statically allocated removal buffer to avoid per-frame allocation
|
||
local reactiveProcs = CleveRoids.reactiveProcs
|
||
if reactiveProcs then
|
||
local hasExpiredProc = false
|
||
local toRemove = CleveRoids._procRemovalBuffer
|
||
local removeCount = 0
|
||
|
||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||
local spellName, procData = next(reactiveProcs)
|
||
while spellName do
|
||
if procData and procData.expiry and time >= procData.expiry then
|
||
removeCount = removeCount + 1
|
||
toRemove[removeCount] = spellName
|
||
hasExpiredProc = true
|
||
end
|
||
spellName, procData = next(reactiveProcs, spellName)
|
||
end
|
||
|
||
-- Remove expired procs using indexed array (no pairs() overhead)
|
||
for i = 1, removeCount do
|
||
reactiveProcs[toRemove[i]] = nil
|
||
toRemove[i] = nil -- Clear for next use
|
||
end
|
||
|
||
-- If any proc expired, immediately update all actions
|
||
if hasExpiredProc then
|
||
CleveRoids.TestForAllActiveActions()
|
||
CleveRoids.isActionUpdateQueued = false
|
||
end
|
||
end
|
||
-- PERFORMANCE: Check for modifier key state changes (no events for these in vanilla WoW)
|
||
-- Only queue update if modifier state actually changed - avoids full TestForAllActiveActions
|
||
local altDown = IsAltKeyDown() and true or false
|
||
local shiftDown = IsShiftKeyDown() and true or false
|
||
local ctrlDown = IsControlKeyDown() and true or false
|
||
|
||
if altDown ~= CleveRoids._lastAltDown or
|
||
shiftDown ~= CleveRoids._lastShiftDown or
|
||
ctrlDown ~= CleveRoids._lastCtrlDown then
|
||
CleveRoids._lastAltDown = altDown
|
||
CleveRoids._lastShiftDown = shiftDown
|
||
CleveRoids._lastCtrlDown = ctrlDown
|
||
-- Modifier changed - queue update in event-driven mode, or just mark for realtime
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.isActionUpdateQueued = true
|
||
end
|
||
end
|
||
|
||
-- Check the saved variable to decide which update mode to use.
|
||
if CleveRoidMacros.realtime == 1 then
|
||
-- Realtime Mode: Force an update on every throttled tick for maximum responsiveness.
|
||
CleveRoids.TestForAllActiveActions()
|
||
else
|
||
-- Event-Driven Mode (Default): Only update if a relevant game event has queued it.
|
||
if CleveRoids.isActionUpdateQueued then
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[OnUpdate]|r Processing queued action update")
|
||
end
|
||
CleveRoids.TestForAllActiveActions()
|
||
CleveRoids.isActionUpdateQueued = false -- Reset the flag after updating
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[OnUpdate]|r Action update complete, flag reset")
|
||
end
|
||
end
|
||
end
|
||
|
||
-- The rest of this function handles time-based logic that must always run.
|
||
if CleveRoids.CurrentSpell.autoAttackLock and (time - CleveRoids.autoAttackLockElapsed) > refreshRate then
|
||
CleveRoids.CurrentSpell.autoAttackLock = false
|
||
CleveRoids.autoAttackLockElapsed = nil
|
||
end
|
||
|
||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||
local Sequences = CleveRoids.Sequences
|
||
local seqKey, sequence = next(Sequences)
|
||
while seqKey do
|
||
if sequence.index > 1 and sequence.reset.secs and (time - (sequence.lastUpdate or 0)) >= sequence.reset.secs then
|
||
CleveRoids.ResetSequence(sequence)
|
||
end
|
||
seqKey, sequence = next(Sequences, seqKey)
|
||
end
|
||
|
||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||
local spell_tracking = CleveRoids.spell_tracking
|
||
local guid, cast = next(spell_tracking)
|
||
while guid do
|
||
local nextGuid = next(spell_tracking, guid) -- Get next before potential removal
|
||
if cast.expires and time > cast.expires then
|
||
spell_tracking[guid] = nil
|
||
end
|
||
guid, cast = nextGuid, nextGuid and spell_tracking[nextGuid]
|
||
end
|
||
|
||
-- PERFORMANCE OPTIMIZATION: Run memory cleanup less frequently (every 5 seconds instead of every frame)
|
||
-- This reduces CPU usage while maintaining effective memory management
|
||
if (time - CleveRoids.lastCleanupTime) >= CleveRoids.CLEANUP_INTERVAL then
|
||
CleveRoids.lastCleanupTime = time
|
||
|
||
-- MEMORY: Clean up carnageDurationOverrides older than 30 seconds
|
||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||
local carnageOverrides = CleveRoids.carnageDurationOverrides
|
||
if carnageOverrides then
|
||
local spellID, data = next(carnageOverrides)
|
||
while spellID do
|
||
local nextID = next(carnageOverrides, spellID)
|
||
if data.timestamp and (time - data.timestamp) > 30 then
|
||
carnageOverrides[spellID] = nil
|
||
end
|
||
spellID, data = nextID, nextID and carnageOverrides[nextID]
|
||
end
|
||
end
|
||
|
||
-- MEMORY: Clean up old ComboPointTracking entries (older than 60 seconds)
|
||
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
|
||
local comboTracking = CleveRoids.ComboPointTracking
|
||
if comboTracking then
|
||
local trackName, data = next(comboTracking)
|
||
while trackName do
|
||
local nextName = next(comboTracking, trackName)
|
||
if data.cast_time and (time - data.cast_time) > 60 then
|
||
comboTracking[trackName] = nil
|
||
end
|
||
trackName, data = nextName, nextName and comboTracking[nextName]
|
||
end
|
||
end
|
||
|
||
-- MEMORY: Clear spell name caches every 60 seconds (12 cleanup cycles)
|
||
CleveRoids._spellCacheCleanupCounter = (CleveRoids._spellCacheCleanupCounter or 0) + 1
|
||
if CleveRoids._spellCacheCleanupCounter >= 12 then
|
||
CleveRoids._spellCacheCleanupCounter = 0
|
||
if CleveRoids.ClearSpellNameCaches then
|
||
CleveRoids.ClearSpellNameCaches()
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Initialize the nested table for the GameTooltip hooks if it doesn't exist
|
||
if not CleveRoids.Hooks.GameTooltip then CleveRoids.Hooks.GameTooltip = {} end
|
||
|
||
-- Save the original GameTooltip.SetAction function before we override it
|
||
CleveRoids.Hooks.GameTooltip.SetAction = GameTooltip.SetAction
|
||
|
||
-- Now, define our custom version of the function
|
||
function GameTooltip.SetAction(self, slot)
|
||
local actions = CleveRoids.GetAction(slot)
|
||
|
||
-- If this is our macro but has no active action, show just the macro name
|
||
if actions and not actions.active then
|
||
local macroName = GetActionText(slot)
|
||
if macroName then
|
||
GameTooltip:SetText(macroName)
|
||
GameTooltip:Show()
|
||
return
|
||
end
|
||
end
|
||
|
||
local action_to_display_info = nil
|
||
if actions then
|
||
-- Only show spell/item tooltip when there's an active action
|
||
if actions.active then
|
||
action_to_display_info = actions.active
|
||
end
|
||
end
|
||
|
||
if action_to_display_info and action_to_display_info.action then
|
||
local action_name = action_to_display_info.action
|
||
|
||
-- NEW: Check if action is a slot ID for tooltip
|
||
local slotId = tonumber(action_name)
|
||
if slotId and slotId >= 1 and slotId <= 19 then
|
||
-- Use the more specific SetInventoryItem function to prevent conflicts with other addons.
|
||
GameTooltip:SetInventoryItem("player", slotId)
|
||
GameTooltip:Show()
|
||
return
|
||
end
|
||
-- End new logic
|
||
|
||
local current_spell_data = CleveRoids.GetSpell(action_name)
|
||
if current_spell_data then
|
||
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
|
||
local rank_info = current_spell_data.rank or (current_spell_data.highest and current_spell_data.highest.rank)
|
||
if rank_info and rank_info ~= "" then
|
||
GameTooltipTextRight1:SetText("|cff808080" .. rank_info .. "|r")
|
||
else
|
||
GameTooltipTextRight1:SetText("")
|
||
end
|
||
GameTooltipTextRight1:Show()
|
||
GameTooltip:Show()
|
||
return
|
||
end
|
||
|
||
local current_item_data = CleveRoids.GetItem(action_name)
|
||
if current_item_data then
|
||
-- Use specific functions based on where the item is located.
|
||
if current_item_data.inventoryID then
|
||
GameTooltip:SetInventoryItem("player", current_item_data.inventoryID)
|
||
elseif current_item_data.bagID and current_item_data.slot then
|
||
GameTooltip:SetBagItem(current_item_data.bagID, current_item_data.slot)
|
||
else
|
||
-- Fallback to the original method if location is unknown.
|
||
GameTooltip:SetHyperlink(current_item_data.link)
|
||
end
|
||
GameTooltip:Show()
|
||
return
|
||
end
|
||
|
||
if action_to_display_info.macro and type(action_to_display_info.macro) == "table" then
|
||
local nested_action_info = action_to_display_info.macro
|
||
local nested_action_name = nested_action_info.action
|
||
|
||
current_spell_data = CleveRoids.GetSpell(nested_action_name)
|
||
if current_spell_data then
|
||
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
|
||
local rank_info = current_spell_data.rank or (current_spell_data.highest and current_spell_data.highest.rank)
|
||
if rank_info and rank_info ~= "" then
|
||
GameTooltipTextRight1:SetText("|cff808080" .. rank_info .. "|r")
|
||
else
|
||
GameTooltipTextRight1:SetText("")
|
||
end
|
||
GameTooltipTextRight1:Show()
|
||
GameTooltip:Show()
|
||
return
|
||
end
|
||
|
||
current_item_data = CleveRoids.GetItem(nested_action_name)
|
||
if current_item_data then
|
||
if current_item_data.inventoryID then
|
||
GameTooltip:SetInventoryItem("player", current_item_data.inventoryID)
|
||
elseif current_item_data.bagID and current_item_data.slot then
|
||
GameTooltip:SetBagItem(current_item_data.bagID, current_item_data.slot)
|
||
else
|
||
GameTooltip:SetHyperlink(current_item_data.link)
|
||
end
|
||
GameTooltip:Show()
|
||
return
|
||
end
|
||
end
|
||
end
|
||
|
||
-- If none of our custom logic handled it, call the original function we saved earlier.
|
||
CleveRoids.Hooks.GameTooltip.SetAction(self, slot)
|
||
end
|
||
|
||
CleveRoids.Hooks.PickupAction = PickupAction
|
||
function PickupAction(slot)
|
||
if not slot then return end
|
||
CleveRoids.ClearAction(slot)
|
||
CleveRoids.ClearSlot(CleveRoids.actionSlots, slot)
|
||
CleveRoids.ClearAction(CleveRoids.reactiveSlots, slot)
|
||
return CleveRoids.Hooks.PickupAction(slot)
|
||
end
|
||
|
||
CleveRoids.Hooks.ActionHasRange = ActionHasRange
|
||
function ActionHasRange(slot)
|
||
if not slot then return nil end
|
||
local actions = CleveRoids.GetAction(slot)
|
||
-- Only override for our macros with #showtooltip
|
||
if actions and actions.tooltip and actions.active then
|
||
if actions.active.inRange ~= -1 then
|
||
return 1 -- Has range check with valid data
|
||
else
|
||
-- For channeled spells (inRange == -1), try proxy slot lookup
|
||
local spellName = actions.active.spell and actions.active.spell.name
|
||
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
|
||
if proxySlot then
|
||
return CleveRoids.Hooks.ActionHasRange(proxySlot)
|
||
end
|
||
end
|
||
end
|
||
-- Not a macro we're tracking - pass through to original
|
||
return CleveRoids.Hooks.ActionHasRange(slot)
|
||
end
|
||
|
||
CleveRoids.Hooks.IsActionInRange = IsActionInRange
|
||
function IsActionInRange(slot, unit)
|
||
if not slot then return nil end
|
||
local actions = CleveRoids.GetAction(slot)
|
||
-- Only override for our macros with #showtooltip
|
||
if actions and actions.tooltip and actions.active and actions.active.type == "spell" then
|
||
if actions.active.inRange ~= -1 then
|
||
return actions.active.inRange
|
||
else
|
||
-- For channeled spells (inRange == -1), try proxy slot lookup
|
||
local spellName = actions.active.spell and actions.active.spell.name
|
||
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
|
||
if proxySlot then
|
||
return CleveRoids.Hooks.IsActionInRange(proxySlot, unit)
|
||
end
|
||
end
|
||
end
|
||
-- Not a macro we're tracking - pass through to original
|
||
return CleveRoids.Hooks.IsActionInRange(slot, unit)
|
||
end
|
||
|
||
CleveRoids.Hooks.OriginalIsUsableAction = IsUsableAction
|
||
CleveRoids.Hooks.IsUsableAction = IsUsableAction
|
||
function IsUsableAction(slot, unit)
|
||
if not slot then return nil, nil end
|
||
local actions = CleveRoids.GetAction(slot)
|
||
|
||
-- If this is one of our macros AND it uses #showtooltip
|
||
if actions and actions.tooltip then
|
||
-- IMPORTANT: Only override usability when #showtooltip is present
|
||
-- Macros without #showtooltip should use default game behavior
|
||
if actions.active then
|
||
-- We have an active action - return its usable state
|
||
return actions.active.usable, actions.active.oom
|
||
else
|
||
-- This is our macro but no action is active (all conditionals failed)
|
||
-- Return nil to make the icon dark
|
||
return nil, nil
|
||
end
|
||
else
|
||
-- Not our macro OR no #showtooltip - use game's default behavior
|
||
return CleveRoids.Hooks.IsUsableAction(slot, unit)
|
||
end
|
||
end
|
||
|
||
CleveRoids.Hooks.IsCurrentAction = IsCurrentAction
|
||
function IsCurrentAction(slot)
|
||
if not slot then return nil end
|
||
local actions = CleveRoids.GetAction(slot)
|
||
|
||
-- Use the same priority as GetActionTexture: active first, then tooltip
|
||
local actionToCheck = (actions and actions.active) or (actions and actions.tooltip)
|
||
|
||
if not actionToCheck then
|
||
return CleveRoids.Hooks.IsCurrentAction(slot)
|
||
else
|
||
local name
|
||
if actionToCheck.spell then
|
||
local rank = actionToCheck.spell.rank or actionToCheck.spell.highest.rank
|
||
name = actionToCheck.spell.name..(rank and ("("..rank..")"))
|
||
|
||
-- Check if this spell is currently queued or being cast via Nampower
|
||
-- Get spell ID for comparison
|
||
local spellId = actionToCheck.spell.id
|
||
if not spellId and GetSpellIdForName then
|
||
spellId = GetSpellIdForName(name)
|
||
end
|
||
|
||
if spellId then
|
||
-- Prefer GetCastInfo (Nampower 2.18+) for cleaner API
|
||
if GetCastInfo then
|
||
local ok, info = pcall(GetCastInfo)
|
||
if ok and info and info.spellId == spellId then
|
||
-- Spell is actively being cast/channeled
|
||
return true
|
||
end
|
||
end
|
||
|
||
-- Also check GetCurrentCastingInfo for queued spell detection
|
||
if GetCurrentCastingInfo then
|
||
local castId, visId, autoId, casting, channeling = GetCurrentCastingInfo()
|
||
|
||
-- Show glow if actively casting/channeling this spell
|
||
if (casting == 1 and castId == spellId) or (channeling == 1 and visId == spellId) then
|
||
return true
|
||
end
|
||
-- Show glow if this spell is queued (castId set but not yet casting)
|
||
if casting == 0 and channeling == 0 and castId == spellId then
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
elseif actionToCheck.item then
|
||
name = actionToCheck.item.name
|
||
end
|
||
|
||
return CleveRoids.Hooks.IsCurrentAction(CleveRoids.GetProxyActionSlot(name) or slot)
|
||
end
|
||
end
|
||
|
||
CleveRoids.Hooks.GetActionTexture = GetActionTexture
|
||
function GetActionTexture(slot)
|
||
if not slot then return nil end
|
||
local actions = CleveRoids.GetAction(slot)
|
||
|
||
-- Check if this is one of our macros
|
||
if actions and (actions.active or actions.tooltip) then
|
||
|
||
-- This block handles the case where all conditionals fail and no explicit
|
||
-- #showtooltip was set. It defaults to the macro's chosen icon.
|
||
if not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then
|
||
-- Get the macro's own icon as fallback
|
||
local macroTexture = nil
|
||
local macroName = GetActionText(slot)
|
||
if macroName then
|
||
local macroID = GetMacroIndexByName(macroName)
|
||
if macroID and macroID > 0 then
|
||
local _, texture = GetMacroInfo(macroID)
|
||
macroTexture = texture
|
||
end
|
||
end
|
||
|
||
-- When no conditionals pass, use macro icon (not first action's icon)
|
||
-- The actions.tooltip is just the first action which didn't pass conditionals
|
||
if macroTexture then
|
||
return macroTexture
|
||
end
|
||
|
||
-- Should never reach here, but return unknown as last resort
|
||
return CleveRoids.unknownTexture
|
||
end
|
||
|
||
-- Prioritize active action, fall back to tooltip
|
||
local a = actions.active or actions.tooltip
|
||
|
||
-- Handle numeric slot actions (e.g., /use 13)
|
||
local slotId = tonumber(a.action)
|
||
if slotId and slotId >= 1 and slotId <= 19 then
|
||
local currentTexture = GetInventoryItemTexture("player", slotId)
|
||
if currentTexture then
|
||
return currentTexture
|
||
end
|
||
|
||
-- Slot is empty, fall back to macro icon
|
||
local macroName = GetActionText(slot)
|
||
if macroName then
|
||
local macroID = GetMacroIndexByName(macroName)
|
||
if macroID and macroID > 0 then
|
||
local _, macroTexture = GetMacroInfo(macroID)
|
||
if macroTexture then
|
||
return macroTexture
|
||
end
|
||
end
|
||
end
|
||
return CleveRoids.unknownTexture
|
||
end
|
||
|
||
-- *** THIS IS THE FIX ***
|
||
-- If an action is active, return its texture directly.
|
||
-- If no action is active, return the tooltip's texture.
|
||
-- If neither has a texture, fall back to the macro's icon.
|
||
local texture = (actions.active and actions.active.texture) or (actions.tooltip and actions.tooltip.texture)
|
||
|
||
-- Check if this is a shapeshift form spell and use active texture if toggled on
|
||
if a and a.spell and a.action then
|
||
-- Strip rank info and underscores from spell name for comparison
|
||
local spellName = string.gsub(a.action, "%s*%(.-%)%s*$", "")
|
||
spellName = string.gsub(spellName, "_", " ")
|
||
-- Check all shapeshift forms to see if this spell matches and is active
|
||
for i = 1, GetNumShapeshiftForms() do
|
||
local icon, name, isActive, isCastable = GetShapeshiftFormInfo(i)
|
||
if name and string.lower(name) == string.lower(spellName) and isActive and icon then
|
||
texture = icon
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Check if this is a toggled buff ability (Prowl, Shadowmeld) and swap icon based on buff state
|
||
-- Note: Stealth is handled above by shapeshift form logic
|
||
if a and a.spell and a.action then
|
||
local spellName = string.gsub(a.action, "%s*%(.-%)%s*$", "")
|
||
spellName = string.gsub(spellName, "_", " ")
|
||
|
||
-- Check if this is one of our toggled buff abilities (not shapeshift forms)
|
||
local toggledAbilities = {
|
||
[CleveRoids.Localized.Spells["Prowl"]] = true,
|
||
[CleveRoids.Localized.Spells["Shadowmeld"]] = true,
|
||
}
|
||
|
||
if toggledAbilities[spellName] then
|
||
-- Check if the buff is active
|
||
if CleveRoids.ValidatePlayerBuff(spellName) then
|
||
-- Buff is active, use the active texture from auraTextures
|
||
local activeTexture = CleveRoids.auraTextures[spellName]
|
||
if activeTexture then
|
||
texture = activeTexture
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if texture then
|
||
return texture
|
||
end
|
||
|
||
-- Final fallback: get the macro's icon
|
||
local macroName = GetActionText(slot)
|
||
if macroName then
|
||
local macroID = GetMacroIndexByName(macroName)
|
||
if macroID and macroID > 0 then
|
||
local _, macroTexture = GetMacroInfo(macroID)
|
||
if macroTexture then
|
||
return macroTexture
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Should never reach here
|
||
return CleveRoids.unknownTexture
|
||
|
||
end
|
||
|
||
-- Not one of our macros, use the original function
|
||
return CleveRoids.Hooks.GetActionTexture(slot)
|
||
end
|
||
|
||
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
|
||
CleveRoids.Hooks.GetActionCooldown = GetActionCooldown
|
||
function GetActionCooldown(slot)
|
||
-- Guard against nil/invalid slot
|
||
if not slot then return 0, 0, 0 end
|
||
|
||
local actions = CleveRoids.GetAction(slot)
|
||
-- Check for actions.active OR actions.tooltip
|
||
if actions and (actions.active or actions.tooltip) then
|
||
-- Prioritize the active action, but fall back to the tooltip action
|
||
local a = actions.active or actions.tooltip
|
||
|
||
local slotId = tonumber(a.action)
|
||
if slotId and slotId >= 1 and slotId <= 19 then
|
||
return GetInventoryItemCooldown("player", slotId)
|
||
end
|
||
|
||
if a.spell then
|
||
return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType)
|
||
elseif a.item then
|
||
if a.item.bagID and a.item.slot then
|
||
return GetContainerItemCooldown(a.item.bagID, a.item.slot)
|
||
elseif a.item.inventoryID then
|
||
return GetInventoryItemCooldown("player", a.item.inventoryID)
|
||
end
|
||
end
|
||
return 0, 0, 0
|
||
else
|
||
return CleveRoids.Hooks.GetActionCooldown(slot)
|
||
end
|
||
end
|
||
|
||
CleveRoids.Hooks.GetActionCount = GetActionCount
|
||
function GetActionCount(slot)
|
||
-- Guard against nil/invalid slot
|
||
if not slot then return 0 end
|
||
|
||
local action = CleveRoids.GetAction(slot)
|
||
local count
|
||
-- Use the same priority as GetActionTexture: active first, then tooltip
|
||
local actionToCheck = (action and action.active) or (action and action.tooltip)
|
||
if actionToCheck then
|
||
|
||
local slotId = tonumber(actionToCheck.action)
|
||
if slotId and slotId >= 1 and slotId <= 19 then
|
||
return GetInventoryItemCount("player", slotId)
|
||
end
|
||
|
||
if actionToCheck.item then
|
||
count = actionToCheck.item.count
|
||
|
||
elseif actionToCheck.spell then
|
||
local reagent = actionToCheck.spell.reagent
|
||
if not reagent then
|
||
local ss, bt = actionToCheck.spell.spellSlot, actionToCheck.spell.bookType
|
||
if ss and bt then
|
||
local _, r = CleveRoids.GetSpellCost(ss, bt)
|
||
reagent = r
|
||
end
|
||
if (not reagent) and _ReagentBySpell and actionToCheck.spell.name then
|
||
reagent = _ReagentBySpell[actionToCheck.spell.name] -- e.g., Vanish → Flash Powder
|
||
end
|
||
actionToCheck.spell.reagent = reagent -- cache it so we don't re-scan every frame
|
||
end
|
||
if reagent then
|
||
count = CleveRoids.GetReagentCount(reagent) -- id-first bag scan, falls back to name/tooltip
|
||
end
|
||
end
|
||
end
|
||
|
||
return count or CleveRoids.Hooks.GetActionCount(slot)
|
||
end
|
||
|
||
CleveRoids.Hooks.IsConsumableAction = IsConsumableAction
|
||
function IsConsumableAction(slot)
|
||
-- Guard against nil/invalid slot
|
||
if not slot then return nil end
|
||
|
||
local action = CleveRoids.GetAction(slot)
|
||
-- Use the same priority as GetActionTexture: active first, then tooltip
|
||
local actionToCheck = (action and action.active) or (action and action.tooltip)
|
||
if actionToCheck then
|
||
|
||
local slotId = tonumber(actionToCheck.action)
|
||
if slotId and slotId >= 1 and slotId <= 19 then
|
||
local _, count = GetInventoryItemCount("player", slotId)
|
||
if count and count > 0 then return 1 end
|
||
end
|
||
|
||
if actionToCheck.item and
|
||
(CleveRoids.countedItemTypes[actionToCheck.item.type]
|
||
or CleveRoids.countedItemTypes[actionToCheck.item.name])
|
||
then
|
||
return 1
|
||
end
|
||
|
||
|
||
if actionToCheck.spell and actionToCheck.spell.reagent then
|
||
return 1
|
||
end
|
||
end
|
||
|
||
return CleveRoids.Hooks.IsConsumableAction(slot)
|
||
end
|
||
|
||
-- Create a hidden tooltip frame to read buff names
|
||
if not AuraScanTooltip and not CleveRoids.hasSuperwow then
|
||
CreateFrame("GameTooltip", "AuraScanTooltip")
|
||
AuraScanTooltip:SetOwner(WorldFrame, "ANCHORNONE")
|
||
AuraScanTooltip:AddFontStrings(
|
||
AuraScanTooltip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText"),
|
||
AuraScanTooltip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
||
)
|
||
end
|
||
|
||
-- Robust named tooltip for scanning spells/items
|
||
if not CleveRoidsTooltipScan then
|
||
-- Try to create with the standard template first
|
||
local ok, _ = pcall(CreateFrame, "GameTooltip", "CleveRoidsTooltipScan", UIParent, "GameTooltipTemplate")
|
||
if not ok or not CleveRoidsTooltipScan then
|
||
-- Fallback: manual tooltip with plenty of prebuilt lines
|
||
CleveRoidsTooltipScan = CreateFrame("GameTooltip", "CleveRoidsTooltipScan", UIParent)
|
||
local L1 = CleveRoidsTooltipScan:CreateFontString("$parentTextLeft1", nil, "GameTooltipText")
|
||
local R1 = CleveRoidsTooltipScan:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
|
||
CleveRoidsTooltipScan:AddFontStrings(L1, R1)
|
||
for i = 2, 32 do
|
||
CleveRoidsTooltipScan:CreateFontString("$parentTextLeft"..i, nil, "GameTooltipText")
|
||
CleveRoidsTooltipScan:CreateFontString("$parentTextRight"..i, nil, "GameTooltipText")
|
||
end
|
||
end
|
||
CleveRoidsTooltipScan:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||
end
|
||
|
||
-- This single dummy frame handles events AND serves as our tooltip scanner.
|
||
CleveRoids.Frame = CreateFrame("GameTooltip")
|
||
|
||
-- Create the extra font strings needed for other functions like GetSpellCost.
|
||
CleveRoids.Frame.costFontString = CleveRoids.Frame:CreateFontString()
|
||
CleveRoids.Frame.rangeFontString = CleveRoids.Frame:CreateFontString()
|
||
CleveRoids.Frame.reagentFontString = CleveRoids.Frame:CreateFontString()
|
||
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame:CreateFontString(), CleveRoids.Frame:CreateFontString())
|
||
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame.costFontString, CleveRoids.Frame.rangeFontString)
|
||
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame:CreateFontString(), CleveRoids.Frame:CreateFontString())
|
||
CleveRoids.Frame:AddFontStrings(CleveRoids.Frame.reagentFontString, CleveRoids.Frame:CreateFontString())
|
||
|
||
CleveRoids.Frame:SetScript("OnUpdate", CleveRoids.OnUpdate)
|
||
CleveRoids.Frame:SetScript("OnEvent", function(...)
|
||
CleveRoids.Frame[event](this,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10)
|
||
end)
|
||
|
||
-- == CORE EVENT REGISTRATION ==
|
||
CleveRoids.Frame:RegisterEvent("PLAYER_LOGIN")
|
||
CleveRoids.Frame:RegisterEvent("ADDON_LOADED")
|
||
CleveRoids.Frame:RegisterEvent("UPDATE_MACROS")
|
||
CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED")
|
||
CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
|
||
CleveRoids.Frame:RegisterEvent("BAG_UPDATE")
|
||
CleveRoids.Frame:RegisterEvent("UNIT_INVENTORY_CHANGED")
|
||
CleveRoids.Frame:RegisterEvent("UNIT_PET")
|
||
|
||
-- == STATE CHANGE EVENT REGISTRATION (for performance) ==
|
||
CleveRoids.Frame:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||
CleveRoids.Frame:RegisterEvent("PLAYER_FOCUS_CHANGED") -- For focus addons
|
||
CleveRoids.Frame:RegisterEvent("PLAYER_ENTER_COMBAT") -- Auto-attack started
|
||
CleveRoids.Frame:RegisterEvent("PLAYER_LEAVE_COMBAT") -- Auto-attack stopped
|
||
CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_DISABLED") -- Entered actual combat (has threat)
|
||
CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_ENABLED") -- Left actual combat (no threat)
|
||
CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
|
||
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
|
||
CleveRoids.Frame:RegisterEvent("UNIT_AURA")
|
||
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH")
|
||
CleveRoids.Frame:RegisterEvent("UNIT_POWER")
|
||
if CleveRoids.hasSuperwow then
|
||
CleveRoids.Frame:RegisterEvent("UNIT_CASTEVENT")
|
||
end
|
||
if QueueSpellByName then
|
||
CleveRoids.Frame:RegisterEvent("SPELL_QUEUE_EVENT")
|
||
CleveRoids.Frame:RegisterEvent("SPELL_CAST_EVENT")
|
||
end
|
||
CleveRoids.Frame:RegisterEvent("START_AUTOREPEAT_SPELL")
|
||
CleveRoids.Frame:RegisterEvent("STOP_AUTOREPEAT_SPELL")
|
||
CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_START")
|
||
CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
|
||
CleveRoids.Frame:RegisterEvent("SPELLCAST_START")
|
||
CleveRoids.Frame:RegisterEvent("SPELLCAST_STOP")
|
||
CleveRoids.Frame:RegisterEvent("SPELLCAST_FAILED")
|
||
CleveRoids.Frame:RegisterEvent("SPELLCAST_INTERRUPTED")
|
||
|
||
-- Nampower SPELL_CAST_EVENT for reliable channel tracking
|
||
if GetCurrentCastingInfo then
|
||
CleveRoids.Frame:RegisterEvent("SPELL_CAST_EVENT")
|
||
end
|
||
|
||
|
||
-- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua
|
||
-- which has the complete implementation including the INTERCEPT path for all commands
|
||
|
||
function CleveRoids.Frame:UNIT_PET()
|
||
if arg1 == "player" then
|
||
CleveRoids.IndexPetSpells()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Simplified PLAYER_LOGIN - requirements already checked
|
||
function CleveRoids.Frame:PLAYER_LOGIN()
|
||
-- Skip if already disabled
|
||
if CleveRoids.disabled then return end
|
||
|
||
_, CleveRoids.playerClass = UnitClass("player")
|
||
_, CleveRoids.playerGuid = UnitExists("player")
|
||
CleveRoids.IndexSpells()
|
||
CleveRoids.IndexPetSpells()
|
||
CleveRoids.initializationTimer = GetTime() + 1.5
|
||
|
||
-- PERFORMANCE: Initialize event-driven cache states
|
||
CleveRoids._cachedPlayerInCombat = UnitAffectingCombat("player") and true or false
|
||
|
||
-- Schedule delayed WDB warmup (loads items into client cache via tooltip scan)
|
||
-- This ensures GetItemInfo() works for all inventory items after a WDB clear
|
||
CleveRoids.wdbWarmupTime = GetTime() + 3.0 -- 3 second delay after login
|
||
end
|
||
|
||
-- PERFORMANCE: WDB warmup - tooltip scan all bag items to ensure they're cached
|
||
-- This prevents GetItemInfo() returning nil for items after a WDB clear
|
||
function CleveRoids.DoWDBWarmup()
|
||
if CleveRoids.wdbWarmupDone then return end
|
||
CleveRoids.wdbWarmupDone = true
|
||
|
||
-- Create a hidden tooltip for scanning if it doesn't exist
|
||
local tip = CleveRoidsWDBTip
|
||
if not tip then
|
||
tip = CreateFrame("GameTooltip", "CleveRoidsWDBTip", UIParent, "GameTooltipTemplate")
|
||
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||
end
|
||
|
||
local scanned = 0
|
||
|
||
-- Scan all bag slots
|
||
for bag = 0, 4 do
|
||
local slots = GetContainerNumSlots(bag) or 0
|
||
for slot = 1, slots do
|
||
local link = GetContainerItemLink(bag, slot)
|
||
if link then
|
||
-- Tooltip scan loads the item into WDB
|
||
tip:ClearLines()
|
||
tip:SetBagItem(bag, slot)
|
||
scanned = scanned + 1
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Scan equipped items
|
||
for slot = 0, 19 do
|
||
local link = GetInventoryItemLink("player", slot)
|
||
if link then
|
||
tip:ClearLines()
|
||
tip:SetInventoryItem("player", slot)
|
||
scanned = scanned + 1
|
||
end
|
||
end
|
||
|
||
-- Now trigger a full item index to populate the cache with valid data
|
||
if CleveRoids.IndexItems then
|
||
CleveRoids.IndexItems()
|
||
end
|
||
|
||
if CleveRoids.debug then
|
||
CleveRoids.Print("|cff88ff88[WDB Warmup]|r Scanned " .. scanned .. " items into cache")
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:ADDON_LOADED(addon)
|
||
-- keep your existing init for CRM:
|
||
if addon == "CleveRoidMacros" or addon == "SuperCleveRoidMacros" then
|
||
CleveRoids.InitializeExtensions()
|
||
end
|
||
-- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua
|
||
end
|
||
|
||
function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time)
|
||
-- Handle melee swings for judgement refresh
|
||
if action == "MAINHAND" or action == "OFFHAND" then
|
||
-- Only process if this is the player's melee swing
|
||
if caster == CleveRoids.playerGuid and CleveRoids.playerClass == "PALADIN" then
|
||
-- Refresh judgements on the target
|
||
-- Defensive: verify libdebuff is a table before accessing properties
|
||
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
|
||
if target and lib and lib.objects then
|
||
local normalizedTarget = CleveRoids.NormalizeGUID(target)
|
||
if normalizedTarget and lib.objects[normalizedTarget] then
|
||
-- Refresh all active Judgements on the target (pfUI-style: by name, not just ID)
|
||
for spellID, rec in pairs(lib.objects[normalizedTarget]) do
|
||
if rec.start and rec.duration then
|
||
local spellName = SpellInfo(spellID)
|
||
if spellName then
|
||
-- Remove rank to get base name
|
||
local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "")
|
||
|
||
-- Check if this is a judgement by name (pfUI approach)
|
||
if lib.judgementNames and lib.judgementNames[baseName] then
|
||
-- Only refresh if the Judgement is still active and was cast by player
|
||
local remaining = rec.duration + rec.start - GetTime()
|
||
if remaining > 0 and rec.caster == "player" then
|
||
-- Refresh the Judgement by updating the start time
|
||
rec.start = GetTime()
|
||
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on %s hit - new duration: %ds",
|
||
baseName, spellID, action, rec.duration)
|
||
)
|
||
end
|
||
|
||
-- Also sync to pfUI if it's loaded
|
||
if pfUI and pfUI.api and pfUI.api.libdebuff then
|
||
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
|
||
local targetLevel = UnitLevel("target") or 0
|
||
|
||
if targetName then
|
||
-- Refresh in pfUI's tracking (by name)
|
||
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player")
|
||
|
||
if CleveRoids.debug then
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cff00ffaa[pfUI Judgement Refresh]|r Synced %s refresh to pfUI", baseName)
|
||
)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return -- Still return early after processing melee
|
||
end
|
||
|
||
-- Debug channel tracking
|
||
if CleveRoids.ChannelTimeDebug then
|
||
local spellName = spell_id and SpellInfo and SpellInfo(spell_id) or "Unknown"
|
||
if string.find(spellName, "Arcane") then
|
||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UNIT_CASTEVENT]|r %s: %s (ID:%s) caster=%s player=%s",
|
||
action, spellName, tostring(spell_id), tostring(caster), tostring(CleveRoids.playerGuid)))
|
||
end
|
||
end
|
||
|
||
-- handle cast spell tracking
|
||
local cast = CleveRoids.spell_tracking[caster]
|
||
if cast_time > 0 and action == "START" or action == "CHANNEL" then
|
||
CleveRoids.spell_tracking[caster] = { spell_id = spell_id, expires = GetTime() + cast_time/1000, type = action }
|
||
|
||
-- ALSO store under "player" literal for easier lookup
|
||
if caster == CleveRoids.playerGuid then
|
||
CleveRoids.spell_tracking["player"] = CleveRoids.spell_tracking[caster]
|
||
|
||
-- For CHANNEL events, capture duration for checkchanneled conditional
|
||
if action == "CHANNEL" then
|
||
CleveRoids.channelStartTime = GetTime()
|
||
-- Try to get accurate duration from spell tooltip (reflects haste)
|
||
local tooltipDuration = CleveRoids.GetChannelDurationFromTooltipByID(spell_id)
|
||
if tooltipDuration then
|
||
CleveRoids.channelDuration = tooltipDuration
|
||
else
|
||
-- Fallback to UNIT_CASTEVENT duration (may not reflect haste)
|
||
CleveRoids.channelDuration = cast_time / 1000
|
||
end
|
||
end
|
||
|
||
-- For START events, capture cast time for checkcasting conditional
|
||
if action == "START" then
|
||
CleveRoids.castStartTime = GetTime()
|
||
-- For cast-time spells, UNIT_CASTEVENT duration is usually accurate
|
||
-- but we could add tooltip scanning here too if needed
|
||
CleveRoids.castDuration = cast_time / 1000
|
||
end
|
||
end
|
||
|
||
if CleveRoids.ChannelTimeDebug and caster == CleveRoids.playerGuid then
|
||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[Tracking]|r Set spell_tracking[%s] AND [player]: type=%s, expires=%.2f",
|
||
tostring(caster), action, GetTime() + cast_time/1000))
|
||
end
|
||
-- Always show for channels if debug is on
|
||
if CleveRoids.ChannelTimeDebug and action == "CHANNEL" then
|
||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[Tracking]|r caster=%s, playerGuid=%s, match=%s",
|
||
tostring(caster), tostring(CleveRoids.playerGuid), tostring(caster == CleveRoids.playerGuid)))
|
||
end
|
||
elseif cast
|
||
and (
|
||
(cast.spell_id == spell_id and (action == "FAIL" or action == "CAST"))
|
||
or (GetTime() > cast.expires)
|
||
)
|
||
then
|
||
if CleveRoids.ChannelTimeDebug and caster == CleveRoids.playerGuid then
|
||
local reason = ""
|
||
if cast.spell_id == spell_id and (action == "FAIL" or action == "CAST") then
|
||
reason = string.format("spell_id match (%s) and action=%s", tostring(spell_id), action)
|
||
elseif GetTime() > cast.expires then
|
||
reason = string.format("expired (%.2f > %.2f)", GetTime(), cast.expires)
|
||
end
|
||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[Tracking]|r CLEARING spell_tracking - %s", reason))
|
||
end
|
||
CleveRoids.spell_tracking[caster] = nil
|
||
-- Also clear "player" literal if this is the player
|
||
if caster == CleveRoids.playerGuid then
|
||
CleveRoids.spell_tracking["player"] = nil
|
||
end
|
||
end
|
||
|
||
-- handle cast sequence (SuperWoW)
|
||
if CleveRoids.currentSequence and caster == CleveRoids.playerGuid then
|
||
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
|
||
|
||
local name, rank = SpellInfo(spell_id)
|
||
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
|
||
local isSeqSpell = active and active.action and (
|
||
active.action == name or
|
||
(nameRank and active.action == nameRank)
|
||
)
|
||
|
||
if isSeqSpell then
|
||
local status = CleveRoids.currentSequence.status
|
||
if status == 0 and (action == "START" or action == "CHANNEL") and cast_time > 0 then
|
||
-- cast_time is ms; GetTime() is seconds
|
||
CleveRoids.currentSequence.status = 1
|
||
CleveRoids.currentSequence.expires = GetTime() + (cast_time / 1000) - 2
|
||
elseif (status == 0 and action == "CAST" and cast_time == 0)
|
||
or (status == 1 and action == "CAST" and CleveRoids.currentSequence.expires) then
|
||
CleveRoids.currentSequence.status = 2
|
||
CleveRoids.currentSequence.lastUpdate = GetTime()
|
||
CleveRoids.AdvanceSequence(CleveRoids.currentSequence)
|
||
CleveRoids.currentSequence = nil
|
||
elseif action == "INTERRUPTED" or action == "FAILED" then
|
||
CleveRoids.currentSequence.status = 1
|
||
end
|
||
end
|
||
end
|
||
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
-- Nampower SPELL_CAST_EVENT handler for reliable channel tracking
|
||
-- This is the PRIMARY source of truth for channel state (not GetCurrentCastingInfo polling)
|
||
function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGuid, itemId)
|
||
local CHANNEL = 4
|
||
|
||
if castType == CHANNEL and success == 1 then
|
||
-- Channel started successfully
|
||
CleveRoids.CurrentSpell.type = "channeled"
|
||
CleveRoids.CurrentSpell.castingSpellId = spellId
|
||
|
||
local spellName = SpellInfo and SpellInfo(spellId)
|
||
if spellName then
|
||
CleveRoids.CurrentSpell.spellName = spellName
|
||
end
|
||
|
||
-- Force immediate action update
|
||
CleveRoids.TestForAllActiveActions()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
|
||
-- Set channel state immediately when event fires
|
||
-- Duration is captured by UNIT_CASTEVENT which fires earlier
|
||
CleveRoids.CurrentSpell.type = "channeled"
|
||
|
||
-- Try to get spell info - prefer GetCastInfo (Nampower 2.18+) for better data
|
||
local spellId = nil
|
||
if GetCastInfo then
|
||
local ok, info = pcall(GetCastInfo)
|
||
if ok and info and info.spellId and info.spellId > 0 then
|
||
spellId = info.spellId
|
||
-- Also capture timing data
|
||
CleveRoids.CurrentSpell.castRemainingMs = info.castRemainingMs
|
||
CleveRoids.CurrentSpell.castEndTime = info.castEndS
|
||
end
|
||
end
|
||
-- Fallback to GetCurrentCastingInfo for older Nampower
|
||
if not spellId and GetCurrentCastingInfo then
|
||
local _, visId = GetCurrentCastingInfo()
|
||
if visId and visId > 0 then
|
||
spellId = visId
|
||
end
|
||
end
|
||
-- Update spell info
|
||
if spellId then
|
||
CleveRoids.CurrentSpell.castingSpellId = spellId
|
||
local spellName = SpellInfo(spellId)
|
||
if spellName then
|
||
CleveRoids.CurrentSpell.spellName = spellName
|
||
end
|
||
end
|
||
|
||
-- Force immediate action update
|
||
CleveRoids.TestForAllActiveActions()
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELLCAST_CHANNEL_STOP()
|
||
-- Channel ended - clear state immediately
|
||
CleveRoids.CurrentSpell.type = ""
|
||
CleveRoids.CurrentSpell.spellName = ""
|
||
CleveRoids.CurrentSpell.castingSpellId = nil
|
||
|
||
-- WARLOCK DARK HARVEST: Mark channeling as ended
|
||
-- Credits: Avitasia / Cursive addon
|
||
if CleveRoids.darkHarvestData and CleveRoids.darkHarvestData.isActive then
|
||
CleveRoids.darkHarvestData.isActive = false
|
||
CleveRoids.darkHarvestData.endTime = GetTime()
|
||
|
||
-- Apply Dark Harvest end to all DoTs on target (finalizes reduction)
|
||
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyDarkHarvestEnd then
|
||
CleveRoids.libdebuff.ApplyDarkHarvestEnd(CleveRoids.darkHarvestData.targetGUID)
|
||
end
|
||
|
||
if CleveRoids.debug then
|
||
local activeTime = CleveRoids.darkHarvestData.endTime - CleveRoids.darkHarvestData.startTime
|
||
DEFAULT_CHAT_FRAME:AddMessage(
|
||
string.format("|cff9482c9[Dark Harvest]|r Channel ended after %.1fs (DoT acceleration stopped)",
|
||
activeTime)
|
||
)
|
||
end
|
||
end
|
||
|
||
-- Force immediate action update
|
||
CleveRoids.TestForAllActiveActions()
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELLCAST_START()
|
||
-- Cast-time spell started
|
||
-- Duration is captured by UNIT_CASTEVENT which fires earlier
|
||
CleveRoids.CurrentSpell.type = "cast"
|
||
|
||
-- Try to get spell info - prefer GetCastInfo (Nampower 2.18+) for better data
|
||
local spellId = nil
|
||
if GetCastInfo then
|
||
local ok, info = pcall(GetCastInfo)
|
||
if ok and info and info.spellId and info.spellId > 0 then
|
||
spellId = info.spellId
|
||
-- Also capture timing data
|
||
CleveRoids.CurrentSpell.castRemainingMs = info.castRemainingMs
|
||
CleveRoids.CurrentSpell.castEndTime = info.castEndS
|
||
CleveRoids.CurrentSpell.gcdRemainingMs = info.gcdRemainingMs
|
||
CleveRoids.CurrentSpell.gcdEndTime = info.gcdEndS
|
||
end
|
||
end
|
||
-- Fallback to GetCurrentCastingInfo for older Nampower
|
||
if not spellId and GetCurrentCastingInfo then
|
||
local castId = GetCurrentCastingInfo()
|
||
if castId and castId > 0 then
|
||
spellId = castId
|
||
end
|
||
end
|
||
-- Update spell info
|
||
if spellId then
|
||
CleveRoids.CurrentSpell.castingSpellId = spellId
|
||
local spellName = SpellInfo(spellId)
|
||
if spellName then
|
||
CleveRoids.CurrentSpell.spellName = spellName
|
||
end
|
||
end
|
||
|
||
-- Force immediate action update
|
||
CleveRoids.TestForAllActiveActions()
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELLCAST_STOP()
|
||
-- Cast finished - clear state immediately
|
||
if CleveRoids.CurrentSpell.type == "cast" then
|
||
CleveRoids.CurrentSpell.type = ""
|
||
CleveRoids.CurrentSpell.spellName = ""
|
||
CleveRoids.CurrentSpell.castingSpellId = nil
|
||
|
||
-- Force immediate action update
|
||
CleveRoids.TestForAllActiveActions()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELLCAST_FAILED()
|
||
-- Cast failed - clear state immediately
|
||
if CleveRoids.CurrentSpell.type == "cast" then
|
||
CleveRoids.CurrentSpell.type = ""
|
||
CleveRoids.CurrentSpell.spellName = ""
|
||
CleveRoids.CurrentSpell.castingSpellId = nil
|
||
|
||
-- Force immediate action update
|
||
CleveRoids.TestForAllActiveActions()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELLCAST_INTERRUPTED()
|
||
-- Cast interrupted - clear state immediately
|
||
if CleveRoids.CurrentSpell.type == "cast" then
|
||
CleveRoids.CurrentSpell.type = ""
|
||
CleveRoids.CurrentSpell.spellName = ""
|
||
CleveRoids.CurrentSpell.castingSpellId = nil
|
||
|
||
-- Force immediate action update
|
||
CleveRoids.TestForAllActiveActions()
|
||
end
|
||
end
|
||
|
||
-- PLAYER_ENTER_COMBAT/PLAYER_LEAVE_COMBAT are for AUTO-ATTACK state (not actual combat)
|
||
function CleveRoids.Frame:PLAYER_ENTER_COMBAT()
|
||
CleveRoids.CurrentSpell.autoAttack = true
|
||
CleveRoids.CurrentSpell.autoAttackLock = false
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:PLAYER_LEAVE_COMBAT()
|
||
CleveRoids.CurrentSpell.autoAttack = false
|
||
CleveRoids.CurrentSpell.autoAttackLock = false
|
||
|
||
-- Reset any sequence with reset=combat that has progressed past the first step
|
||
for _, sequence in pairs(CleveRoids.Sequences) do
|
||
if sequence.index > 1 and sequence.reset and sequence.reset.combat then
|
||
CleveRoids.ResetSequence(sequence)
|
||
end
|
||
end
|
||
|
||
-- Full re-index after combat to refresh any items/bags that were skipped
|
||
-- during combat for performance. Use a slight delay to avoid spam.
|
||
local now = GetTime()
|
||
if (now - (CleveRoids.lastItemIndexTime or 0)) > 0.5 then
|
||
CleveRoids.lastItemIndexTime = now
|
||
CleveRoids.IndexItems()
|
||
CleveRoids.Actions = {}
|
||
CleveRoids.Macros = {}
|
||
CleveRoids.IndexActionBars()
|
||
end
|
||
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
-- PLAYER_REGEN_DISABLED/PLAYER_REGEN_ENABLED are for ACTUAL combat state (threat/aggro)
|
||
-- These events track what UnitAffectingCombat("player") reports
|
||
function CleveRoids.Frame:PLAYER_REGEN_DISABLED()
|
||
-- PERFORMANCE: Cache actual combat state for event-driven [combat]/[nocombat] conditionals
|
||
CleveRoids._cachedPlayerInCombat = true
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:PLAYER_REGEN_ENABLED()
|
||
-- PERFORMANCE: Cache actual combat state for event-driven [combat]/[nocombat] conditionals
|
||
CleveRoids._cachedPlayerInCombat = false
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:PLAYER_TARGET_CHANGED()
|
||
CleveRoids.CurrentSpell.autoAttack = false
|
||
CleveRoids.CurrentSpell.autoAttackLock = false
|
||
|
||
-- Clear resist state when target changes
|
||
CleveRoids.ClearResistState()
|
||
|
||
-- Reset any sequence with reset=target that has progressed past the first step
|
||
for _, sequence in pairs(CleveRoids.Sequences) do
|
||
if sequence.index > 1 and sequence.reset and sequence.reset.target then
|
||
CleveRoids.ResetSequence(sequence)
|
||
end
|
||
end
|
||
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:UPDATE_MACROS()
|
||
CleveRoids.currentSequence = nil
|
||
-- Explicitly nil tables before re-assignment
|
||
CleveRoids.ParsedMsg = nil;
|
||
CleveRoids.ParsedMsg = {}
|
||
|
||
CleveRoids.Macros = nil;
|
||
CleveRoids.Macros = {}
|
||
|
||
CleveRoids.Actions = nil;
|
||
CleveRoids.Actions = {}
|
||
|
||
CleveRoids.Sequences = nil;
|
||
CleveRoids.Sequences = {}
|
||
|
||
CleveRoids.IndexSpells()
|
||
CleveRoids.IndexTalents()
|
||
CleveRoids.IndexPetSpells()
|
||
CleveRoids.IndexActionBars()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELLS_CHANGED()
|
||
-- PERFORMANCE: Clear spell caches when spells change (learn new ranks, etc.)
|
||
CleveRoids.spellIdCache = {}
|
||
CleveRoids.spellNameCache = {}
|
||
CleveRoids.Frame:UPDATE_MACROS()
|
||
end
|
||
|
||
function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
|
||
CleveRoids.ClearAction(arg1)
|
||
CleveRoids.IndexActionSlot(arg1)
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:BAG_UPDATE()
|
||
-- In combat: Skip entirely for zero lag
|
||
if UnitAffectingCombat("player") then
|
||
return
|
||
end
|
||
|
||
-- Out of combat: Full indexing with throttle
|
||
local now = GetTime()
|
||
if (now - (CleveRoids.lastItemIndexTime or 0)) > 1.0 then
|
||
CleveRoids.lastItemIndexTime = now
|
||
CleveRoids.IndexItems()
|
||
|
||
-- Directly clear all relevant caches and force a UI refresh for all buttons.
|
||
CleveRoids.Actions = {}
|
||
CleveRoids.Macros = {}
|
||
CleveRoids.IndexActionBars()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:UNIT_INVENTORY_CHANGED()
|
||
if arg1 ~= "player" then return end
|
||
|
||
-- PERFORMANCE: Invalidate equipment cache for HasGearEquipped
|
||
if CleveRoids.InvalidateEquipmentCache then
|
||
CleveRoids.InvalidateEquipmentCache()
|
||
end
|
||
|
||
-- In combat: Skip ALL processing - EquipBagItem already handles cache invalidation
|
||
-- This eliminates lag from IndexEquippedItems during rapid gear swapping
|
||
if UnitAffectingCombat("player") then
|
||
return
|
||
end
|
||
|
||
-- Out of combat: Full indexing with throttle
|
||
local now = GetTime()
|
||
if (now - (CleveRoids.lastEquipIndexTime or 0)) < 0.2 then
|
||
CleveRoids.equipIndexPendingTime = now
|
||
return
|
||
end
|
||
|
||
CleveRoids.lastEquipIndexTime = now
|
||
CleveRoids.equipIndexPendingTime = nil
|
||
CleveRoids.lastItemIndexTime = now
|
||
CleveRoids.IndexItems()
|
||
CleveRoids.Actions = {}
|
||
CleveRoids.Macros = {}
|
||
CleveRoids.IndexActionBars()
|
||
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:START_AUTOREPEAT_SPELL()
|
||
local _, className = UnitClass("player")
|
||
if className == "HUNTER" then
|
||
CleveRoids.CurrentSpell.autoShot = true
|
||
else
|
||
CleveRoids.CurrentSpell.wand = true
|
||
end
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:STOP_AUTOREPEAT_SPELL()
|
||
local _, className = UnitClass("player")
|
||
if className == "HUNTER" then
|
||
CleveRoids.CurrentSpell.autoShot = false
|
||
else
|
||
CleveRoids.CurrentSpell.wand = false
|
||
end
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
|
||
-- Generic event handlers that just queue an update
|
||
function CleveRoids.Frame:PLAYER_FOCUS_CHANGED()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
function CleveRoids.Frame:UPDATE_SHAPESHIFT_FORM()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
function CleveRoids.Frame:SPELL_UPDATE_COOLDOWN()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
-- PERFORMANCE OPTIMIZATION: Throttled event handlers to reduce CPU spam
|
||
-- UNIT_AURA can fire dozens of times per second during combat
|
||
-- 100ms throttle reduces calls by 90%+ while maintaining responsiveness
|
||
function CleveRoids.Frame:UNIT_AURA()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
local now = GetTime()
|
||
if (now - CleveRoids.lastUnitAuraUpdate) >= CleveRoids.EVENT_THROTTLE then
|
||
CleveRoids.lastUnitAuraUpdate = now
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
end
|
||
function CleveRoids.Frame:UNIT_HEALTH()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
local now = GetTime()
|
||
if (now - CleveRoids.lastUnitHealthUpdate) >= CleveRoids.EVENT_THROTTLE then
|
||
CleveRoids.lastUnitHealthUpdate = now
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
end
|
||
function CleveRoids.Frame:UNIT_POWER()
|
||
if CleveRoidMacros.realtime == 0 then
|
||
local now = GetTime()
|
||
if (now - CleveRoids.lastUnitPowerUpdate) >= CleveRoids.EVENT_THROTTLE then
|
||
CleveRoids.lastUnitPowerUpdate = now
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELL_QUEUE_EVENT()
|
||
if event == "SPELL_QUEUE_EVENT" then
|
||
local eventCode = arg1
|
||
local spellId = arg2
|
||
|
||
local NORMAL_QUEUED = 2
|
||
local NON_GCD_QUEUED = 4
|
||
local ON_SWING_QUEUED = 0
|
||
local NORMAL_QUEUE_POPPED = 3
|
||
local NON_GCD_QUEUE_POPPED = 5
|
||
local ON_SWING_QUEUE_POPPED = 1
|
||
|
||
if eventCode == NORMAL_QUEUED or eventCode == NON_GCD_QUEUED or eventCode == ON_SWING_QUEUED then
|
||
CleveRoids.queuedSpell = {
|
||
spellId = spellId,
|
||
queueType = eventCode,
|
||
queueTime = GetTime()
|
||
}
|
||
if SpellInfo then
|
||
local name = SpellInfo(spellId)
|
||
if name then
|
||
CleveRoids.queuedSpell.spellName = name
|
||
end
|
||
end
|
||
-- BUGFIX: Update casting state when spell is queued (for [casting] conditional)
|
||
if CleveRoids.UpdateCastingState then
|
||
CleveRoids.UpdateCastingState()
|
||
end
|
||
CleveRoids.QueueActionUpdate()
|
||
elseif eventCode == NORMAL_QUEUE_POPPED or eventCode == NON_GCD_QUEUE_POPPED or eventCode == ON_SWING_QUEUE_POPPED then
|
||
CleveRoids.queuedSpell = nil
|
||
-- BUGFIX: Update casting state when spell queue pops (for [casting] conditional)
|
||
if CleveRoids.UpdateCastingState then
|
||
CleveRoids.UpdateCastingState()
|
||
end
|
||
CleveRoids.QueueActionUpdate()
|
||
end
|
||
end
|
||
end
|
||
|
||
function CleveRoids.Frame:SPELL_CAST_EVENT()
|
||
if event == "SPELL_CAST_EVENT" then
|
||
local success = arg1
|
||
local spellId = arg2
|
||
|
||
-- BUGFIX: Update casting state on spell cast events (for [casting] conditional)
|
||
if CleveRoids.UpdateCastingState then
|
||
CleveRoids.UpdateCastingState()
|
||
end
|
||
|
||
if success == 1 then
|
||
CleveRoids.lastCastSpell = {
|
||
spellId = spellId,
|
||
timestamp = GetTime()
|
||
}
|
||
if SpellInfo then
|
||
local name = SpellInfo(spellId)
|
||
if name then
|
||
CleveRoids.lastCastSpell.spellName = name
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
|
||
CleveRoids.Hooks.SendChatMessage = SendChatMessage
|
||
function SendChatMessage(msg, ...)
|
||
-- Filter out #showtooltip lines
|
||
-- pfUI's macrotweak also does this, but our pattern is more specific
|
||
if msg and string.find(msg, "^#showtooltip") then
|
||
return
|
||
end
|
||
|
||
-- Call the original (or pfUI's hook if it's in the chain)
|
||
CleveRoids.Hooks.SendChatMessage(msg, unpack(arg))
|
||
end
|
||
|
||
CleveRoids.RegisterActionEventHandler = function(fn)
|
||
if type(fn) == "function" then
|
||
table.insert(CleveRoids.actionEventHandlers, fn)
|
||
end
|
||
end
|
||
|
||
CleveRoids.RegisterMouseOverResolver = function(fn)
|
||
if type(fn) == "function" then
|
||
table.insert(CleveRoids.mouseOverResolvers, fn)
|
||
end
|
||
end
|
||
|
||
CleverMacro = true
|
||
|
||
do
|
||
local f = CreateFrame("Frame")
|
||
f:SetScript("OnEvent", function(self, event, arg1)
|
||
if event == "PLAYER_LOGIN" then
|
||
-- This ensures we wait until the player is fully in the world and all addons are loaded.
|
||
self:UnregisterEvent("PLAYER_LOGIN")
|
||
|
||
-- Ensure both pfUI and its focus module are loaded before attempting to hook.
|
||
-- This also checks that the slash command we want to modify exists.
|
||
if pfUI and pfUI.uf and pfUI.uf.focus and SlashCmdList.PFFOCUS then
|
||
|
||
local original_PFFOCUS_Handler = SlashCmdList.PFFOCUS
|
||
SlashCmdList.PFFOCUS = function(msg)
|
||
-- First, execute the original /focus command from pfUI to set the unit name.
|
||
original_PFFOCUS_Handler(msg)
|
||
|
||
-- Now, if a focus name was set, find the corresponding UnitID.
|
||
if pfUI.uf.focus.unitname then
|
||
local focusName = pfUI.uf.focus.unitname
|
||
local found_label, found_id = nil, nil
|
||
|
||
-- This function iterates through all known friendly units to find a
|
||
-- name match and return its specific UnitID components.
|
||
local function findUnitID()
|
||
-- Check party members and their pets
|
||
for i = 1, GetNumPartyMembers() do
|
||
if strlower(UnitName("party"..i) or "") == focusName then
|
||
return "party", i
|
||
end
|
||
if UnitExists("partypet"..i) and strlower(UnitName("partypet"..i) or "") == focusName then
|
||
return "partypet", i
|
||
end
|
||
end
|
||
|
||
-- Check raid members and their pets
|
||
for i = 1, GetNumRaidMembers() do
|
||
if strlower(UnitName("raid"..i) or "") == focusName then
|
||
return "raid", i
|
||
end
|
||
if UnitExists("raidpet"..i) and strlower(UnitName("raidpet"..i) or "") == focusName then
|
||
return "raidpet", i
|
||
end
|
||
end
|
||
|
||
-- Check player and pet
|
||
if strlower(UnitName("player") or "") == focusName then return "player", nil end
|
||
if UnitExists("pet") and strlower(UnitName("pet") or "") == focusName then return "pet", nil end
|
||
|
||
return nil, nil
|
||
end
|
||
|
||
found_label, found_id = findUnitID()
|
||
|
||
-- Store the found label and ID. CleveRoids' Core.lua will use this
|
||
-- for a direct, reliable cast without needing to change your target.
|
||
pfUI.uf.focus.label = found_label
|
||
pfUI.uf.focus.id = found_id
|
||
else
|
||
-- Focus was cleared (e.g., via /clearfocus), so ensure our cached data is cleared too.
|
||
pfUI.uf.focus.label = nil
|
||
pfUI.uf.focus.id = nil
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end)
|
||
f:RegisterEvent("PLAYER_LOGIN")
|
||
end
|
||
|
||
SLASH_CLEVEROID1 = "/cleveroid"
|
||
SLASH_CLEVEROID2 = "/cleveroidmacros"
|
||
SlashCmdList["CLEVEROID"] = function(msg)
|
||
if type(msg) ~= "string" then
|
||
msg = ""
|
||
end
|
||
local cmd, val, val2
|
||
local s, e, a, b, c = string.find(msg, "^(%S*)%s*(%S*)%s*(.*)$")
|
||
if a then cmd = a else cmd = "" end
|
||
if b then val = b else val = "" end
|
||
if c then val2 = c else val2 = "" end
|
||
|
||
-- No command: show current value and available commands
|
||
if cmd == "" then
|
||
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("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)")
|
||
if CleveRoids.hasSuperwow then
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages")
|
||
end
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrodebug - Toggle macro length warning debug")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrorefdebug - Toggle macro reference {Name} execution debug")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrostatus - Check macro length warning status")
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Spell School Detection:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid schooltest <spellID or name> - Test spell school detection')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listschools - List all learned spell schools')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearschools - Clear learned spell school data')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Immunity Tracking:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listimmune [school] - List immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "<NPC>" <school> [buff] - Add immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "<NPC>" <school> - Remove immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00CC Immunity Tracking:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listccimmune [type] - List CC immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addccimmune "<NPC>" <type> [buff] - Add CC immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeccimmune "<NPC>" <type> - Remove CC immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearccimmune [type] - Clear CC immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage(" CC types: stun, fear, root, silence, sleep, charm, polymorph, banish, horror, disorient, snare")
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Talent Modifiers:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid talenttabs - Show talent tab IDs for your class')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listtab <tab> - List all talents in a tab with their IDs')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid talents - Show current talent ranks')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testtalent <spellID> - Test talent modifier for a spell')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid diagnosetalent <spellID> - Diagnose talent modifier issues')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Equipment Modifiers:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testequip <spellID> - Test equipment modifier for a spell')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equipdebug <item name> - Debug item lookup for /equip')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equiplog - Toggle real-time equip command logging')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Reactive Proc Tracking:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listprocs - Show active reactive ability procs')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid setproc <spell> [duration] - Manually set proc (testing)')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearproc [spell|all] - Clear reactive proc(s)')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Casting Detection:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testcasting - Test [selfcasting]/[noselfcasting] conditionals')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testchannel - Test [channeltime] conditional tracking')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid channeldebug - Toggle [channeltime] conditional debug output')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Action Slot Debug:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid slotdebug <slot> - Debug action slot state (tooltip/range/mana)')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid rangedebug <spell> - Debug spell range checking (channeled spells)')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Slam Rotation (Warrior):|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid slamdebug - Show Slam cast time and clip window calculations')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Debuff Tracking Debug:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid debuffdebug [spell] - Debug debuff tracking on target')
|
||
return
|
||
end
|
||
|
||
-- realtime
|
||
if cmd == "realtime" then
|
||
local num = tonumber(val)
|
||
if num == 0 or num == 1 then
|
||
CleveRoidMacros.realtime = num
|
||
CleveRoids.Print("realtime set to " .. num)
|
||
else
|
||
CleveRoids.Print("Usage: /cleveroid realtime 0 or 1 - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load.)")
|
||
CleveRoids.Print("Current realtime = " .. tostring(CleveRoidMacros.realtime))
|
||
end
|
||
return
|
||
end
|
||
|
||
-- refresh
|
||
if cmd == "refresh" then
|
||
local num = tonumber(val)
|
||
if num and num >= 1 and num <= 10 then
|
||
CleveRoidMacros.refresh = num
|
||
CleveRoids.cachedRefreshRate = nil -- Invalidate cached rate
|
||
CleveRoids.Print("refresh set to " .. num .. " times per second")
|
||
else
|
||
CleveRoids.Print("Usage: /cleveroid refresh X - Set refresh rate. (1 to 10 updates per second. Default: 5)")
|
||
CleveRoids.Print("Current refresh = " .. tostring(CleveRoidMacros.refresh) .. " times per second")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- learn (manual set duration)
|
||
if cmd == "learn" then
|
||
if not CleveRoids.hasSuperwow then
|
||
CleveRoids.Print("Learning system requires SuperWoW client!")
|
||
return
|
||
end
|
||
local spellID = tonumber(val)
|
||
local duration = tonumber(val2)
|
||
if spellID and duration then
|
||
local _, playerGUID = UnitExists("player")
|
||
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
|
||
CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {}
|
||
CleveRoids_LearnedDurations[spellID][playerGUID] = duration
|
||
local spellName = SpellInfo(spellID) or "Unknown"
|
||
CleveRoids.Print("Set " .. spellName .. " (ID:" .. spellID .. ") duration to " .. duration .. "s")
|
||
else
|
||
CleveRoids.Print("Usage: /cleveroid learn <spellID> <duration> - Manually set spell duration")
|
||
CleveRoids.Print("Example: /cleveroid learn 11597 30")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- forget (delete learned duration)
|
||
if cmd == "forget" or cmd == "unlearn" then
|
||
if not CleveRoids.hasSuperwow then
|
||
CleveRoids.Print("Learning system requires SuperWoW client!")
|
||
return
|
||
end
|
||
if val == "all" then
|
||
CleveRoids_LearnedDurations = {}
|
||
CleveRoids.Print("Forgot all learned spell durations")
|
||
else
|
||
local spellID = tonumber(val)
|
||
if spellID and CleveRoids_LearnedDurations and CleveRoids_LearnedDurations[spellID] then
|
||
local spellName = SpellInfo(spellID) or "Unknown"
|
||
CleveRoids_LearnedDurations[spellID] = nil
|
||
CleveRoids.Print("Forgot " .. spellName .. " (ID:" .. spellID .. ") duration")
|
||
elseif spellID then
|
||
CleveRoids.Print("No learned duration for spell ID " .. spellID)
|
||
else
|
||
CleveRoids.Print("Usage: /cleveroid forget <spellID> - Forget learned duration")
|
||
CleveRoids.Print(" /cleveroid forget all - Forget all learned durations")
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
-- debug (toggle learning messages)
|
||
if cmd == "debug" then
|
||
local num = tonumber(val)
|
||
if num == 0 or num == 1 then
|
||
CleveRoids.debug = (num == 1)
|
||
CleveRoids.Print("debug set to " .. num)
|
||
else
|
||
CleveRoids.debug = not CleveRoids.debug
|
||
CleveRoids.Print("debug " .. (CleveRoids.debug and "enabled" or "disabled"))
|
||
end
|
||
return
|
||
end
|
||
|
||
-- macrodebug (toggle macro length warning debug messages)
|
||
if cmd == "macrodebug" then
|
||
CleveRoids.MacroLengthDebug = not CleveRoids.MacroLengthDebug
|
||
CleveRoids.Print("Macro length warning debug " .. (CleveRoids.MacroLengthDebug and "enabled" or "disabled"))
|
||
return
|
||
end
|
||
|
||
-- macrorefdebug (toggle macro reference execution debug messages)
|
||
if cmd == "macrorefdebug" or cmd == "refdebug" then
|
||
CleveRoids.macroRefDebug = not CleveRoids.macroRefDebug
|
||
CleveRoids.Print("Macro reference debug " .. (CleveRoids.macroRefDebug and "enabled" or "disabled"))
|
||
CleveRoids.Print("Use /cast [cond] {MacroName} and watch for [MacroRef] messages")
|
||
return
|
||
end
|
||
|
||
-- slotdebug <slot> - Debug action slot state for tooltip/range/mana issues
|
||
if cmd == "slotdebug" then
|
||
local slot = tonumber(val)
|
||
if not slot then
|
||
CleveRoids.Print("Usage: /cleveroid slotdebug <slot> - Debug action slot (1-120)")
|
||
return
|
||
end
|
||
local actions = CleveRoids.GetAction(slot)
|
||
CleveRoids.Print("|cff00ff00=== Slot " .. slot .. " Debug ===|r")
|
||
if not actions then
|
||
CleveRoids.Print("No actions data for slot " .. slot)
|
||
CleveRoids.Print("GetActionText: " .. tostring(GetActionText(slot)))
|
||
return
|
||
end
|
||
CleveRoids.Print("actions.tooltip: " .. tostring(actions.tooltip ~= nil))
|
||
CleveRoids.Print("actions.explicitTooltip: " .. tostring(actions.explicitTooltip))
|
||
CleveRoids.Print("actions.list count: " .. tostring(actions.list and table.getn(actions.list) or 0))
|
||
if actions.active then
|
||
CleveRoids.Print("|cff00ffffActive action:|r")
|
||
CleveRoids.Print(" .action: " .. tostring(actions.active.action))
|
||
CleveRoids.Print(" .type: " .. tostring(actions.active.type))
|
||
CleveRoids.Print(" .spell: " .. tostring(actions.active.spell ~= nil))
|
||
if actions.active.spell then
|
||
CleveRoids.Print(" .spell.name: " .. tostring(actions.active.spell.name))
|
||
CleveRoids.Print(" .spell.cost: " .. tostring(actions.active.spell.cost))
|
||
end
|
||
CleveRoids.Print(" .usable: " .. tostring(actions.active.usable))
|
||
CleveRoids.Print(" .oom: " .. tostring(actions.active.oom))
|
||
CleveRoids.Print(" .inRange: " .. tostring(actions.active.inRange))
|
||
CleveRoids.Print(" .conditionals: " .. tostring(actions.active.conditionals ~= nil))
|
||
if actions.active.conditionals then
|
||
CleveRoids.Print(" .target: " .. tostring(actions.active.conditionals.target))
|
||
end
|
||
else
|
||
CleveRoids.Print("|cffff0000No active action|r")
|
||
end
|
||
CleveRoids.Print("|cffaaaa00Hook conditions:|r")
|
||
local hasTooltip = actions.tooltip ~= nil
|
||
local hasActive = actions.active ~= nil
|
||
local typeIsSpell = actions.active and actions.active.type == "spell"
|
||
CleveRoids.Print(" actions.tooltip: " .. tostring(hasTooltip))
|
||
CleveRoids.Print(" actions.active: " .. tostring(hasActive))
|
||
CleveRoids.Print(" .type == 'spell': " .. tostring(typeIsSpell))
|
||
CleveRoids.Print(" Range hook would: " .. (hasTooltip and hasActive and typeIsSpell and "USE CUSTOM" or "FALLBACK"))
|
||
return
|
||
end
|
||
|
||
-- macrostatus (check macro length warning status)
|
||
if cmd == "macrostatus" then
|
||
CleveRoids.Print("|cff00ff00=== MacroLengthWarn Status ===|r")
|
||
|
||
-- Check if file loaded
|
||
if CleveRoids.MacroLengthWarnLoaded then
|
||
CleveRoids.Print("File: |cff00ff00LOADED|r")
|
||
else
|
||
CleveRoids.Print("File: |cffff0000NOT LOADED|r - Check for Lua errors!")
|
||
end
|
||
|
||
-- Check if extension is registered
|
||
local ext = CleveRoids.Extensions and CleveRoids.Extensions["MacroLengthWarn"]
|
||
if ext then
|
||
CleveRoids.Print("Extension: |cff00ff00REGISTERED|r")
|
||
|
||
-- Check if OnLoad was called
|
||
if ext.ShowMessages then
|
||
CleveRoids.Print("OnLoad: |cff00ff00CALLED|r")
|
||
|
||
-- Call the status function
|
||
ext.ShowMessages()
|
||
else
|
||
CleveRoids.Print("OnLoad: |cffff0000NOT CALLED|r")
|
||
end
|
||
else
|
||
CleveRoids.Print("Extension: |cffff0000NOT REGISTERED|r")
|
||
CleveRoids.Print("The MacroLengthWarn extension failed to register!")
|
||
end
|
||
|
||
-- Check key functions
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("Function Status:")
|
||
CleveRoids.Print(" EditMacro: " .. (EditMacro and "|cff00ff00EXISTS|r" or "|cffff0000MISSING|r"))
|
||
CleveRoids.Print(" MacroFrame_SaveMacro: " .. (MacroFrame_SaveMacro and "|cff00ff00EXISTS|r" or "|cffffff00NOT LOADED YET|r"))
|
||
|
||
return
|
||
end
|
||
|
||
-- channeldebug (toggle channeltime conditional debug)
|
||
if cmd == "channeldebug" then
|
||
CleveRoids.ChannelTimeDebug = not CleveRoids.ChannelTimeDebug
|
||
CleveRoids.Print("Channel time debug " .. (CleveRoids.ChannelTimeDebug and "enabled" or "disabled"))
|
||
if CleveRoids.ChannelTimeDebug then
|
||
CleveRoids.Print("You will see messages every time [channeltime] is evaluated")
|
||
CleveRoids.Print("This shows if your macro is re-evaluating during the channel")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- testchannel (debug channel time detection)
|
||
if cmd == "testchannel" or cmd == "channeltest" then
|
||
CleveRoids.Print("|cff00ff00=== Channel Time Test ===|r")
|
||
|
||
-- Check spell tracking
|
||
local playerCast = CleveRoids.spell_tracking[CleveRoids.playerGuid]
|
||
if not playerCast then
|
||
CleveRoids.Print("|cffffff00No spell tracking data for player|r")
|
||
CleveRoids.Print("You must be casting or channeling a spell for this to show data")
|
||
else
|
||
CleveRoids.Print("Spell tracking found:")
|
||
CleveRoids.Print(" Type: " .. tostring(playerCast.type))
|
||
CleveRoids.Print(" Spell ID: " .. tostring(playerCast.spell_id))
|
||
if playerCast.spell_id and SpellInfo then
|
||
local spellName = SpellInfo(playerCast.spell_id)
|
||
CleveRoids.Print(" Spell Name: " .. tostring(spellName))
|
||
end
|
||
if playerCast.expires then
|
||
local timeLeft = playerCast.expires - GetTime()
|
||
CleveRoids.Print(" Expires at: " .. tostring(playerCast.expires))
|
||
CleveRoids.Print(" Time left: " .. string.format("%.2f", timeLeft) .. "s")
|
||
|
||
-- Test conditionals
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("Conditional tests:")
|
||
CleveRoids.Print(" [channeltime:<0.5]: " .. (timeLeft < 0.5 and "|cff00ff00TRUE|r" or "|cffff0000FALSE|r"))
|
||
CleveRoids.Print(" [channeltime:<1.0]: " .. (timeLeft < 1.0 and "|cff00ff00TRUE|r" or "|cffff0000FALSE|r"))
|
||
CleveRoids.Print(" [channeltime:>2.0]: " .. (timeLeft > 2.0 and "|cff00ff00TRUE|r" or "|cffff0000FALSE|r"))
|
||
else
|
||
CleveRoids.Print(" Expires: NOT SET")
|
||
end
|
||
end
|
||
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ffffInstructions:|r")
|
||
CleveRoids.Print("1. Start channeling Arcane Missiles")
|
||
CleveRoids.Print("2. Run /cleveroid testchannel while channeling")
|
||
CleveRoids.Print("3. Check if spell tracking is working")
|
||
|
||
CleveRoids.Print("|cff00ff00=== End Test ===|r")
|
||
return
|
||
end
|
||
|
||
-- rangedebug <spell> - Debug range checking for a spell
|
||
if cmd == "rangedebug" or cmd == "testrange" then
|
||
-- Combine val and val2 for multi-word spell names
|
||
local spellName = val
|
||
if val2 and val2 ~= "" then
|
||
spellName = val .. " " .. val2
|
||
end
|
||
if not spellName or spellName == "" then
|
||
CleveRoids.Print("Usage: /cleveroid rangedebug <spell name>")
|
||
CleveRoids.Print("Example: /cleveroid rangedebug Arcane Missiles")
|
||
return
|
||
end
|
||
|
||
CleveRoids.Print("|cff00ff00=== Range Debug: " .. spellName .. " ===|r")
|
||
|
||
-- Get spell ID
|
||
local spellId = nil
|
||
if GetSpellIdForName then
|
||
spellId = GetSpellIdForName(spellName)
|
||
CleveRoids.Print("Spell ID: " .. (spellId and tostring(spellId) or "|cffff0000NOT FOUND|r"))
|
||
else
|
||
CleveRoids.Print("GetSpellIdForName: |cffff0000NOT AVAILABLE|r")
|
||
end
|
||
|
||
if spellId and spellId > 0 then
|
||
-- Check native IsSpellInRange
|
||
if IsSpellInRange then
|
||
local target = UnitExists("target") and "target" or nil
|
||
if target then
|
||
local result = IsSpellInRange(spellId, target)
|
||
CleveRoids.Print("Native IsSpellInRange: " .. tostring(result))
|
||
if result == -1 then
|
||
CleveRoids.Print(" |cffffff00(-1 means non-unit-targeted spell, using fallback)|r")
|
||
elseif result == nil then
|
||
CleveRoids.Print(" |cffffff00(nil means error or unknown spell)|r")
|
||
end
|
||
else
|
||
CleveRoids.Print("Native IsSpellInRange: |cffffff00No target selected|r")
|
||
end
|
||
end
|
||
|
||
-- Check GetSpellRec fields
|
||
local API = CleveRoids.NampowerAPI
|
||
if API then
|
||
-- Try rangeMax (may not exist)
|
||
local rangeMax = API.GetSpellField(spellId, "rangeMax")
|
||
CleveRoids.Print("rangeMax field: " .. (rangeMax and tostring(rangeMax) or "|cffffff00nil|r"))
|
||
|
||
-- Try rangeIndex
|
||
local rangeIndex = API.GetSpellField(spellId, "rangeIndex")
|
||
CleveRoids.Print("rangeIndex field: " .. (rangeIndex and tostring(rangeIndex) or "|cffffff00nil|r"))
|
||
|
||
if rangeIndex and API.SpellRangeTable then
|
||
local lookupRange = API.SpellRangeTable[rangeIndex]
|
||
CleveRoids.Print("SpellRangeTable[" .. rangeIndex .. "]: " .. (lookupRange and (tostring(lookupRange) .. " yards") or "|cffffff00not found|r"))
|
||
end
|
||
|
||
-- Final GetSpellRange result
|
||
local finalRange = API.GetSpellRange(spellId)
|
||
CleveRoids.Print("API.GetSpellRange: " .. (finalRange and (tostring(finalRange) .. " yards") or "|cffff0000nil|r"))
|
||
|
||
-- UnitXP distance check
|
||
if CleveRoids.hasUnitXP and UnitExists("target") then
|
||
local distance = UnitXP("distanceBetween", "player", "target")
|
||
CleveRoids.Print("Distance to target: " .. (distance and (string.format("%.1f", distance) .. " yards") or "|cffffff00nil|r"))
|
||
|
||
if finalRange and distance then
|
||
local inRange = distance <= finalRange
|
||
CleveRoids.Print("In range (distance <= spellRange): " .. (inRange and "|cff00ff00YES|r" or "|cffff0000NO|r"))
|
||
end
|
||
elseif not CleveRoids.hasUnitXP then
|
||
CleveRoids.Print("UnitXP: |cffff0000NOT INSTALLED (required for fallback)|r")
|
||
else
|
||
CleveRoids.Print("Distance check: |cffffff00No target selected|r")
|
||
end
|
||
|
||
-- Final API.IsSpellInRange result
|
||
if UnitExists("target") then
|
||
local finalResult = API.IsSpellInRange(spellId, "target")
|
||
CleveRoids.Print("API.IsSpellInRange: " .. tostring(finalResult))
|
||
else
|
||
-- Test without target
|
||
local finalResult = API.IsSpellInRange(spellId, "player")
|
||
CleveRoids.Print("API.IsSpellInRange (no target, using player): " .. tostring(finalResult))
|
||
end
|
||
|
||
-- Target type detection
|
||
local targetA = API.GetSpellField(spellId, "effectImplicitTargetA")
|
||
local targetB = API.GetSpellField(spellId, "effectImplicitTargetB")
|
||
|
||
-- Format target data (handle tables)
|
||
local function formatTarget(t)
|
||
if not t then return "|cffffff00nil|r" end
|
||
if type(t) == "table" then
|
||
local parts = {}
|
||
for i = 1, 3 do
|
||
table.insert(parts, tostring(t[i] or 0))
|
||
end
|
||
return "{" .. table.concat(parts, ", ") .. "}"
|
||
end
|
||
return tostring(t)
|
||
end
|
||
|
||
CleveRoids.Print("effectImplicitTargetA: " .. formatTarget(targetA))
|
||
CleveRoids.Print("effectImplicitTargetB: " .. formatTarget(targetB))
|
||
|
||
local isUnitTargeted = API.IsUnitTargetedSpell(spellId)
|
||
CleveRoids.Print("Is unit-targeted spell: " .. (isUnitTargeted == true and "|cff00ff00YES|r" or isUnitTargeted == false and "|cffff0000NO|r" or "|cffffff00UNKNOWN|r"))
|
||
|
||
-- Self-cast/ground-targeted detection summary
|
||
local isSelfCast = (isUnitTargeted == false)
|
||
CleveRoids.Print("Detected as self/ground-targeted: " .. (isSelfCast and "|cff00ff00YES|r" or "|cffff0000NO|r"))
|
||
end
|
||
end
|
||
|
||
CleveRoids.Print("|cff00ff00=== End Range Debug ===|r")
|
||
return
|
||
end
|
||
|
||
-- testcasting (debug casting state detection)
|
||
if cmd == "testcasting" or cmd == "casttest" then
|
||
CleveRoids.Print("|cff00ff00=== Casting State Test ===|r")
|
||
|
||
-- Check GetCurrentCastingInfo availability
|
||
if not GetCurrentCastingInfo then
|
||
CleveRoids.Print("|cffff0000ERROR: GetCurrentCastingInfo not available!|r")
|
||
CleveRoids.Print("This requires Nampower to be installed.")
|
||
return
|
||
end
|
||
|
||
-- Get current casting info
|
||
local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo()
|
||
|
||
CleveRoids.Print("GetCurrentCastingInfo() values:")
|
||
CleveRoids.Print(" castId: " .. tostring(castId))
|
||
CleveRoids.Print(" visId: " .. tostring(visId))
|
||
CleveRoids.Print(" autoId: " .. tostring(autoId))
|
||
CleveRoids.Print(" casting: " .. tostring(casting))
|
||
CleveRoids.Print(" channeling: " .. tostring(channeling))
|
||
CleveRoids.Print(" onswing: " .. tostring(onswing))
|
||
CleveRoids.Print(" autoattack: " .. tostring(autoattack))
|
||
|
||
-- Test conditionals
|
||
local isCasting = (casting and casting == 1) or false
|
||
local isChanneling = (channeling and channeling == 1) or false
|
||
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("Conditional results:")
|
||
CleveRoids.Print(" [selfcasting]: " .. tostring(isCasting or isChanneling))
|
||
CleveRoids.Print(" [noselfcasting]: " .. tostring(not isCasting and not isChanneling))
|
||
|
||
CleveRoids.Print(" ")
|
||
if isCasting then
|
||
CleveRoids.Print("|cffff00ffYou ARE currently CASTING|r")
|
||
elseif isChanneling then
|
||
CleveRoids.Print("|cffff00ffYou ARE currently CHANNELING|r")
|
||
else
|
||
CleveRoids.Print("|cff00ff00You are NOT casting or channeling|r")
|
||
end
|
||
|
||
CleveRoids.Print("|cff00ff00=== End Test ===|r")
|
||
return
|
||
end
|
||
|
||
-- listimmune (list immunity data)
|
||
if cmd == "listimmune" or cmd == "immunelist" then
|
||
CleveRoids.ListImmunities(val ~= "" and val or nil)
|
||
return
|
||
end
|
||
|
||
-- clearimmune (clear immunity data)
|
||
if cmd == "clearimmune" then
|
||
CleveRoids.ClearImmunities(val ~= "" and val or nil)
|
||
return
|
||
end
|
||
|
||
-- addimmune (manually add immunity)
|
||
if cmd == "addimmune" then
|
||
-- Parse: /cleveroid addimmune <NPC Name> <school> [buff]
|
||
-- Example: /cleveroid addimmune "Golemagg the Incinerator" fire
|
||
-- Example: /cleveroid addimmune Vaelastrasz fire "Burning Adrenaline"
|
||
local npcName, school, buffName = nil, nil, nil
|
||
|
||
-- Try to extract quoted NPC name
|
||
local _, _, quotedNpc, rest = string.find(msg, '^addimmune%s+"([^"]+)"%s*(.*)$')
|
||
if quotedNpc then
|
||
npcName = quotedNpc
|
||
-- Parse school and optional buff from rest
|
||
local _, _, sch, buff = string.find(rest, "^(%S+)%s*(.*)$")
|
||
school = sch
|
||
if buff and buff ~= "" then
|
||
-- Check if buff is quoted
|
||
local _, _, quotedBuff = string.find(buff, '^"([^"]+)"$')
|
||
buffName = quotedBuff or buff
|
||
end
|
||
else
|
||
-- No quoted NPC, use simple parsing
|
||
npcName = val
|
||
school = val2
|
||
end
|
||
|
||
CleveRoids.AddImmunity(npcName, school, buffName)
|
||
return
|
||
end
|
||
|
||
-- removeimmune (manually remove immunity)
|
||
if cmd == "removeimmune" then
|
||
-- Parse: /cleveroid removeimmune <NPC Name> <school>
|
||
local npcName, school = nil, nil
|
||
|
||
-- Try to extract quoted NPC name
|
||
local _, _, quotedNpc, sch = string.find(msg, '^removeimmune%s+"([^"]+)"%s*(%S*)$')
|
||
if quotedNpc then
|
||
npcName = quotedNpc
|
||
school = sch
|
||
else
|
||
npcName = val
|
||
school = val2
|
||
end
|
||
|
||
CleveRoids.RemoveImmunity(npcName, school)
|
||
return
|
||
end
|
||
|
||
-- ========== CC IMMUNITY COMMANDS ==========
|
||
|
||
-- listccimmune (list CC immunity data)
|
||
if cmd == "listccimmune" or cmd == "ccimmunelist" then
|
||
CleveRoids.ListCCImmunities(val ~= "" and val or nil)
|
||
return
|
||
end
|
||
|
||
-- clearccimmune (clear CC immunity data)
|
||
if cmd == "clearccimmune" then
|
||
CleveRoids.ClearCCImmunities(val ~= "" and val or nil)
|
||
return
|
||
end
|
||
|
||
-- addccimmune (manually add CC immunity)
|
||
if cmd == "addccimmune" then
|
||
-- Parse: /cleveroid addccimmune <NPC Name> <cctype> [buff]
|
||
-- Example: /cleveroid addccimmune "Stone Guardian" stun
|
||
-- Example: /cleveroid addccimmune "Boss Name" fear "Enrage"
|
||
local npcName, ccType, buffName = nil, nil, nil
|
||
|
||
-- Try to extract quoted NPC name
|
||
local _, _, quotedNpc, rest = string.find(msg, '^addccimmune%s+"([^"]+)"%s*(.*)$')
|
||
if quotedNpc then
|
||
npcName = quotedNpc
|
||
-- Parse ccType and optional buff from rest
|
||
local _, _, cct, buff = string.find(rest, "^(%S+)%s*(.*)$")
|
||
ccType = cct
|
||
if buff and buff ~= "" then
|
||
-- Check if buff is quoted
|
||
local _, _, quotedBuff = string.find(buff, '^"([^"]+)"$')
|
||
buffName = quotedBuff or buff
|
||
end
|
||
else
|
||
-- No quoted NPC, use simple parsing
|
||
npcName = val
|
||
ccType = val2
|
||
end
|
||
|
||
CleveRoids.AddCCImmunity(npcName, ccType, buffName)
|
||
return
|
||
end
|
||
|
||
-- removeccimmune (manually remove CC immunity)
|
||
if cmd == "removeccimmune" then
|
||
-- Parse: /cleveroid removeccimmune <NPC Name> <cctype>
|
||
local npcName, ccType = nil, nil
|
||
|
||
-- Try to extract quoted NPC name
|
||
local _, _, quotedNpc, cct = string.find(msg, '^removeccimmune%s+"([^"]+)"%s*(%S*)$')
|
||
if quotedNpc then
|
||
npcName = quotedNpc
|
||
ccType = cct
|
||
else
|
||
npcName = val
|
||
ccType = val2
|
||
end
|
||
|
||
CleveRoids.RemoveCCImmunityCommand(npcName, ccType)
|
||
return
|
||
end
|
||
|
||
-- schooltest (test spell school detection)
|
||
if cmd == "schooltest" or cmd == "testschool" then
|
||
local input = val
|
||
if input == "" then
|
||
CleveRoids.Print("Usage: /cleveroid schooltest <spellID or name>")
|
||
CleveRoids.Print("Example: /cleveroid schooltest 133")
|
||
CleveRoids.Print("Example: /cleveroid schooltest Fireball")
|
||
return
|
||
end
|
||
|
||
local spellID = tonumber(input)
|
||
local spellName = nil
|
||
local school = nil
|
||
|
||
if spellID then
|
||
-- Input is a spell ID
|
||
spellName = SpellInfo and SpellInfo(spellID)
|
||
school = CleveRoids.GetSpellSchoolByID(spellID)
|
||
else
|
||
-- Input is a spell name
|
||
spellName = input
|
||
spellID = CleveRoids.GetSpellIdForName and CleveRoids.GetSpellIdForName(spellName)
|
||
school = CleveRoids.GetSpellSchool(spellName, spellID)
|
||
end
|
||
|
||
CleveRoids.Print("|cff88ff88=== Spell School Test ===|r")
|
||
CleveRoids.Print("Spell: " .. (spellName or "Unknown") .. " (ID:" .. (spellID or "Unknown") .. ")")
|
||
CleveRoids.Print("School: " .. (school or "|cffff0000Unknown|r"))
|
||
|
||
if spellID and CleveRoids_SpellSchools[spellID] then
|
||
CleveRoids.Print("|cff00ff00Source:|r Learned from Nampower damage events")
|
||
elseif school then
|
||
CleveRoids.Print("|cffffaa00Source:|r Tooltip/pattern matching")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- listschools (show learned spell schools)
|
||
if cmd == "listschools" or cmd == "schools" then
|
||
CleveRoids.Print("|cff88ff88=== Learned Spell Schools ===|r")
|
||
if not CleveRoids_SpellSchools or not next(CleveRoids_SpellSchools) then
|
||
CleveRoids.Print("No spell schools learned yet. Deal damage to enemies!")
|
||
CleveRoids.Print("Nampower damage events will automatically track spell schools.")
|
||
else
|
||
local count = 0
|
||
local bySchool = {}
|
||
|
||
-- Group by school
|
||
for spellID, school in pairs(CleveRoids_SpellSchools) do
|
||
if not bySchool[school] then
|
||
bySchool[school] = {}
|
||
end
|
||
table.insert(bySchool[school], spellID)
|
||
count = count + 1
|
||
end
|
||
|
||
CleveRoids.Print("Total spells tracked: " .. count)
|
||
CleveRoids.Print(" ")
|
||
|
||
-- Display by school
|
||
for school, spellIDs in pairs(bySchool) do
|
||
local schoolColor = "|cff88ff88"
|
||
if school == "fire" then schoolColor = "|cffff4400"
|
||
elseif school == "frost" then schoolColor = "|cff00ffff"
|
||
elseif school == "nature" then schoolColor = "|cff00ff00"
|
||
elseif school == "shadow" then schoolColor = "|cff8800ff"
|
||
elseif school == "arcane" then schoolColor = "|cffff00ff"
|
||
elseif school == "holy" then schoolColor = "|cffffff00"
|
||
elseif school == "bleed" then schoolColor = "|cffff0000"
|
||
end
|
||
|
||
CleveRoids.Print(schoolColor .. string.upper(school) .. "|r (" .. table.getn(spellIDs) .. " spells):")
|
||
for _, spellID in ipairs(spellIDs) do
|
||
local spellName = SpellInfo and SpellInfo(spellID) or "Unknown"
|
||
CleveRoids.Print(" " .. spellName .. " (ID:" .. spellID .. ")")
|
||
end
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
-- clearschools (clear learned spell schools)
|
||
if cmd == "clearschools" then
|
||
local count = 0
|
||
if CleveRoids_SpellSchools then
|
||
for _ in pairs(CleveRoids_SpellSchools) do
|
||
count = count + 1
|
||
end
|
||
end
|
||
CleveRoids_SpellSchools = {}
|
||
CleveRoids.spellSchoolMapping = CleveRoids_SpellSchools
|
||
CleveRoids.Print("Cleared " .. count .. " learned spell schools")
|
||
return
|
||
end
|
||
|
||
-- combotrack (show combo point tracking info)
|
||
if cmd == "combotrack" or cmd == "combo" then
|
||
CleveRoids.ShowComboTracking()
|
||
return
|
||
end
|
||
|
||
-- comboclear (clear combo tracking data)
|
||
if cmd == "comboclear" then
|
||
CleveRoids.ComboPointTracking = {}
|
||
CleveRoids.ComboPointTracking.byID = {}
|
||
CleveRoids.Print("Combo point tracking data cleared")
|
||
return
|
||
end
|
||
|
||
-- combolearn (show learned combo durations)
|
||
if cmd == "combolearn" or cmd == "combodurations" then
|
||
CleveRoids.Print("=== Learned Combo Durations ===")
|
||
if not CleveRoids_ComboDurations or not next(CleveRoids_ComboDurations) then
|
||
CleveRoids.Print("No learned combo durations yet. Cast finishers and let them expire!")
|
||
else
|
||
for spellID, cpData in pairs(CleveRoids_ComboDurations) do
|
||
local spellName = SpellInfo(spellID) or ("Spell " .. spellID)
|
||
CleveRoids.Print(spellName .. " (ID:" .. spellID .. "):")
|
||
for cp = 1, 5 do
|
||
if cpData[cp] then
|
||
CleveRoids.Print(" " .. cp .. " CP = " .. cpData[cp] .. "s")
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
-- talenttabs (show talent tab IDs)
|
||
if cmd == "talenttabs" or cmd == "tabs" then
|
||
CleveRoids.Print("=== Talent Tabs ===")
|
||
for i = 1, GetNumTalentTabs() do
|
||
local name, _, pointsSpent = GetTalentTabInfo(i)
|
||
CleveRoids.Print("Tab " .. i .. ": " .. name .. " (" .. pointsSpent .. " points)")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- listtab (show all talents in a specific tab with their IDs)
|
||
if cmd == "listtab" or cmd == "tab" then
|
||
local tabNum = tonumber(val)
|
||
if not tabNum or tabNum < 1 or tabNum > GetNumTalentTabs() then
|
||
CleveRoids.Print("Usage: /cleveroid listtab <tab number>")
|
||
CleveRoids.Print("Example: /cleveroid listtab 2")
|
||
CleveRoids.Print("Use /cleveroid talenttabs to see your tab numbers")
|
||
return
|
||
end
|
||
|
||
local tabName = GetTalentTabInfo(tabNum)
|
||
CleveRoids.Print("=== " .. tabName .. " (Tab " .. tabNum .. ") ===")
|
||
|
||
local numTalents = GetNumTalents(tabNum)
|
||
for i = 1, numTalents do
|
||
local name, _, _, _, rank, maxRank = GetTalentInfo(tabNum, i)
|
||
if name then
|
||
local rankText = rank .. "/" .. maxRank
|
||
CleveRoids.Print("ID " .. i .. ": " .. name .. " [" .. rankText .. "]")
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
-- talents (show current talent ranks)
|
||
if cmd == "talents" or cmd == "talent" then
|
||
CleveRoids.Print("=== Current Talents ===")
|
||
|
||
-- Ensure talents are indexed
|
||
if not CleveRoids.Talents or table.getn(CleveRoids.Talents) == 0 then
|
||
if CleveRoids.IndexTalents then
|
||
CleveRoids.IndexTalents()
|
||
end
|
||
end
|
||
|
||
local count = 0
|
||
for name, rank in pairs(CleveRoids.Talents) do
|
||
if type(name) == "string" and tonumber(rank) and tonumber(rank) > 0 then
|
||
CleveRoids.Print(name .. ": Rank " .. rank)
|
||
count = count + 1
|
||
end
|
||
end
|
||
|
||
if count == 0 then
|
||
CleveRoids.Print("No talents learned yet!")
|
||
else
|
||
CleveRoids.Print("Total: " .. count .. " talents")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- diagnosetalent (comprehensive diagnostic for talent modifier)
|
||
if cmd == "diagnosetalent" or cmd == "diagtalent" then
|
||
local spellID = tonumber(val)
|
||
if not spellID then
|
||
CleveRoids.Print("Usage: /cleveroid diagnosetalent <spellID>")
|
||
CleveRoids.Print("Example: /cleveroid diagnosetalent 1943 (Rupture Rank 1)")
|
||
return
|
||
end
|
||
|
||
-- Inline diagnostic
|
||
local baseDuration = 10
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00=== Talent Modifier Diagnostic ===|r")
|
||
|
||
local spellName = SpellInfo(spellID)
|
||
if not spellName then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ERROR: Invalid spell ID " .. tostring(spellID) .. "|r")
|
||
return
|
||
end
|
||
DEFAULT_CHAT_FRAME:AddMessage("Spell: " .. spellName .. " (ID: " .. spellID .. ")")
|
||
|
||
local modifier = CleveRoids.talentModifiers and CleveRoids.talentModifiers[spellID]
|
||
if not modifier then
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000No talent modifier registered for this spell|r")
|
||
return
|
||
end
|
||
|
||
local talentDesc = modifier.talent or ("Tab " .. tostring(modifier.tab) .. " ID " .. tostring(modifier.id))
|
||
DEFAULT_CHAT_FRAME:AddMessage("Modifier registered: " .. talentDesc)
|
||
|
||
-- Test position-based lookup
|
||
local talentRank = 0
|
||
local lookupMethod = "none"
|
||
|
||
if modifier.tab and modifier.id then
|
||
local _, name, _, _, rank = GetTalentInfo(modifier.tab, modifier.id)
|
||
talentRank = tonumber(rank) or 0
|
||
lookupMethod = "position"
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Position lookup (Tab " .. modifier.tab .. ", ID " .. modifier.id .. "):|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage(" Talent name: " .. tostring(name))
|
||
DEFAULT_CHAT_FRAME:AddMessage(" Rank: " .. talentRank)
|
||
end
|
||
|
||
if modifier.talent and CleveRoids.GetTalentRank then
|
||
local nameRank = CleveRoids.GetTalentRank(modifier.talent)
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffaaaa00Name lookup (" .. modifier.talent .. "):|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage(" Rank: " .. nameRank)
|
||
if talentRank == 0 and nameRank > 0 then
|
||
talentRank = nameRank
|
||
lookupMethod = "name"
|
||
end
|
||
end
|
||
|
||
DEFAULT_CHAT_FRAME:AddMessage("Final rank: " .. talentRank .. " (via " .. lookupMethod .. ")")
|
||
|
||
if talentRank > 0 then
|
||
local modifiedDuration = modifier.modifier(baseDuration, talentRank)
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Test calculation:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage(" Base: " .. baseDuration .. "s")
|
||
DEFAULT_CHAT_FRAME:AddMessage(" Modified: " .. modifiedDuration .. "s")
|
||
DEFAULT_CHAT_FRAME:AddMessage(" Bonus: +" .. (modifiedDuration - baseDuration) .. "s")
|
||
else
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000You don't have " .. talentDesc .. "!|r")
|
||
end
|
||
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00=== End Diagnostic ===|r")
|
||
return
|
||
end
|
||
|
||
-- testtalent (test talent modifier for a spell)
|
||
if cmd == "testtalent" or cmd == "talenttest" then
|
||
local spellID = tonumber(val)
|
||
if not spellID then
|
||
CleveRoids.Print("Usage: /cleveroid testtalent <spellID>")
|
||
CleveRoids.Print("Example: /cleveroid testtalent 1943 (Rupture Rank 1)")
|
||
return
|
||
end
|
||
|
||
local spellName = SpellInfo(spellID) or ("Spell " .. spellID)
|
||
local modifier = CleveRoids.talentModifiers and CleveRoids.talentModifiers[spellID]
|
||
|
||
if not modifier then
|
||
CleveRoids.Print(spellName .. " (ID:" .. spellID .. ") has no talent modifier configured")
|
||
return
|
||
end
|
||
|
||
-- Get talent rank using position-based or name-based lookup
|
||
local talentRank = 0
|
||
local maxRank = 3
|
||
local talentName = modifier.talent or ("Tab " .. tostring(modifier.tab) .. " ID " .. tostring(modifier.id))
|
||
|
||
-- Position-based lookup (preferred)
|
||
if modifier.tab and modifier.id then
|
||
local _, name, _, _, rank, max = GetTalentInfo(modifier.tab, modifier.id)
|
||
talentRank = tonumber(rank) or 0
|
||
maxRank = tonumber(max) or 3
|
||
if name then
|
||
talentName = name
|
||
end
|
||
end
|
||
|
||
-- Name-based fallback
|
||
if talentRank == 0 and modifier.talent and CleveRoids.GetTalentRank then
|
||
talentRank = CleveRoids.GetTalentRank(modifier.talent)
|
||
end
|
||
|
||
CleveRoids.Print("=== Talent Modifier Test ===")
|
||
CleveRoids.Print("Spell: " .. spellName .. " (ID:" .. spellID .. ")")
|
||
CleveRoids.Print("Talent: " .. talentName)
|
||
CleveRoids.Print("Your Rank: " .. talentRank .. "/" .. maxRank)
|
||
|
||
if talentRank == 0 then
|
||
CleveRoids.Print("|cffff0000You don't have this talent!|r")
|
||
else
|
||
-- Test with a base duration (use 10s as example)
|
||
local baseDur = 10
|
||
local modDur = modifier.modifier(baseDur, talentRank)
|
||
CleveRoids.Print("Example: 10s base -> " .. modDur .. "s modified (+" .. (modDur - baseDur) .. "s)")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- testequip (test equipment modifier for a spell)
|
||
if cmd == "testequip" or cmd == "equiptest" then
|
||
local spellID = tonumber(val)
|
||
if not spellID then
|
||
CleveRoids.Print("Usage: /cleveroid testequip <spellID>")
|
||
CleveRoids.Print("Example: /cleveroid testequip 1079 (Rip Rank 1)")
|
||
return
|
||
end
|
||
|
||
local spellName = SpellInfo(spellID) or ("Spell " .. spellID)
|
||
local modifier = CleveRoids.equipmentModifiers and CleveRoids.equipmentModifiers[spellID]
|
||
|
||
if not modifier then
|
||
CleveRoids.Print(spellName .. " (ID:" .. spellID .. ") has no equipment modifier configured")
|
||
return
|
||
end
|
||
|
||
local itemID = CleveRoids.GetEquippedItemID and CleveRoids.GetEquippedItemID(modifier.slot)
|
||
local itemName = "None"
|
||
if itemID then
|
||
local itemLink = GetInventoryItemLink("player", modifier.slot)
|
||
if itemLink then
|
||
itemName = string.match(itemLink, "%[(.-)%]") or ("Item " .. itemID)
|
||
end
|
||
end
|
||
|
||
CleveRoids.Print("=== Equipment Modifier Test ===")
|
||
CleveRoids.Print("Spell: " .. spellName .. " (ID:" .. spellID .. ")")
|
||
CleveRoids.Print("Slot: " .. modifier.slot .. " (Ranged/Relic)")
|
||
CleveRoids.Print("Equipped: " .. itemName .. (itemID and (" [" .. itemID .. "]") or ""))
|
||
|
||
if not itemID then
|
||
CleveRoids.Print("|cffff0000No item equipped in this slot!|r")
|
||
else
|
||
-- Test with a base duration (use 10s as example)
|
||
local baseDur = 10
|
||
local modDur = modifier.modifier(baseDur, itemID)
|
||
if modDur ~= baseDur then
|
||
CleveRoids.Print("Example: 10s base -> " .. modDur .. "s modified")
|
||
if modDur < baseDur then
|
||
CleveRoids.Print("|cffff0000Duration reduced by " .. (baseDur - modDur) .. "s|r")
|
||
else
|
||
CleveRoids.Print("|cff00ff00Duration increased by " .. (modDur - baseDur) .. "s|r")
|
||
end
|
||
else
|
||
CleveRoids.Print("|cffffaa00This item has no effect on this spell|r")
|
||
end
|
||
end
|
||
return
|
||
end
|
||
|
||
-- equiplog (toggle real-time equip logging)
|
||
if cmd == "equiplog" then
|
||
CleveRoids.equipDebugLog = not CleveRoids.equipDebugLog
|
||
CleveRoids.Print("Equip command logging " .. (CleveRoids.equipDebugLog and "|cff00ff00ENABLED|r" or "|cffff0000DISABLED|r"))
|
||
if CleveRoids.equipDebugLog then
|
||
CleveRoids.Print("Use /equip commands to see detailed lookup info")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- equipdebug (debug item lookup for /equip command)
|
||
if cmd == "equipdebug" then
|
||
local itemName = val
|
||
if val2 and val2 ~= "" then
|
||
itemName = val .. " " .. val2
|
||
end
|
||
if not itemName or itemName == "" then
|
||
CleveRoids.Print("Usage: /cleveroid equipdebug <item name>")
|
||
CleveRoids.Print('Example: /cleveroid equipdebug Idol of Ferocity')
|
||
return
|
||
end
|
||
-- Strip quotes if present
|
||
itemName = string.gsub(itemName, '"', "")
|
||
itemName = string.gsub(itemName, "_", " ")
|
||
|
||
CleveRoids.Print("|cff00ff00=== Equipment Debug: " .. itemName .. " ===|r")
|
||
|
||
-- Check equipped slot 18 (relic/idol)
|
||
local slot18Link = GetInventoryItemLink("player", 18)
|
||
if slot18Link then
|
||
local _, _, slot18Name = string.find(slot18Link, "|h%[(.-)%]|h")
|
||
CleveRoids.Print("Slot 18 equipped: " .. (slot18Name or "unknown"))
|
||
else
|
||
CleveRoids.Print("Slot 18 equipped: (empty)")
|
||
end
|
||
|
||
-- Check cache
|
||
local Items = CleveRoids.Items or {}
|
||
local cached = Items[itemName]
|
||
CleveRoids.Print("|cffffaa00Cache lookup:|r")
|
||
if cached then
|
||
if type(cached) == "table" then
|
||
if cached.inventoryID then
|
||
CleveRoids.Print(" Found in cache: EQUIPPED (inventoryID=" .. cached.inventoryID .. ")")
|
||
elseif cached.bagID then
|
||
CleveRoids.Print(" Found in cache: BAG (bag=" .. cached.bagID .. ", slot=" .. cached.slot .. ")")
|
||
else
|
||
CleveRoids.Print(" Found in cache: (type unknown)")
|
||
end
|
||
elseif type(cached) == "string" then
|
||
CleveRoids.Print(" Found in cache: -> " .. cached .. " (canonical name)")
|
||
end
|
||
else
|
||
CleveRoids.Print(" Not in cache")
|
||
end
|
||
|
||
-- Try GetItemFast
|
||
CleveRoids.Print("|cffffaa00GetItemFast:|r")
|
||
local fastItem = CleveRoids.GetItemFast and CleveRoids.GetItemFast(itemName)
|
||
if fastItem then
|
||
if fastItem.inventoryID then
|
||
CleveRoids.Print(" Returns: EQUIPPED (inventoryID=" .. fastItem.inventoryID .. ")")
|
||
elseif fastItem.bagID then
|
||
CleveRoids.Print(" Returns: BAG (bag=" .. fastItem.bagID .. ", slot=" .. fastItem.slot .. ")")
|
||
end
|
||
else
|
||
CleveRoids.Print(" Returns: nil")
|
||
end
|
||
|
||
-- Try FindItemQuick
|
||
CleveRoids.Print("|cffffaa00FindItemQuick (scans equipped then bags):|r")
|
||
local quickItem = CleveRoids.FindItemQuick and CleveRoids.FindItemQuick(itemName)
|
||
if quickItem then
|
||
if quickItem.inventoryID then
|
||
CleveRoids.Print(" Returns: EQUIPPED (inventoryID=" .. quickItem.inventoryID .. ")")
|
||
elseif quickItem.bagID then
|
||
CleveRoids.Print(" Returns: BAG (bag=" .. quickItem.bagID .. ", slot=" .. quickItem.slot .. ")")
|
||
end
|
||
else
|
||
CleveRoids.Print(" Returns: nil (item not found)")
|
||
end
|
||
|
||
-- Check HasGearEquipped (used by [equipped] conditional)
|
||
CleveRoids.Print("|cffffaa00HasGearEquipped (conditional check):|r")
|
||
local hasEquipped = CleveRoids.HasGearEquipped and CleveRoids.HasGearEquipped(itemName)
|
||
CleveRoids.Print(" [equipped:" .. itemName .. "] = " .. (hasEquipped and "|cff00ff00TRUE|r" or "|cffff0000FALSE|r"))
|
||
|
||
-- Combat state
|
||
CleveRoids.Print("|cffffaa00Combat state:|r")
|
||
CleveRoids.Print(" UnitAffectingCombat: " .. (UnitAffectingCombat("player") and "IN COMBAT" or "not in combat"))
|
||
CleveRoids.Print(" playerClass: " .. (CleveRoids.playerClass or "unknown"))
|
||
|
||
return
|
||
end
|
||
|
||
-- listprocs (show active reactive procs)
|
||
if cmd == "listprocs" or cmd == "procs" or cmd == "reactive" then
|
||
CleveRoids.Print("=== Active Reactive Procs ===")
|
||
local found = false
|
||
local now = GetTime()
|
||
local _, currentTargetGUID = UnitExists("target")
|
||
|
||
if CleveRoids.reactiveProcs then
|
||
for spellName, procData in pairs(CleveRoids.reactiveProcs) do
|
||
if procData and procData.expiry and procData.expiry > now then
|
||
local remaining = procData.expiry - now
|
||
local guidInfo = ""
|
||
|
||
if procData.targetGUID then
|
||
local matches = currentTargetGUID and (currentTargetGUID == procData.targetGUID)
|
||
if matches then
|
||
guidInfo = " |cff00ff00[Current Target]|r"
|
||
else
|
||
local targetName = UnitName("target")
|
||
if targetName then
|
||
guidInfo = " |cffff0000[Wrong Target: " .. targetName .. "]|r"
|
||
else
|
||
guidInfo = " |cffff0000[No Target]|r"
|
||
end
|
||
end
|
||
end
|
||
|
||
CleveRoids.Print(spellName .. ": " .. string.format("%.1fs", remaining) .. " remaining" .. guidInfo)
|
||
found = true
|
||
end
|
||
end
|
||
end
|
||
|
||
if not found then
|
||
CleveRoids.Print("|cffffaa00No active reactive procs|r")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- setproc (manually set a reactive proc for testing)
|
||
if cmd == "setproc" or cmd == "procset" then
|
||
if val == "" then
|
||
CleveRoids.Print("Usage: /cleveroid setproc <spell> [duration]")
|
||
CleveRoids.Print("Example: /cleveroid setproc Overpower 5")
|
||
CleveRoids.Print("Note: Uses current target's GUID for target-specific procs")
|
||
return
|
||
end
|
||
|
||
local duration = tonumber(val2) or 5.0
|
||
local _, targetGUID = UnitExists("target")
|
||
|
||
if CleveRoids.SetReactiveProc then
|
||
CleveRoids.SetReactiveProc(val, duration, targetGUID)
|
||
local guidMsg = targetGUID and (" for target [" .. (UnitName("target") or "Unknown") .. "]") or ""
|
||
CleveRoids.Print("Set " .. val .. " proc for " .. duration .. " seconds" .. guidMsg)
|
||
else
|
||
CleveRoids.Print("|cffff0000Reactive proc system not loaded!|r")
|
||
end
|
||
return
|
||
end
|
||
|
||
-- clearproc (clear a reactive proc)
|
||
if cmd == "clearproc" or cmd == "procclear" then
|
||
if val == "" or val == "all" then
|
||
CleveRoids.reactiveProcs = {}
|
||
CleveRoids.Print("Cleared all reactive procs")
|
||
else
|
||
if CleveRoids.ClearReactiveProc then
|
||
CleveRoids.ClearReactiveProc(val)
|
||
CleveRoids.Print("Cleared " .. val .. " proc")
|
||
else
|
||
CleveRoids.Print("|cffff0000Reactive proc system not loaded!|r")
|
||
end
|
||
end
|
||
CleveRoids.QueueActionUpdate()
|
||
return
|
||
end
|
||
|
||
-- slamdebug (show Slam cast time and window calculations for Warrior rotation)
|
||
if cmd == "slamdebug" or cmd == "slam" or cmd == "slamtime" then
|
||
CleveRoids.Print("|cff88ff88=== Slam Clip Window Debug ===|r")
|
||
|
||
-- Get swing timer info
|
||
local attackSpeed = UnitAttackSpeed("player")
|
||
if not attackSpeed or attackSpeed <= 0 then
|
||
CleveRoids.Print("|cffff0000No attack speed available!|r")
|
||
CleveRoids.Print("You need to be in combat or have a weapon equipped.")
|
||
return
|
||
end
|
||
|
||
-- Check SP_SwingTimer
|
||
if st_timer == nil then
|
||
CleveRoids.Print("|cffff0000SP_SwingTimer not detected!|r")
|
||
CleveRoids.Print("The [noslamclip] conditionals require SP_SwingTimer addon.")
|
||
CleveRoids.Print("Get it at: https://github.com/jrc13245/SP_SwingTimer")
|
||
else
|
||
local timeElapsed = attackSpeed - st_timer
|
||
local percentElapsed = (timeElapsed / attackSpeed) * 100
|
||
CleveRoids.Print("|cffffaa00SP_SwingTimer:|r st_timer=" .. string.format("%.3f", st_timer) .. "s")
|
||
CleveRoids.Print("Swing elapsed: " .. string.format("%.2f", percentElapsed) .. "%")
|
||
end
|
||
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cffffaa00Swing Timer:|r " .. string.format("%.3f", attackSpeed) .. "s")
|
||
|
||
-- Get Slam cast time from tooltip scanning
|
||
local slamCastTime = CleveRoids.GetSlamCastTime()
|
||
local isDefault = (slamCastTime == 1.5)
|
||
|
||
CleveRoids.Print("|cffffaa00Slam Cast Time:|r " .. string.format("%.3f", slamCastTime) .. "s" .. (isDefault and " (default)" or " (from tooltip)"))
|
||
|
||
-- Show tooltip scanning diagnostic info
|
||
if CleveRoids.GetSlamSpellSlot then
|
||
local slot = CleveRoids.GetSlamSpellSlot()
|
||
if slot then
|
||
CleveRoids.Print(" Slam found in spellbook slot: " .. slot)
|
||
else
|
||
CleveRoids.Print(" |cffff0000Slam not found in spellbook!|r Are you a Warrior?")
|
||
end
|
||
end
|
||
|
||
if isDefault then
|
||
CleveRoids.Print(" Note: Could not read from tooltip, using default 1.5s")
|
||
end
|
||
|
||
-- Calculate windows
|
||
CleveRoids.Print(" ")
|
||
local slamWindowTime = attackSpeed - slamCastTime
|
||
local slamWindow = 0
|
||
if slamWindowTime > 0 then
|
||
slamWindow = (slamWindowTime / attackSpeed) * 100
|
||
end
|
||
|
||
local instantWindowTime = (2 * attackSpeed) - slamCastTime - 1.5
|
||
local instantWindow = 0
|
||
if instantWindowTime > 0 then
|
||
instantWindow = (instantWindowTime / attackSpeed) * 100
|
||
end
|
||
|
||
CleveRoids.Print("|cff00ff00=== Window Calculations ===|r")
|
||
CleveRoids.Print("|cffffaa00Slam Window:|r " .. string.format("%.2f", slamWindow) .. "%")
|
||
CleveRoids.Print(" Formula: (SwingTimer - SlamCast) / SwingTimer")
|
||
CleveRoids.Print(" (" .. string.format("%.3f", attackSpeed) .. " - " .. string.format("%.3f", slamCastTime) .. ") / " .. string.format("%.3f", attackSpeed) .. " = " .. string.format("%.2f", slamWindow) .. "%")
|
||
CleveRoids.Print(" Max time to cast Slam: " .. string.format("%.3f", slamWindowTime) .. "s into swing")
|
||
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cffffaa00Instant Window:|r " .. string.format("%.2f", instantWindow) .. "%")
|
||
CleveRoids.Print(" Formula: (2×SwingTimer - SlamCast - GCD) / SwingTimer")
|
||
CleveRoids.Print(" (2×" .. string.format("%.3f", attackSpeed) .. " - " .. string.format("%.3f", slamCastTime) .. " - 1.5) / " .. string.format("%.3f", attackSpeed) .. " = " .. string.format("%.2f", instantWindow) .. "%")
|
||
CleveRoids.Print(" Max time to cast instant: " .. string.format("%.3f", instantWindowTime) .. "s into swing")
|
||
|
||
-- Show current status
|
||
if st_timer ~= nil then
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== Current Status ===|r")
|
||
local timeElapsed = attackSpeed - st_timer
|
||
local percentElapsed = (timeElapsed / attackSpeed) * 100
|
||
local inSlamWindow = percentElapsed <= slamWindow
|
||
local inInstantWindow = percentElapsed <= instantWindow
|
||
CleveRoids.Print("[noslamclip]: " .. (inSlamWindow and "|cff00ff00TRUE|r (safe to Slam)" or "|cffff0000FALSE|r (would clip)"))
|
||
CleveRoids.Print("[nonextslamclip]: " .. (inInstantWindow and "|cff00ff00TRUE|r (safe to instant)" or "|cffff0000FALSE|r (would clip next Slam)"))
|
||
end
|
||
|
||
return
|
||
end
|
||
|
||
-- formdebug (debug shapeshift form detection)
|
||
if cmd == "formdebug" or cmd == "form" or cmd == "shapeshiftdebug" then
|
||
CleveRoids.Print("|cff88ff88=== Shapeshift Form Debug ===|r")
|
||
CleveRoids.Print("Player class: " .. tostring(CleveRoids.playerClass))
|
||
|
||
local numForms = GetNumShapeshiftForms()
|
||
CleveRoids.Print("Number of forms: " .. tostring(numForms))
|
||
|
||
local currentIndex = CleveRoids.GetCurrentShapeshiftIndex()
|
||
CleveRoids.Print("Current form index: " .. tostring(currentIndex))
|
||
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== Form Details ===|r")
|
||
for i = 1, numForms do
|
||
local icon, name, isActive, isCastable = GetShapeshiftFormInfo(i)
|
||
local activeStr = isActive and "|cff00ff00ACTIVE|r" or "|cff888888inactive|r"
|
||
local castableStr = isCastable and "castable" or "not castable"
|
||
CleveRoids.Print(string.format("Form %d: %s - %s (%s)", i, tostring(name), activeStr, castableStr))
|
||
end
|
||
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== ValidatePlayerBuff Tests ===|r")
|
||
local testForms = {"Cat Form", "Bear Form", "Dire Bear Form", "Travel Form", "Aquatic Form", "Moonkin Form"}
|
||
for _, formName in ipairs(testForms) do
|
||
local result = CleveRoids.ValidatePlayerBuff(formName)
|
||
local resultStr = result and "|cff00ff00true|r" or "|cffff0000false|r"
|
||
CleveRoids.Print(" ValidatePlayerBuff('" .. formName .. "') = " .. resultStr)
|
||
end
|
||
return
|
||
end
|
||
|
||
-- debuffdebug (debug debuff tracking on target)
|
||
if cmd == "debuffdebug" or cmd == "debuff" or cmd == "trackdebug" then
|
||
local searchName = val
|
||
if val2 and val2 ~= "" then
|
||
searchName = val .. " " .. val2
|
||
end
|
||
-- Strip underscores and quotes
|
||
if searchName and searchName ~= "" then
|
||
searchName = string.gsub(searchName, "_", " ")
|
||
searchName = string.gsub(searchName, '"', "")
|
||
end
|
||
|
||
CleveRoids.Print("|cff88ff88=== Debuff Tracking Debug ===|r")
|
||
|
||
local _, guid = UnitExists("target")
|
||
if not guid then
|
||
CleveRoids.Print("|cffff0000No target selected!|r")
|
||
return
|
||
end
|
||
|
||
local targetName = UnitName("target") or "Unknown"
|
||
guid = CleveRoids.NormalizeGUID(guid)
|
||
CleveRoids.Print("Target: " .. targetName .. " (GUID: " .. tostring(guid) .. ")")
|
||
|
||
-- Show tracking table for this target
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== Tracked Debuffs (libdebuff.objects) ===|r")
|
||
local lib = CleveRoids.libdebuff
|
||
if lib and lib.objects and lib.objects[guid] then
|
||
local count = 0
|
||
for spellID, rec in pairs(lib.objects[guid]) do
|
||
if rec and rec.start and rec.duration then
|
||
local timeRemaining = rec.duration + rec.start - GetTime()
|
||
local spellName = SpellInfo and SpellInfo(spellID) or "Unknown"
|
||
local caster = rec.caster or "unknown"
|
||
local stacks = rec.stacks or 0
|
||
if timeRemaining > 0 then
|
||
CleveRoids.Print(string.format(" |cff00ff00[%d]|r %s: %.1fs left (caster: %s, stacks: %d)",
|
||
spellID, spellName, timeRemaining, caster, stacks))
|
||
count = count + 1
|
||
else
|
||
CleveRoids.Print(string.format(" |cffff0000[%d]|r %s: EXPIRED %.1fs ago (caster: %s)",
|
||
spellID, spellName, -timeRemaining, caster))
|
||
end
|
||
end
|
||
end
|
||
if count == 0 then
|
||
CleveRoids.Print(" (no active tracked debuffs)")
|
||
end
|
||
else
|
||
CleveRoids.Print(" (no tracking data for this target)")
|
||
end
|
||
|
||
-- Show actual debuff slots (1-16 via UnitDebuff, 17-48 via overflow)
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== Debuff Slots (UnitDebuff 1-16) ===|r")
|
||
local debuffCount = 0
|
||
for i = 1, 16 do
|
||
local texture, stacks, debuffType, spellID = UnitDebuff("target", i)
|
||
if texture then
|
||
local spellName = SpellInfo and SpellInfo(spellID) or "slot" .. i
|
||
CleveRoids.Print(string.format(" Slot %d: [%d] %s (stacks: %d)",
|
||
i, spellID or 0, spellName, stacks or 0))
|
||
debuffCount = debuffCount + 1
|
||
end
|
||
end
|
||
if debuffCount == 0 then
|
||
CleveRoids.Print(" (no debuffs in slots 1-16)")
|
||
end
|
||
|
||
-- Show overflow debuffs in buff slots (17-48)
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== Overflow Debuffs (UnitBuff 1-32 as debuffs 17-48) ===|r")
|
||
local overflowCount = 0
|
||
for i = 1, 32 do
|
||
local texture, stacks, spellID = UnitBuff("target", i)
|
||
if texture and spellID then
|
||
-- Check if this might be an overflow debuff by checking libdebuff durations
|
||
local isDebuff = lib and lib.durations and lib.durations[spellID]
|
||
if isDebuff then
|
||
local spellName = SpellInfo and SpellInfo(spellID) or "slot" .. i
|
||
CleveRoids.Print(string.format(" Buff Slot %d (=Debuff %d): [%d] %s (stacks: %d) |cffff8800OVERFLOW|r",
|
||
i, i + 16, spellID, spellName, stacks or 0))
|
||
overflowCount = overflowCount + 1
|
||
end
|
||
end
|
||
end
|
||
if overflowCount == 0 then
|
||
CleveRoids.Print(" (no overflow debuffs detected)")
|
||
end
|
||
|
||
-- If a specific debuff name was provided, test the conditional
|
||
if searchName and searchName ~= "" then
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== Testing [debuff:\"" .. searchName .. "\"] ===|r")
|
||
|
||
-- Test ValidateUnitDebuff
|
||
local result = CleveRoids.ValidateUnitDebuff("target", { name = searchName })
|
||
CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "'}): " ..
|
||
(result and "|cff00ff00true|r" or "|cffff0000false|r"))
|
||
|
||
-- Test with time conditional
|
||
local resultTime = CleveRoids.ValidateUnitDebuff("target", { name = searchName, operator = "<", amount = 99999 })
|
||
CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "', operator='<', amount=99999}): " ..
|
||
(resultTime and "|cff00ff00true|r" or "|cffff0000false|r"))
|
||
|
||
-- Look for spell IDs matching this name
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("|cff00ff00=== Spell ID Lookup for \"" .. searchName .. "\" ===|r")
|
||
local foundIDs = {}
|
||
-- Check Spells table
|
||
if CleveRoids.Spells then
|
||
for id, name in pairs(CleveRoids.Spells) do
|
||
if type(name) == "string" and string.lower(name) == string.lower(searchName) then
|
||
table.insert(foundIDs, id)
|
||
end
|
||
end
|
||
end
|
||
-- Also check SpellInfo
|
||
if SpellInfo then
|
||
for id = 1, 30000 do
|
||
local name = SpellInfo(id)
|
||
if name and string.lower(name) == string.lower(searchName) then
|
||
local found = false
|
||
for _, existingID in ipairs(foundIDs) do
|
||
if existingID == id then found = true break end
|
||
end
|
||
if not found then
|
||
table.insert(foundIDs, id)
|
||
end
|
||
end
|
||
-- Stop early if we found some
|
||
if table.getn(foundIDs) > 10 then break end
|
||
end
|
||
end
|
||
|
||
if table.getn(foundIDs) > 0 then
|
||
for _, id in ipairs(foundIDs) do
|
||
local tracked = lib and lib.objects and lib.objects[guid] and lib.objects[guid][id]
|
||
local trackedStr = tracked and "|cff00ff00TRACKED|r" or "|cff888888not tracked|r"
|
||
CleveRoids.Print(" SpellID " .. id .. ": " .. trackedStr)
|
||
if tracked then
|
||
local remaining = tracked.duration + tracked.start - GetTime()
|
||
CleveRoids.Print(" -> " .. string.format("%.1fs remaining (caster: %s)", remaining, tracked.caster or "?"))
|
||
end
|
||
end
|
||
else
|
||
CleveRoids.Print(" (no spell IDs found for this name)")
|
||
end
|
||
end
|
||
|
||
return
|
||
end
|
||
|
||
-- tooltipdebug (debug spell tooltip scanning)
|
||
if cmd == "tooltipdebug" or cmd == "ttdebug" or cmd == "spelltooltip" then
|
||
local spellName = val
|
||
if val2 and val2 ~= "" then
|
||
spellName = val .. " " .. val2
|
||
end
|
||
if not spellName or spellName == "" then
|
||
spellName = "Arcane Missiles" -- Default to Arcane Missiles
|
||
end
|
||
-- Strip underscores
|
||
spellName = string.gsub(spellName, "_", " ")
|
||
|
||
CleveRoids.Print("|cff88ff88=== Tooltip Scan Debug: " .. spellName .. " ===|r")
|
||
|
||
-- Find highest rank spell in spellbook (same logic as GetSpellSlotByName)
|
||
local slot, bookType = nil, nil
|
||
local foundRank = nil
|
||
local i = 1
|
||
while true do
|
||
local name, rank = GetSpellName(i, BOOKTYPE_SPELL)
|
||
if not name then break end
|
||
if name == spellName then
|
||
-- Keep updating to find the last (highest) rank
|
||
slot = i
|
||
bookType = BOOKTYPE_SPELL
|
||
foundRank = rank
|
||
end
|
||
i = i + 1
|
||
end
|
||
|
||
if not slot then
|
||
CleveRoids.Print("|cffff0000Spell not found in spellbook!|r")
|
||
return
|
||
end
|
||
|
||
CleveRoids.Print("Found in spellbook slot: " .. slot .. " (rank: " .. (foundRank or "none") .. ")")
|
||
|
||
-- Create tooltip if needed
|
||
if not CleveRoidsDebugTooltip then
|
||
CreateFrame("GameTooltip", "CleveRoidsDebugTooltip", nil, "GameTooltipTemplate")
|
||
CleveRoidsDebugTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
|
||
end
|
||
|
||
CleveRoidsDebugTooltip:ClearLines()
|
||
CleveRoidsDebugTooltip:SetSpell(slot, bookType)
|
||
|
||
CleveRoids.Print("Tooltip lines (" .. CleveRoidsDebugTooltip:NumLines() .. "):")
|
||
for lineNum = 1, CleveRoidsDebugTooltip:NumLines() do
|
||
local leftText = getglobal("CleveRoidsDebugTooltipTextLeft" .. lineNum)
|
||
local rightText = getglobal("CleveRoidsDebugTooltipTextRight" .. lineNum)
|
||
|
||
local leftStr = leftText and leftText:GetText() or ""
|
||
local rightStr = rightText and rightText:GetText() or ""
|
||
|
||
if leftStr and leftStr ~= "" then
|
||
CleveRoids.Print(" L" .. lineNum .. ": " .. leftStr)
|
||
-- Check for duration pattern
|
||
local duration = string.match(leftStr, "for (%d+%.?%d*) sec")
|
||
if duration then
|
||
CleveRoids.Print(" |cff00ff00^ Found 'for X sec': " .. duration .. "s|r")
|
||
end
|
||
local duration2 = string.match(leftStr, "over (%d+%.?%d*) sec")
|
||
if duration2 then
|
||
CleveRoids.Print(" |cff00ff00^ Found 'over X sec': " .. duration2 .. "s|r")
|
||
end
|
||
end
|
||
if rightStr and rightStr ~= "" then
|
||
CleveRoids.Print(" R" .. lineNum .. ": " .. rightStr)
|
||
end
|
||
end
|
||
|
||
-- Show what GetSpellDurationFromTooltip returns
|
||
local cachedDuration = CleveRoids.GetSpellDurationFromTooltip and CleveRoids.GetSpellDurationFromTooltip(spellName)
|
||
CleveRoids.Print(" ")
|
||
CleveRoids.Print("GetSpellDurationFromTooltip result: " .. (cachedDuration and (cachedDuration .. "s") or "|cffff0000nil|r"))
|
||
|
||
return
|
||
end
|
||
|
||
-- Unknown command fallback
|
||
CleveRoids.Print("Usage:")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates (Default: 0. 1 = on, increases CPU load)")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1 to 10 updates per second. Default: 5)")
|
||
if CleveRoids.hasSuperwow then
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
|
||
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages")
|
||
end
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Immunity Tracking:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listimmune [school] - List immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "<NPC>" <school> [buff] - Add immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "<NPC>" <school> - Remove immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00CC Immunity Tracking:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listccimmune [type] - List CC immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addccimmune "<NPC>" <type> [buff] - Add CC immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeccimmune "<NPC>" <type> - Remove CC immunity')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearccimmune [type] - Clear CC immunity data')
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r")
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data')
|
||
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)')
|
||
end
|
||
|
||
SLASH_CLEAREQUIPQUEUE1 = "/clearequipqueue"
|
||
SlashCmdList.CLEAREQUIPQUEUE = function()
|
||
-- Release all entries back to pool
|
||
for i = 1, CleveRoids.equipmentQueueLen do
|
||
local entry = CleveRoids.equipmentQueue[i]
|
||
if entry then
|
||
CleveRoids.equipmentQueue[i] = nil
|
||
-- Return to pool if space available
|
||
if table.getn(CleveRoids.queueEntryPool) < 10 then
|
||
entry.item = nil
|
||
entry.slotName = nil
|
||
entry.inventoryId = nil
|
||
table.insert(CleveRoids.queueEntryPool, entry)
|
||
end
|
||
end
|
||
end
|
||
CleveRoids.equipmentQueueLen = 0
|
||
if CleveRoids.equipQueueFrame then
|
||
CleveRoids.equipQueueFrame:Hide()
|
||
end
|
||
CleveRoids.Print("Equipment queue cleared")
|
||
end
|
||
|
||
SLASH_EQUIPQUEUESTATUS1 = "/equipqueuestatus"
|
||
SlashCmdList.EQUIPQUEUESTATUS = function()
|
||
local count = CleveRoids.equipmentQueueLen
|
||
CleveRoids.Print("Equipment queue has " .. count .. " pending items")
|
||
|
||
for i = 1, count do
|
||
local entry = CleveRoids.equipmentQueue[i]
|
||
if entry then
|
||
local itemName = (entry.item and entry.item.name) or "Unknown"
|
||
local slotName = entry.slotName or "Unknown"
|
||
local retries = entry.retries or 0
|
||
CleveRoids.Print(i .. ". " .. itemName .. " -> " .. slotName .. " (retries: " .. retries .. ")")
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Apply temporary weapon enchants (poisons, oils, sharpening stones, etc.)
|
||
-- Usage: /applymain [conditionals] ItemName
|
||
-- Usage: /applyoff [conditionals] ItemName
|
||
-- Example: /applymain [nomhimbue] Instant Poison
|
||
-- Example: /applyoff [combat] Crippling Poison
|
||
|
||
local function FindItemInBags(itemName)
|
||
-- Require a non-empty item name to prevent matching everything
|
||
if not itemName or itemName == "" then
|
||
return nil
|
||
end
|
||
|
||
local searchName = string.lower(itemName)
|
||
for bag = 0, 4 do
|
||
for slot = 1, GetContainerNumSlots(bag) do
|
||
local link = GetContainerItemLink(bag, slot)
|
||
if link then
|
||
local _, _, foundName = string.find(link, "%[(.+)%]")
|
||
if foundName and string.find(string.lower(foundName), searchName, 1, true) then
|
||
return bag, slot, foundName
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
function CleveRoids.DoApply(hand, msg)
|
||
local weaponSlots = {
|
||
["main"] = 16,
|
||
["off"] = 17,
|
||
}
|
||
|
||
local slot = weaponSlots[hand]
|
||
if not slot then
|
||
CleveRoids.Print("Invalid hand: " .. tostring(hand))
|
||
return false
|
||
end
|
||
|
||
local action = function(itemName)
|
||
local bag, bagSlot, foundName = FindItemInBags(itemName)
|
||
if not bag then
|
||
CleveRoids.Print("Item not found: " .. itemName)
|
||
return false
|
||
end
|
||
|
||
-- Apply the item to the weapon
|
||
UseContainerItem(bag, bagSlot)
|
||
PickupInventoryItem(slot)
|
||
ReplaceEnchant()
|
||
ClearCursor()
|
||
return true
|
||
end
|
||
|
||
local handled = false
|
||
for _, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
|
||
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then
|
||
handled = true
|
||
break
|
||
end
|
||
end
|
||
return handled
|
||
end
|
||
|
||
SLASH_APPLYMAIN1 = "/applymain"
|
||
SlashCmdList.APPLYMAIN = function(msg)
|
||
-- Require an item name argument
|
||
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
|
||
CleveRoids.Print("Usage: /applymain [conditionals] ItemName")
|
||
return
|
||
end
|
||
CleveRoids.DoApply("main", msg)
|
||
end
|
||
|
||
SLASH_APPLYOFF1 = "/applyoff"
|
||
SlashCmdList.APPLYOFF = function(msg)
|
||
-- Require an item name argument
|
||
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
|
||
CleveRoids.Print("Usage: /applyoff [conditionals] ItemName")
|
||
return
|
||
end
|
||
CleveRoids.DoApply("off", msg)
|
||
end
|