diff --git a/ComboPointTracker.lua b/ComboPointTracker.lua index 636bcc0..a670a0a 100644 --- a/ComboPointTracker.lua +++ b/ComboPointTracker.lua @@ -574,7 +574,25 @@ function CleveRoids.ShowComboTracking() end end --- Combo commands now consolidated under /cleveroid combotrack, comboclear, combodebug +-- Slash command for debugging +SLASH_COMBOTRACK1 = "/combotrack" +SlashCmdList.COMBOTRACK = function(msg) + if msg == "show" then + CleveRoids.ShowComboTracking() + elseif msg == "clear" then + CleveRoids.ComboPointTracking = {} + CleveRoids.ComboPointTracking.byID = {} + CleveRoids.Print("Combo tracking data cleared") + elseif msg == "debug" then + CleveRoids.debug = not CleveRoids.debug + CleveRoids.Print("Combo tracking debug: " .. (CleveRoids.debug and "ON" or "OFF")) + else + CleveRoids.Print("ComboTrack commands:") + CleveRoids.Print(" /combotrack show - Display current tracking data") + CleveRoids.Print(" /combotrack clear - Clear tracking data") + CleveRoids.Print(" /combotrack debug - Toggle debug output") + end +end -- Export to global namespace NOW, before Extension registration _G["CleveRoids"] = CleveRoids diff --git a/Conditionals.lua b/Conditionals.lua index c11c358..a397732 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -4815,31 +4815,6 @@ CleveRoids.MULTISCAN_PRIORITIES = { triangle = 4, diamond = 3, circle = 2, star = 1, } --- Multiscan options (Cursive-inspired filters that can combine with any priority) --- Usage: [multiscan:priority,option1,option2,minhp=1000,refreshtime=3,name="Mob Name"] --- Options: --- allowooc - Allow out of combat targets (default: only in-combat) --- priotarget - Always prioritize current target first --- ignoretarget - Ignore current target when scanning --- playeronly - Only target players, ignore NPCs --- npconly - Only target NPCs, ignore players --- tracked - Only consider targets in MultiTargetTracker --- minhp=X - Minimum HP threshold (raw value) --- maxhp=X - Maximum HP threshold (raw value) --- minhppct=X - Minimum HP percentage (0-100) --- maxhppct=X - Maximum HP percentage (0-100) --- refreshtime=X - Only target if debuff has less than X seconds remaining (for DoT refresh) --- name=X - Filter by name (partial match, use quotes for spaces: name="Mob Name") --- noname=X - Exclude targets with name containing X -CleveRoids.MULTISCAN_OPTIONS = { - allowooc = true, - priotarget = true, - ignoretarget = true, - playeronly = true, - npconly = true, - tracked = true, -} - -- Static conditionals that don't depend on target (checked before scanning) CleveRoids.STATIC_CONDITIONALS = { group = true, nogroup = true, @@ -4866,30 +4841,19 @@ CleveRoids.STATIC_CONDITIONALS = { --- @param priority string Priority type (e.g., "nearest", "highesthp") --- @param currentTargetGuid string|nil GUID of player's current target (exempt from combat check) --- @param specifiedUnitGuid string|nil GUID of @unit specified in macro (also exempt from combat check) ---- @param options table|nil Parsed multiscan options ---- @param conditionals table|nil Full conditionals for options checking --- @return number|nil Score (lower = better) or nil if invalid candidate -function CleveRoids.GetMultiscanScore(unit, priority, currentTargetGuid, specifiedUnitGuid, options, conditionals) +function CleveRoids.GetMultiscanScore(unit, priority, currentTargetGuid, specifiedUnitGuid) if not unit or not UnitExists(unit) then return nil end if UnitIsDeadOrGhost(unit) then return nil end -- Must be attackable if not UnitCanAttack("player", unit) then return nil end + -- Combat check: must be in combat with player, UNLESS it's current target OR specified @unit local _, unitGuid = UnitExists(unit) local isCurrentTarget = currentTargetGuid and unitGuid == currentTargetGuid local isSpecifiedUnit = specifiedUnitGuid and unitGuid == specifiedUnitGuid - - -- Combat check: must be in combat with player, UNLESS: - -- - it's current target OR specified @unit - -- - OR allowooc option is set - local allowOOC = options and options.allowooc - if not allowOOC and not isCurrentTarget and not isSpecifiedUnit and not UnitAffectingCombat(unit) then - return nil - end - - -- Check additional options filters - if options and not CleveRoids.CheckMultiscanOptions(unit, unitGuid, options, conditionals, currentTargetGuid) then + if not isCurrentTarget and not isSpecifiedUnit and not UnitAffectingCombat(unit) then return nil end @@ -4971,212 +4935,6 @@ function CleveRoids.CheckStaticConditionals(conditionals) return true end ---- Parse multiscan options from the conditional value ---- @param multiscanValue string|table The raw multiscan conditional value ---- @return string priority, table options -function CleveRoids.ParseMultiscanOptions(multiscanValue) - local options = { - allowooc = false, - priotarget = false, - ignoretarget = false, - playeronly = false, - npconly = false, - tracked = false, - minhp = nil, - maxhp = nil, - minhppct = nil, - maxhppct = nil, - refreshtime = nil, - name = nil, - noname = nil, - } - - local priority = nil - local values = {} - - -- Collect all values - if type(multiscanValue) == "table" then - for i = 1, table.getn(multiscanValue) do - table.insert(values, multiscanValue[i]) - end - elseif type(multiscanValue) == "string" then - -- Single value, might contain commas? No, conditionals system splits by / - -- The value is just the priority in this case - table.insert(values, multiscanValue) - end - - -- First value is always the priority - if table.getn(values) > 0 then - priority = string.lower(CleveRoids.Trim(values[1])) - end - - -- Remaining values are options - for i = 2, table.getn(values) do - local opt = CleveRoids.Trim(values[i]) - local optLower = string.lower(opt) - - -- Check for key=value options - local eqStart, eqEnd = string.find(optLower, "=") - if eqStart then - local key = string.sub(optLower, 1, eqStart - 1) - local value = string.sub(opt, eqEnd + 1) -- Keep original case for name - - -- Remove quotes from value if present - if string.sub(value, 1, 1) == '"' and string.sub(value, -1) == '"' then - value = string.sub(value, 2, -2) - end - - if key == "minhp" then - options.minhp = tonumber(value) - elseif key == "maxhp" then - options.maxhp = tonumber(value) - elseif key == "minhppct" then - options.minhppct = tonumber(value) - elseif key == "maxhppct" then - options.maxhppct = tonumber(value) - elseif key == "refreshtime" then - options.refreshtime = tonumber(value) - elseif key == "name" then - options.name = value - elseif key == "noname" then - options.noname = value - end - else - -- Boolean flag options - if optLower == "allowooc" then - options.allowooc = true - elseif optLower == "priotarget" then - options.priotarget = true - elseif optLower == "ignoretarget" then - options.ignoretarget = true - elseif optLower == "playeronly" then - options.playeronly = true - elseif optLower == "npconly" then - options.npconly = true - elseif optLower == "tracked" then - options.tracked = true - end - end - end - - return priority, options -end - ---- Check if a unit passes multiscan options filters ---- @param unit string Unit token ---- @param guid string Unit GUID ---- @param options table Parsed options ---- @param conditionals table Full conditionals table (for refreshtime spell check) ---- @param currentTargetGuid string|nil Current target GUID ---- @return boolean passes -function CleveRoids.CheckMultiscanOptions(unit, guid, options, conditionals, currentTargetGuid) - -- ignoretarget: Skip current target - if options.ignoretarget and currentTargetGuid and guid == currentTargetGuid then - return false - end - - -- playeronly: Only players - if options.playeronly and not UnitIsPlayer(unit) then - return false - end - - -- npconly: Only NPCs - if options.npconly and UnitIsPlayer(unit) then - return false - end - - -- tracked: Only tracked targets - if options.tracked then - local tracker = CleveRoids.MultiTargetTracker - if tracker and tracker.IsTracked then - if not tracker.IsTracked(guid) then - return false - end - else - return false -- Tracker not available - end - end - - -- minhp: Minimum raw HP - if options.minhp then - local hp = UnitHealth(unit) - if hp < options.minhp then - return false - end - end - - -- maxhp: Maximum raw HP - if options.maxhp then - local hp = UnitHealth(unit) - if hp > options.maxhp then - return false - end - end - - -- minhppct: Minimum HP percentage - if options.minhppct then - local maxHp = UnitHealthMax(unit) - if maxHp > 0 then - local pct = (UnitHealth(unit) / maxHp) * 100 - if pct < options.minhppct then - return false - end - end - end - - -- maxhppct: Maximum HP percentage - if options.maxhppct then - local maxHp = UnitHealthMax(unit) - if maxHp > 0 then - local pct = (UnitHealth(unit) / maxHp) * 100 - if pct > options.maxhppct then - return false - end - end - end - - -- name: Filter by name (partial match) - if options.name then - local unitName = UnitName(unit) or "" - if not string.find(string.lower(unitName), string.lower(options.name)) then - return false - end - end - - -- noname: Exclude by name (partial match) - if options.noname then - local unitName = UnitName(unit) or "" - if string.find(string.lower(unitName), string.lower(options.noname)) then - return false - end - end - - -- refreshtime: Only target if debuff expires within X seconds - -- Uses the spell from conditionals.action (the spell being cast) - if options.refreshtime then - local tracker = CleveRoids.MultiTargetTracker - if tracker and tracker.GetActiveDebuffs then - local debuffs, count = tracker.GetActiveDebuffs(guid) - if debuffs and count > 0 then - -- Check if any debuff has more than refreshtime remaining - local hasLongDebuff = false - for i = 1, count do - if debuffs[i].remaining > options.refreshtime then - hasLongDebuff = true - break - end - end - if hasLongDebuff then - return false -- Has debuff with too much time remaining - end - end - -- No debuffs or all debuffs are expiring soon - pass - end - end - - return true -end - --- Main multiscan resolution function --- Scans enemies using UnitXP, finds best target matching priority and conditionals --- @param conditionals table The conditionals table containing multiscan value @@ -5188,10 +4946,16 @@ function CleveRoids.ResolveMultiscanTarget(conditionals, specifiedUnit) return nil end - -- Parse priority and options from conditionals.multiscan - local priority, options = CleveRoids.ParseMultiscanOptions(conditionals.multiscan) + -- Parse priority from conditionals.multiscan + local priority = nil + if type(conditionals.multiscan) == "table" then + priority = conditionals.multiscan[1] + elseif type(conditionals.multiscan) == "string" then + priority = conditionals.multiscan + end if not priority then return nil end + priority = string.lower(CleveRoids.Trim(priority)) -- Validate priority type local priorityType = CleveRoids.MULTISCAN_PRIORITIES[priority] @@ -5331,7 +5095,7 @@ function CleveRoids.ResolveMultiscanTarget(conditionals, specifiedUnit) end end - local score = CleveRoids.GetMultiscanScore(unit, priorityType, currentTargetGuid, specifiedUnitGuid, options, conditionals) + local score = CleveRoids.GetMultiscanScore(unit, priorityType, currentTargetGuid, specifiedUnitGuid) if not score then return end -- Validate against target-dependent conditionals @@ -5340,18 +5104,8 @@ function CleveRoids.ResolveMultiscanTarget(conditionals, specifiedUnit) end end - -- Handle priotarget option: always check current target first - if options.priotarget and currentTargetGuid then - evaluateCandidate("target") - -- If current target is valid, return it immediately - if table.getn(candidates) > 0 then - return candidates[1].guid - end - end - -- Always consider current target (exempt from combat check via GetMultiscanScore) - -- Skip if priotarget already checked it - if currentTargetGuid and not options.priotarget then + if currentTargetGuid then evaluateCandidate("target") end diff --git a/Console.lua b/Console.lua index b5c0ff9..ea6e8c6 100644 --- a/Console.lua +++ b/Console.lua @@ -51,18 +51,6 @@ SlashCmdList.EQUIPMH = CleveRoids.DoEquipMainhand SLASH_EQUIPOH1 = "/equipoh" SlashCmdList.EQUIPOH = CleveRoids.DoEquipOffhand -SLASH_EQSLOT111 = "/equip11" -SlashCmdList.EQSLOT11 = CleveRoids.DoEquipRing1 - -SLASH_EQSLOT121 = "/equip12" -SlashCmdList.EQSLOT12 = CleveRoids.DoEquipRing2 - -SLASH_EQSLOT131 = "/equip13" -SlashCmdList.EQSLOT13 = CleveRoids.DoEquipTrinket1 - -SLASH_EQSLOT141 = "/equip14" -SlashCmdList.EQSLOT14 = CleveRoids.DoEquipTrinket2 - SLASH_UNSHIFT1 = "/unshift" SlashCmdList.UNSHIFT = CleveRoids.DoUnshift diff --git a/Core.lua b/Core.lua index ed27efa..dfe41dd 100644 --- a/Core.lua +++ b/Core.lua @@ -2794,9 +2794,9 @@ function CleveRoids.DoUse(msg) return false end -function CleveRoids.EquipBagItem(msg, slotOrOffhand) +function CleveRoids.EquipBagItem(msg, offhand) if CleveRoids.equipDebugLog then - CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' slot=" .. tostring(slotOrOffhand) .. "|r") + CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' offhand=" .. tostring(offhand) .. "|r") end if CleveRoids.equipInProgress then @@ -2806,13 +2806,7 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) 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 invslot = offhand and 17 or 16 local API = CleveRoids.NampowerAPI -- v2.18+: Use native fast lookup for item ID or name @@ -2925,38 +2919,7 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) 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) + -- PERFORMANCE: Try EquipItemByName FIRST before any lookups -- This is the fastest path - no item lookup, no cursor operations if EquipItemByName then local ok = pcall(EquipItemByName, msg, invslot) @@ -3056,14 +3019,6 @@ 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 _unshiftAction() local currentShapeshiftIndex = CleveRoids.GetCurrentShapeshiftIndex() if currentShapeshiftIndex ~= 0 then @@ -3095,118 +3050,6 @@ function CleveRoids.DoEquipOffhand(msg) return false end --- Trinket equip using slot 13 (which works), then swap if needed for slot 14 -local function DoEquipTrinketSlot13(msg) - local action = function(m) return CleveRoids.EquipBagItem(m, 13) end - local parts = CleveRoids.splitStringIgnoringQuotes(msg) - for i = 1, table.getn(parts) do - local v = string.gsub(parts[i], "^%?", "") - if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then - return true - end - end - return false -end - --- For slot 14: normal API for bag items, manual for already-equipped -local function DoEquipTrinketSlot14(msg) - local action = function(m) - -- Find item location - local item = CleveRoids.GetItem(m) - if not item then - return false - end - - -- Item in bags: use normal EquipBagItem - if item.bagID and item.slot then - return CleveRoids.EquipBagItem(m, 14) - end - - -- Item already equipped: manual pickup and swap - if item.inventoryID then - ClearCursor() - PickupInventoryItem(item.inventoryID) - if CursorHasItem and CursorHasItem() then - EquipCursorItem(14) - -- If displaced item on cursor, put it in original slot - if CursorHasItem and CursorHasItem() then - EquipCursorItem(item.inventoryID) - end - ClearCursor() - return true - end - end - - return false - end - local parts = CleveRoids.splitStringIgnoringQuotes(msg) - for i = 1, table.getn(parts) do - local v = string.gsub(parts[i], "^%?", "") - if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then - return true - end - end - return false -end - -CleveRoids.DoEquipTrinket1 = function(msg) return DoEquipTrinketSlot13(msg) end -CleveRoids.DoEquipTrinket2 = function(msg) return DoEquipTrinketSlot14(msg) end - --- Ring slot 11: normal equip -local function DoEquipRingSlot11(msg) - local action = function(m) return CleveRoids.EquipBagItem(m, 11) end - local parts = CleveRoids.splitStringIgnoringQuotes(msg) - for i = 1, table.getn(parts) do - local v = string.gsub(parts[i], "^%?", "") - if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then - return true - end - end - return false -end - --- Ring slot 12: normal API for bag items, manual for already-equipped -local function DoEquipRingSlot12(msg) - local action = function(m) - local item = CleveRoids.GetItem(m) - if not item then - return false - end - - -- Item in bags: use normal EquipBagItem - if item.bagID and item.slot then - return CleveRoids.EquipBagItem(m, 12) - end - - -- Item already equipped: manual pickup and swap - if item.inventoryID then - ClearCursor() - PickupInventoryItem(item.inventoryID) - if CursorHasItem and CursorHasItem() then - EquipCursorItem(12) - if CursorHasItem and CursorHasItem() then - EquipCursorItem(item.inventoryID) - end - ClearCursor() - return true - end - end - - return false - end - local parts = CleveRoids.splitStringIgnoringQuotes(msg) - for i = 1, table.getn(parts) do - local v = string.gsub(parts[i], "^%?", "") - if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then - return true - end - end - return false -end - -CleveRoids.DoEquipRing1 = function(msg) return DoEquipRingSlot11(msg) end -CleveRoids.DoEquipRing2 = function(msg) return DoEquipRingSlot12(msg) end - function CleveRoids.DoUnshift(msg) local handled -- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation @@ -4929,7 +4772,6 @@ SlashCmdList["CLEVEROID"] = function(msg) 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('/cleveroid combodebug - Toggle combo tracking debug output') 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 - List all talents in a tab with their IDs') @@ -4940,8 +4782,6 @@ SlashCmdList["CLEVEROID"] = function(msg) DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testequip - Test equipment modifier for a spell') DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equipdebug - Debug item lookup for /equip') DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equiplog - Toggle real-time equip command logging') - DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equipqueue clear - Clear pending equipment queue') - DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equipqueue status - Show queue status') 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 [duration] - Manually set proc (testing)') @@ -4957,8 +4797,6 @@ SlashCmdList["CLEVEROID"] = function(msg) 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') - DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Multi-Target Tracker:|r") - DEFAULT_CHAT_FRAME:AddMessage('/cleveroid mtt - Show Multi-Target Tracker commands') return end @@ -5618,13 +5456,6 @@ SlashCmdList["CLEVEROID"] = function(msg) return end - -- combodebug (toggle combo tracking debug) - if cmd == "combodebug" then - CleveRoids.debug = not CleveRoids.debug - CleveRoids.Print("Combo tracking debug: " .. (CleveRoids.debug and "ON" or "OFF")) - return - end - -- talenttabs (show talent tab IDs) if cmd == "talenttabs" or cmd == "tabs" then CleveRoids.Print("=== Talent Tabs ===") @@ -6383,96 +6214,6 @@ SlashCmdList["CLEVEROID"] = function(msg) return end - -- equipqueue (Equipment Queue management) - if cmd == "equipqueue" or cmd == "eq" then - if val == "clear" then - -- 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") - elseif val == "status" then - 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 - else - CleveRoids.Print("Equipment Queue commands:") - CleveRoids.Print(" /cleveroid equipqueue clear - Clear pending equipment queue") - CleveRoids.Print(" /cleveroid equipqueue status - Show queue status") - end - return - end - - -- mtt (Multi-Target Tracker) - if cmd == "mtt" then - local tracker = CleveRoids.MultiTargetTracker - if not tracker then - CleveRoids.Print("Multi-Target Tracker not loaded!") - return - end - - if val == "unlock" then - tracker.Unlock() - elseif val == "lock" then - tracker.Lock() - elseif val == "show" then - tracker.Show() - CleveRoids.Print("Multi-Target Tracker: Enabled (will show when tracking)") - elseif val == "hide" then - tracker.Hide() - CleveRoids.Print("Multi-Target Tracker: Disabled (persists across reloads)") - elseif val == "toggle" then - tracker.Toggle() - if tracker.IsDisabled and tracker.IsDisabled() then - CleveRoids.Print("Multi-Target Tracker: Disabled (persists across reloads)") - else - CleveRoids.Print("Multi-Target Tracker: Enabled (will show when tracking)") - end - elseif val == "clear" then - tracker.ClearTargets() - elseif val == "reset" then - tracker.ResetPosition() - elseif val == "debug" then - tracker.Debug() - elseif val == "dump" then - tracker.DumpState() - else - CleveRoids.Print("Multi-Target Tracker commands:") - CleveRoids.Print(" /cleveroid mtt unlock - Show frame for positioning") - CleveRoids.Print(" /cleveroid mtt lock - Lock and resume auto-hide") - CleveRoids.Print(" /cleveroid mtt show - Enable tracker (persists)") - CleveRoids.Print(" /cleveroid mtt hide - Disable tracker (persists)") - CleveRoids.Print(" /cleveroid mtt toggle - Toggle enabled state (persists)") - CleveRoids.Print(" /cleveroid mtt clear - Clear all tracked targets") - CleveRoids.Print(" /cleveroid mtt reset - Reset frame position") - CleveRoids.Print(" /cleveroid mtt debug - Toggle debug mode") - CleveRoids.Print(" /cleveroid mtt dump - Dump tracking state") - end - return - end - -- Unknown command fallback CleveRoids.Print("Usage:") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings") @@ -6497,12 +6238,43 @@ SlashCmdList["CLEVEROID"] = function(msg) 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('/cleveroid combodebug - Toggle combo tracking debug output') - DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Equipment Queue:|r") - DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equipqueue clear - Clear pending equipment queue') - DEFAULT_CHAT_FRAME:AddMessage('/cleveroid equipqueue status - Show queue status') - DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Multi-Target Tracker:|r") - DEFAULT_CHAT_FRAME:AddMessage('/cleveroid mtt - Show Multi-Target Tracker commands') end --- Equipment queue commands consolidated under /cleveroid equipqueue clear|status +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 diff --git a/Extensions/MultiTargetTracker.lua b/Extensions/MultiTargetTracker.lua deleted file mode 100644 index 68580a6..0000000 --- a/Extensions/MultiTargetTracker.lua +++ /dev/null @@ -1,1418 +0,0 @@ ---[[ - Multi-Target Debuff Tracker Extension (Cursive-style rewrite) - Tracks player's debuffs across multiple enemy targets - Based on Cursive addon architecture by Pepopo - - Features: - - Per-class spell tables (only tracks spells you can cast) - - UNIT_CASTEVENT based tracking with debuff scan verification - - Resist/immune detection via chat parsing - - Display priority: raid marks -> highest HP -> rest -]] - -local _G = _G or getfenv(0) -local CleveRoids = _G.CleveRoids or {} - -local Extension = CleveRoids.RegisterExtension("MultiTargetTracker") - --- ============================================================================ --- Configuration --- ============================================================================ - -local MAX_TRACKED_TARGETS = 15 -local MAX_DEBUFFS_PER_ROW = 8 -local UPDATE_INTERVAL = 0.1 -- 10 Hz - -local CONFIG = { - barHeight = 20, - barSpacing = 2, - targetIndicatorSize = 8, - raidIconSize = 16, - healthBarWidth = 140, - debuffIconSize = 20, - debuffIconSpacing = 2, - padding = 2, - titleHeight = 16, - textSize = 10, -} - -local BACKDROP_BORDER = { - edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", - tile = true, tileSize = 16, edgeSize = 8, - insets = { left = 2, right = 2, top = 2, bottom = 2 } -} - -local BACKDROP_BACKGROUND = { - bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", - tile = true, tileSize = 16, - insets = { left = 0, right = 0, top = 0, bottom = 0 } -} - -local COLORS = { - borderNormal = { 0.2, 0.2, 0.2, 1 }, - borderHover = { 1, 1, 1, 1 }, - borderCombat = { 0.8, 0.2, 0.2, 1 }, - healthDefault = { 1, 0.8, 0.2, 1 }, - timerNormal = { 1, 1, 1 }, - timerExpiring = { 1, 0.2, 0.2 }, - titleText = { 1, 0.82, 0, 1 }, -} - --- ============================================================================ --- Per-Class Spell Tables (Cursive format) --- ============================================================================ - -local playerClassName = nil -- Set in LoadClassSpells() - --- Combo point tracking for finisher duration calculation -local previousComboPoints = 0 -local currentComboPoints = 0 - -local function GetComboPointsUsed() - if currentComboPoints == 0 then - return previousComboPoints - end - return currentComboPoints -end - --- Duration calculators for combo point scaling -local function getRipDuration() - return 8 + GetComboPointsUsed() * 2 -end - -local function getRuptureDuration() - local duration = 6 + GetComboPointsUsed() * 2 - -- Check for Improved Rupture talent (Assassination tree, tier 10) - local _, _, _, _, count = GetTalentInfo(1, 10) - if count and count > 0 then - duration = duration + (count * 2) - end - return duration -end - -local function getKidneyShotDuration() - return 1 + GetComboPointsUsed() -end - -local function getGougeDuration() - -- Improved Gouge talent (Combat tree, tier 1) - local _, _, _, _, count = GetTalentInfo(2, 1) - if count and count > 0 then - return 4 + (count * 0.5) - end - return 4 -end - --- Class spell tables -local function getDruidSpells() - return { - -- Entangling Roots - [339] = { name = "entangling roots", rank = 1, duration = 12 }, - [1062] = { name = "entangling roots", rank = 2, duration = 15 }, - [5195] = { name = "entangling roots", rank = 3, duration = 18 }, - [5196] = { name = "entangling roots", rank = 4, duration = 21 }, - [9852] = { name = "entangling roots", rank = 5, duration = 24 }, - [9853] = { name = "entangling roots", rank = 6, duration = 27 }, - -- Hibernate - [2637] = { name = "hibernate", rank = 1, duration = 20 }, - [18657] = { name = "hibernate", rank = 2, duration = 30 }, - [18658] = { name = "hibernate", rank = 3, duration = 40 }, - -- Faerie Fire (all versions use same name for blocking) - [770] = { name = "faerie fire", rank = 1, duration = 40 }, - [778] = { name = "faerie fire", rank = 2, duration = 40 }, - [9749] = { name = "faerie fire", rank = 3, duration = 40 }, - [9907] = { name = "faerie fire", rank = 4, duration = 40 }, - [16855] = { name = "faerie fire", rank = 1, duration = 40 }, - [17387] = { name = "faerie fire", rank = 2, duration = 40 }, - [17388] = { name = "faerie fire", rank = 3, duration = 40 }, - [17389] = { name = "faerie fire", rank = 4, duration = 40 }, - [16857] = { name = "faerie fire", rank = 1, duration = 40 }, - [17390] = { name = "faerie fire", rank = 2, duration = 40 }, - [17391] = { name = "faerie fire", rank = 3, duration = 40 }, - [17392] = { name = "faerie fire", rank = 4, duration = 40 }, - -- Insect Swarm - [5570] = { name = "insect swarm", rank = 1, duration = 18 }, - [24974] = { name = "insect swarm", rank = 2, duration = 18 }, - [24975] = { name = "insect swarm", rank = 3, duration = 18 }, - [24976] = { name = "insect swarm", rank = 4, duration = 18 }, - [24977] = { name = "insect swarm", rank = 5, duration = 18 }, - -- Moonfire - [8921] = { name = "moonfire", rank = 1, duration = 9 }, - [8924] = { name = "moonfire", rank = 2, duration = 12 }, - [8925] = { name = "moonfire", rank = 3, duration = 12 }, - [8926] = { name = "moonfire", rank = 4, duration = 12 }, - [8927] = { name = "moonfire", rank = 5, duration = 12 }, - [8928] = { name = "moonfire", rank = 6, duration = 12 }, - [8929] = { name = "moonfire", rank = 7, duration = 12 }, - [9833] = { name = "moonfire", rank = 8, duration = 12 }, - [9834] = { name = "moonfire", rank = 9, duration = 12 }, - [9835] = { name = "moonfire", rank = 10, duration = 12 }, - -- Rake - [1822] = { name = "rake", rank = 1, duration = 9 }, - [1823] = { name = "rake", rank = 2, duration = 9 }, - [1824] = { name = "rake", rank = 3, duration = 9 }, - [9904] = { name = "rake", rank = 4, duration = 9 }, - -- Rip (combo point scaling) - [1079] = { name = "rip", rank = 1, duration = 8, calculateDuration = getRipDuration }, - [9492] = { name = "rip", rank = 2, duration = 8, calculateDuration = getRipDuration }, - [9493] = { name = "rip", rank = 3, duration = 8, calculateDuration = getRipDuration }, - [9752] = { name = "rip", rank = 4, duration = 8, calculateDuration = getRipDuration }, - [9894] = { name = "rip", rank = 5, duration = 8, calculateDuration = getRipDuration }, - [9896] = { name = "rip", rank = 6, duration = 8, calculateDuration = getRipDuration }, - -- Bash - [5211] = { name = "bash", rank = 1, duration = 2 }, - [6798] = { name = "bash", rank = 2, duration = 3 }, - [8983] = { name = "bash", rank = 3, duration = 4 }, - -- Demoralizing Roar - [99] = { name = "demoralizing roar", rank = 1, duration = 30 }, - [1735] = { name = "demoralizing roar", rank = 2, duration = 30 }, - [9490] = { name = "demoralizing roar", rank = 3, duration = 30 }, - [9747] = { name = "demoralizing roar", rank = 4, duration = 30 }, - [9898] = { name = "demoralizing roar", rank = 5, duration = 30 }, - -- Pounce Bleed - [9005] = { name = "pounce bleed", rank = 1, duration = 18 }, - [9823] = { name = "pounce bleed", rank = 2, duration = 18 }, - [9827] = { name = "pounce bleed", rank = 3, duration = 18 }, - } -end - -local function getRogueSpells() - return { - -- Blind - [2094] = { name = "blind", rank = 1, duration = 10 }, - [21060] = { name = "blind", rank = 1, duration = 10 }, - -- Sap - [6770] = { name = "sap", rank = 1, duration = 25 }, - [2070] = { name = "sap", rank = 2, duration = 35 }, - [11297] = { name = "sap", rank = 3, duration = 45 }, - -- Gouge (talent scaling) - [1776] = { name = "gouge", rank = 1, duration = 4, calculateDuration = getGougeDuration }, - [1777] = { name = "gouge", rank = 2, duration = 4, calculateDuration = getGougeDuration }, - [8629] = { name = "gouge", rank = 3, duration = 4, calculateDuration = getGougeDuration }, - [11285] = { name = "gouge", rank = 4, duration = 4, calculateDuration = getGougeDuration }, - [11286] = { name = "gouge", rank = 5, duration = 4, calculateDuration = getGougeDuration }, - -- Rupture (combo point + talent scaling) - [1943] = { name = "rupture", rank = 1, duration = 6, calculateDuration = getRuptureDuration }, - [8639] = { name = "rupture", rank = 2, duration = 6, calculateDuration = getRuptureDuration }, - [8640] = { name = "rupture", rank = 3, duration = 6, calculateDuration = getRuptureDuration }, - [11273] = { name = "rupture", rank = 4, duration = 6, calculateDuration = getRuptureDuration }, - [11274] = { name = "rupture", rank = 5, duration = 6, calculateDuration = getRuptureDuration }, - [11275] = { name = "rupture", rank = 6, duration = 6, calculateDuration = getRuptureDuration }, - -- Kidney Shot (combo point scaling) - [408] = { name = "kidney shot", rank = 1, duration = 1, calculateDuration = getKidneyShotDuration }, - [8643] = { name = "kidney shot", rank = 2, duration = 1, calculateDuration = getKidneyShotDuration }, - -- Expose Armor - [8647] = { name = "expose armor", rank = 1, duration = 30 }, - [8649] = { name = "expose armor", rank = 2, duration = 30 }, - [8650] = { name = "expose armor", rank = 3, duration = 30 }, - [11197] = { name = "expose armor", rank = 4, duration = 30 }, - [11198] = { name = "expose armor", rank = 5, duration = 30 }, - -- Garrote - [703] = { name = "garrote", rank = 1, duration = 18 }, - [8631] = { name = "garrote", rank = 2, duration = 18 }, - [8632] = { name = "garrote", rank = 3, duration = 18 }, - [8633] = { name = "garrote", rank = 4, duration = 18 }, - [11289] = { name = "garrote", rank = 5, duration = 18 }, - [11290] = { name = "garrote", rank = 6, duration = 18 }, - -- Deadly Poison - [2818] = { name = "deadly poison", rank = 1, duration = 12 }, - [2819] = { name = "deadly poison ii", rank = 2, duration = 12 }, - [11353] = { name = "deadly poison iii", rank = 3, duration = 12 }, - [11354] = { name = "deadly poison iv", rank = 4, duration = 12 }, - [25349] = { name = "deadly poison v", rank = 5, duration = 12 }, - -- Hemorrhage - [16511] = { name = "hemorrhage", rank = 1, duration = 15 }, - -- Cheap Shot - [1833] = { name = "cheap shot", rank = 1, duration = 4 }, - [14902] = { name = "cheap shot", rank = 1, duration = 4 }, - } -end - -local function getWarriorSpells() - return { - -- Rend - [772] = { name = "rend", rank = 1, duration = 9 }, - [6546] = { name = "rend", rank = 2, duration = 12 }, - [6547] = { name = "rend", rank = 3, duration = 15 }, - [6548] = { name = "rend", rank = 4, duration = 18 }, - [11572] = { name = "rend", rank = 5, duration = 21 }, - [11573] = { name = "rend", rank = 6, duration = 21 }, - [11574] = { name = "rend", rank = 7, duration = 21 }, - -- Hamstring - [1715] = { name = "hamstring", rank = 1, duration = 15 }, - [7372] = { name = "hamstring", rank = 2, duration = 15 }, - [7373] = { name = "hamstring", rank = 3, duration = 15 }, - -- Thunder Clap - [6343] = { name = "thunder clap", rank = 1, duration = 10 }, - [8198] = { name = "thunder clap", rank = 2, duration = 14 }, - [8205] = { name = "thunder clap", rank = 3, duration = 18 }, - [11580] = { name = "thunder clap", rank = 4, duration = 22 }, - [11581] = { name = "thunder clap", rank = 5, duration = 26 }, - -- Demoralizing Shout - [1160] = { name = "demoralizing shout", rank = 1, duration = 30 }, - [6190] = { name = "demoralizing shout", rank = 2, duration = 30 }, - [11554] = { name = "demoralizing shout", rank = 3, duration = 30 }, - [11555] = { name = "demoralizing shout", rank = 4, duration = 30 }, - [11556] = { name = "demoralizing shout", rank = 5, duration = 30 }, - -- Sunder Armor - [7386] = { name = "sunder armor", rank = 1, duration = 30 }, - [7405] = { name = "sunder armor", rank = 2, duration = 30 }, - [8380] = { name = "sunder armor", rank = 3, duration = 30 }, - [11596] = { name = "sunder armor", rank = 4, duration = 30 }, - [11597] = { name = "sunder armor", rank = 5, duration = 30 }, - } -end - -local function getWarlockSpells() - return { - -- Corruption - [172] = { name = "corruption", rank = 1, duration = 12 }, - [6222] = { name = "corruption", rank = 2, duration = 15 }, - [6223] = { name = "corruption", rank = 3, duration = 18 }, - [7648] = { name = "corruption", rank = 4, duration = 18 }, - [11671] = { name = "corruption", rank = 5, duration = 18 }, - [11672] = { name = "corruption", rank = 6, duration = 18 }, - [25311] = { name = "corruption", rank = 7, duration = 18 }, - -- Curse of Agony - [980] = { name = "curse of agony", rank = 1, duration = 24 }, - [1014] = { name = "curse of agony", rank = 2, duration = 24 }, - [6217] = { name = "curse of agony", rank = 3, duration = 24 }, - [11711] = { name = "curse of agony", rank = 4, duration = 24 }, - [11712] = { name = "curse of agony", rank = 5, duration = 24 }, - [11713] = { name = "curse of agony", rank = 6, duration = 24 }, - -- Siphon Life - [18265] = { name = "siphon life", rank = 1, duration = 30 }, - [18879] = { name = "siphon life", rank = 2, duration = 30 }, - [18880] = { name = "siphon life", rank = 3, duration = 30 }, - [18881] = { name = "siphon life", rank = 4, duration = 30 }, - -- Curse of Doom - [603] = { name = "curse of doom", rank = 1, duration = 60 }, - -- Curse of Recklessness - [704] = { name = "curse of recklessness", rank = 1, duration = 120 }, - [7658] = { name = "curse of recklessness", rank = 2, duration = 120 }, - [7659] = { name = "curse of recklessness", rank = 3, duration = 120 }, - [11717] = { name = "curse of recklessness", rank = 4, duration = 120 }, - -- Curse of Shadow - [17862] = { name = "curse of shadow", rank = 1, duration = 300 }, - [17937] = { name = "curse of shadow", rank = 2, duration = 300 }, - -- Curse of the Elements - [1490] = { name = "curse of the elements", rank = 1, duration = 300 }, - [11721] = { name = "curse of the elements", rank = 2, duration = 300 }, - [11722] = { name = "curse of the elements", rank = 3, duration = 300 }, - -- Curse of Tongues - [1714] = { name = "curse of tongues", rank = 1, duration = 30 }, - [11719] = { name = "curse of tongues", rank = 2, duration = 30 }, - -- Curse of Weakness - [702] = { name = "curse of weakness", rank = 1, duration = 120 }, - [1108] = { name = "curse of weakness", rank = 2, duration = 120 }, - [6205] = { name = "curse of weakness", rank = 3, duration = 120 }, - [7646] = { name = "curse of weakness", rank = 4, duration = 120 }, - [11707] = { name = "curse of weakness", rank = 5, duration = 120 }, - [11708] = { name = "curse of weakness", rank = 6, duration = 120 }, - -- Curse of Exhaustion - [18223] = { name = "curse of exhaustion", rank = 1, duration = 12 }, - -- Immolate - [348] = { name = "immolate", rank = 1, duration = 15 }, - [707] = { name = "immolate", rank = 2, duration = 15 }, - [1094] = { name = "immolate", rank = 3, duration = 15 }, - [2941] = { name = "immolate", rank = 4, duration = 15 }, - [11665] = { name = "immolate", rank = 5, duration = 15 }, - [11667] = { name = "immolate", rank = 6, duration = 15 }, - [11668] = { name = "immolate", rank = 7, duration = 15 }, - [25309] = { name = "immolate", rank = 8, duration = 15 }, - -- Death Coil - [6789] = { name = "death coil", rank = 1, duration = 3 }, - [17925] = { name = "death coil", rank = 2, duration = 3 }, - [17926] = { name = "death coil", rank = 3, duration = 3 }, - -- Banish - [710] = { name = "banish", rank = 1, duration = 20 }, - [18647] = { name = "banish", rank = 2, duration = 30 }, - -- Fear - [5782] = { name = "fear", rank = 1, duration = 10 }, - [6213] = { name = "fear", rank = 2, duration = 15 }, - [6215] = { name = "fear", rank = 3, duration = 20 }, - } -end - -local function getHunterSpells() - return { - -- Scorpid Sting - [3043] = { name = "scorpid sting", rank = 1, duration = 20 }, - [14275] = { name = "scorpid sting", rank = 2, duration = 20 }, - [14276] = { name = "scorpid sting", rank = 3, duration = 20 }, - [14277] = { name = "scorpid sting", rank = 4, duration = 20 }, - -- Serpent Sting - [1978] = { name = "serpent sting", rank = 1, duration = 15 }, - [13549] = { name = "serpent sting", rank = 2, duration = 15 }, - [13550] = { name = "serpent sting", rank = 3, duration = 15 }, - [13551] = { name = "serpent sting", rank = 4, duration = 15 }, - [13552] = { name = "serpent sting", rank = 5, duration = 15 }, - [13553] = { name = "serpent sting", rank = 6, duration = 15 }, - [13554] = { name = "serpent sting", rank = 7, duration = 15 }, - [13555] = { name = "serpent sting", rank = 8, duration = 15 }, - [25295] = { name = "serpent sting", rank = 9, duration = 15 }, - -- Viper Sting - [3034] = { name = "viper sting", rank = 1, duration = 8 }, - [14279] = { name = "viper sting", rank = 2, duration = 8 }, - [14280] = { name = "viper sting", rank = 3, duration = 8 }, - -- Wing Clip - [2974] = { name = "wing clip", rank = 1, duration = 10 }, - [14267] = { name = "wing clip", rank = 2, duration = 10 }, - [14268] = { name = "wing clip", rank = 3, duration = 10 }, - -- Concussive Shot - [5116] = { name = "concussive shot", rank = 1, duration = 4 }, - -- Wyvern Sting - [19386] = { name = "wyvern sting", rank = 1, duration = 12 }, - [24132] = { name = "wyvern sting", rank = 2, duration = 12 }, - [24133] = { name = "wyvern sting", rank = 3, duration = 12 }, - -- Hunter's Mark - [1130] = { name = "hunter's mark", rank = 1, duration = 120 }, - [14323] = { name = "hunter's mark", rank = 2, duration = 120 }, - [14324] = { name = "hunter's mark", rank = 3, duration = 120 }, - [14325] = { name = "hunter's mark", rank = 4, duration = 120 }, - } -end - -local function getMageSpells() - return { - -- Polymorph - [118] = { name = "polymorph", rank = 1, duration = 20 }, - [12824] = { name = "polymorph", rank = 2, duration = 30 }, - [12825] = { name = "polymorph", rank = 3, duration = 40 }, - [12826] = { name = "polymorph", rank = 4, duration = 50 }, - [28270] = { name = "polymorph: cow", rank = 1, duration = 50 }, - [28271] = { name = "polymorph: turtle", rank = 1, duration = 50 }, - [28272] = { name = "polymorph: pig", rank = 1, duration = 50 }, - -- Frostbolt (snare component) - [116] = { name = "frostbolt", rank = 1, duration = 5 }, - [205] = { name = "frostbolt", rank = 2, duration = 6 }, - [837] = { name = "frostbolt", rank = 3, duration = 6 }, - [7322] = { name = "frostbolt", rank = 4, duration = 7 }, - [8406] = { name = "frostbolt", rank = 5, duration = 7 }, - [8407] = { name = "frostbolt", rank = 6, duration = 8 }, - [8408] = { name = "frostbolt", rank = 7, duration = 8 }, - [10179] = { name = "frostbolt", rank = 8, duration = 9 }, - [10180] = { name = "frostbolt", rank = 9, duration = 9 }, - [10181] = { name = "frostbolt", rank = 10, duration = 9 }, - [25304] = { name = "frostbolt", rank = 11, duration = 9 }, - } -end - -local function getPriestSpells() - return { - -- Shackle Undead - [9485] = { name = "shackle undead", rank = 1, duration = 30 }, - [9486] = { name = "shackle undead", rank = 2, duration = 40 }, - [10955] = { name = "shackle undead", rank = 3, duration = 50 }, - -- Mind Control - [605] = { name = "mind control", rank = 1, duration = 60 }, - [10911] = { name = "mind control", rank = 2, duration = 30 }, - [10912] = { name = "mind control", rank = 3, duration = 30 }, - -- Devouring Plague - [2944] = { name = "devouring plague", rank = 1, duration = 24 }, - [19276] = { name = "devouring plague", rank = 2, duration = 24 }, - [19277] = { name = "devouring plague", rank = 3, duration = 24 }, - [19278] = { name = "devouring plague", rank = 4, duration = 24 }, - [19279] = { name = "devouring plague", rank = 5, duration = 24 }, - [19280] = { name = "devouring plague", rank = 6, duration = 24 }, - -- Shadow Word: Pain - [589] = { name = "shadow word: pain", rank = 1, duration = 18 }, - [594] = { name = "shadow word: pain", rank = 2, duration = 18 }, - [970] = { name = "shadow word: pain", rank = 3, duration = 18 }, - [992] = { name = "shadow word: pain", rank = 4, duration = 18 }, - [2767] = { name = "shadow word: pain", rank = 5, duration = 18 }, - [10892] = { name = "shadow word: pain", rank = 6, duration = 18 }, - [10893] = { name = "shadow word: pain", rank = 7, duration = 18 }, - [10894] = { name = "shadow word: pain", rank = 8, duration = 18 }, - -- Vampiric Embrace - [15286] = { name = "vampiric embrace", rank = 1, duration = 60 }, - -- Holy Fire (DoT component) - [14914] = { name = "holy fire", rank = 1, duration = 10 }, - [15262] = { name = "holy fire", rank = 2, duration = 10 }, - [15263] = { name = "holy fire", rank = 3, duration = 10 }, - [15264] = { name = "holy fire", rank = 4, duration = 10 }, - [15265] = { name = "holy fire", rank = 5, duration = 10 }, - [15266] = { name = "holy fire", rank = 6, duration = 10 }, - [15267] = { name = "holy fire", rank = 7, duration = 10 }, - [15261] = { name = "holy fire", rank = 8, duration = 10 }, - } -end - -local function getShamanSpells() - return { - -- Flame Shock - [8050] = { name = "flame shock", rank = 1, duration = 12 }, - [8052] = { name = "flame shock", rank = 2, duration = 12 }, - [8053] = { name = "flame shock", rank = 3, duration = 12 }, - [10447] = { name = "flame shock", rank = 4, duration = 12 }, - [10448] = { name = "flame shock", rank = 5, duration = 12 }, - [29228] = { name = "flame shock", rank = 6, duration = 12 }, - -- Frost Shock (snare) - [8056] = { name = "frost shock", rank = 1, duration = 8 }, - [8058] = { name = "frost shock", rank = 2, duration = 8 }, - [10472] = { name = "frost shock", rank = 3, duration = 8 }, - [10473] = { name = "frost shock", rank = 4, duration = 8 }, - } -end - -local function getPaladinSpells() - return { - -- Judgement of the Crusader - [21183] = { name = "judgement of the crusader", rank = 1, duration = 10 }, - [20188] = { name = "judgement of the crusader", rank = 2, duration = 10 }, - [20300] = { name = "judgement of the crusader", rank = 3, duration = 10 }, - [20301] = { name = "judgement of the crusader", rank = 4, duration = 10 }, - [20302] = { name = "judgement of the crusader", rank = 5, duration = 10 }, - [20303] = { name = "judgement of the crusader", rank = 6, duration = 10 }, - -- Judgement of Light - [20185] = { name = "judgement of light", rank = 1, duration = 10 }, - [20267] = { name = "judgement of light", rank = 2, duration = 10 }, - [20268] = { name = "judgement of light", rank = 3, duration = 10 }, - [20271] = { name = "judgement of light", rank = 4, duration = 10 }, - -- Judgement of Wisdom - [20186] = { name = "judgement of wisdom", rank = 1, duration = 10 }, - [20354] = { name = "judgement of wisdom", rank = 2, duration = 10 }, - [20355] = { name = "judgement of wisdom", rank = 3, duration = 10 }, - -- Judgement of Justice - [20184] = { name = "judgement of justice", rank = 1, duration = 10 }, - -- Hammer of Justice - [853] = { name = "hammer of justice", rank = 1, duration = 3 }, - [5588] = { name = "hammer of justice", rank = 2, duration = 4 }, - [5589] = { name = "hammer of justice", rank = 3, duration = 5 }, - [10308] = { name = "hammer of justice", rank = 4, duration = 6 }, - } -end - --- ============================================================================ --- State Variables --- ============================================================================ - -local trackedSpellIds = {} -- spellID -> { name, rank, duration, texture, ... } -local trackedSpellNamesToTextures = {} -- "rake" -> texture path -local discoveredGuids = {} -- guid -> timestamp (when discovered) -local trackedDebuffs = {} -- guid -> { [spellName] = { spellID, rank, duration, start, texture } } -local pendingCast = {} -- Current pending cast { spellID, targetGuid, time } -local lastTargetGuid = nil -- Last target GUID for resist detection - -local mainFrame = nil -local unitFrames = {} -- [row] -> frame -local isFrameVisible = false -local isDisabled = false -local isUnlocked = false - --- ============================================================================ --- Spell Loading --- ============================================================================ - -local function LoadClassSpells() - trackedSpellIds = {} - trackedSpellNamesToTextures = {} - - -- Get player class at load time (not file parse time) - local _, className = UnitClass("player") - playerClassName = className - - if not playerClassName then - CleveRoids.Print("MultiTargetTracker: Could not determine player class") - return false - end - - local spells = nil - if playerClassName == "DRUID" then - spells = getDruidSpells() - elseif playerClassName == "ROGUE" then - spells = getRogueSpells() - elseif playerClassName == "WARRIOR" then - spells = getWarriorSpells() - elseif playerClassName == "WARLOCK" then - spells = getWarlockSpells() - elseif playerClassName == "HUNTER" then - spells = getHunterSpells() - elseif playerClassName == "MAGE" then - spells = getMageSpells() - elseif playerClassName == "PRIEST" then - spells = getPriestSpells() - elseif playerClassName == "SHAMAN" then - spells = getShamanSpells() - elseif playerClassName == "PALADIN" then - spells = getPaladinSpells() - end - - if not spells then - CleveRoids.Print("MultiTargetTracker: No spells defined for class " .. tostring(playerClassName)) - return false - end - - local spellCount = 0 - for spellID, data in pairs(spells) do - -- Get texture via SpellInfo - local name, rank, texture = SpellInfo(spellID) - data.texture = texture - trackedSpellIds[spellID] = data - trackedSpellNamesToTextures[data.name] = texture - spellCount = spellCount + 1 - end - - return true, spellCount -end - --- ============================================================================ --- GUID Discovery (Cursive core.lua style) --- ============================================================================ - -local function AddGuid(guid) - if not guid or guid == "" or guid == "0x0" or guid == "0x000000000" then - return - end - if string.sub(guid, 1, 2) ~= "0x" then - return - end - if UnitExists(guid) and not UnitIsDead(guid) then - discoveredGuids[guid] = GetTime() - end -end - -local function AddUnit(unit) - local _, guid = UnitExists(unit) - if guid and not UnitIsDead(unit) then - discoveredGuids[guid] = GetTime() - end -end - -local function RemoveGuid(guid) - discoveredGuids[guid] = nil - trackedDebuffs[guid] = nil -end - --- ============================================================================ --- Debuff Scanning --- ============================================================================ - -local function ScanGuidForDebuff(guid, spellID) - if not CleveRoids.hasSuperwow then return false end - - -- Scan debuff slots 1-16 - for slot = 1, 16 do - local _, _, _, id = UnitDebuff(guid, slot) - if not id then break end - if id == spellID then return true end - end - - -- Scan buff slots (for overflow debuffs) 1-32 - for slot = 1, 32 do - local _, _, id = UnitBuff(guid, slot) - if not id then break end - if id == spellID then return true end - end - - return false -end - -local function ScanGuidForAllDebuffs(guid) - if not CleveRoids.hasSuperwow then return end - if not trackedDebuffs[guid] then return end - - for spellName, debuffData in pairs(trackedDebuffs[guid]) do - if not ScanGuidForDebuff(guid, debuffData.spellID) then - -- Debuff no longer on target, remove it - trackedDebuffs[guid][spellName] = nil - end - end -end - --- ============================================================================ --- Debuff Application --- ============================================================================ - -local function GetDebuffDuration(spellID) - local spellData = trackedSpellIds[spellID] - if not spellData then return 0 end - - if spellData.calculateDuration then - return spellData.calculateDuration() - end - - return spellData.duration or 0 -end - -local function ApplyDebuff(spellID, targetGuid, startTime, duration) - local spellData = trackedSpellIds[spellID] - if not spellData then return end - - -- Verify debuff is actually on target - if not ScanGuidForDebuff(targetGuid, spellID) then - return - end - - if not trackedDebuffs[targetGuid] then - trackedDebuffs[targetGuid] = {} - end - - trackedDebuffs[targetGuid][spellData.name] = { - spellID = spellID, - rank = spellData.rank, - duration = duration, - start = startTime, - texture = spellData.texture, - } - - -- Ensure GUID is in discovery list - discoveredGuids[targetGuid] = GetTime() -end - -local function RemoveDebuff(guid, spellName) - if trackedDebuffs[guid] and trackedDebuffs[guid][spellName] then - trackedDebuffs[guid][spellName] = nil - end -end - -local function TimeRemaining(debuffData) - local remaining = debuffData.duration - (GetTime() - debuffData.start) - return remaining > 0 and remaining or 0 -end - -local function HasAnyDebuff(guid) - if trackedDebuffs[guid] and next(trackedDebuffs[guid]) then - return true - end - return false -end - --- ============================================================================ --- Event Handlers --- ============================================================================ - -local eventFrame = CreateFrame("Frame", "CleveRoidsMTTEventFrame", UIParent) - --- GUID Discovery events -eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED") -eventFrame:RegisterEvent("UNIT_COMBAT") -eventFrame:RegisterEvent("UNIT_MODEL_CHANGED") - --- Cast tracking -eventFrame:RegisterEvent("UNIT_CASTEVENT") - --- Resist/immune detection -eventFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") - --- Fade detection -eventFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER") - --- Combo points for finisher tracking -eventFrame:RegisterEvent("PLAYER_COMBO_POINTS") - --- Resist patterns -local resist_pattern = "Your (.+) was resisted by (.+)" -local immune_pattern = "Your (.+) fail.+%. (.+) is immune" -local missed_pattern = "Your (.+) missed (.+)" -local parried_pattern = "Your (.+) is parried by (.+)" -local blocked_pattern = "Your (.+) was blocked by (.+)" -local dodged_pattern = "Your (.+) was dodged by (.+)" - --- Fade pattern -local fades_pattern = "(.+) fades from (.+)" - -eventFrame:SetScript("OnEvent", function() - if event == "PLAYER_TARGET_CHANGED" then - AddUnit("target") - - elseif event == "UNIT_COMBAT" or event == "UNIT_MODEL_CHANGED" then - -- arg1 is GUID in SuperWoW - if arg1 then - AddGuid(arg1) - end - - elseif event == "UNIT_CASTEVENT" then - -- UNIT_CASTEVENT args: - -- arg1 = casterGUID - -- arg2 = targetGUID - -- arg3 = event type ("START", "CAST", "FAIL", "CHANNEL") - -- arg4 = spell ID - -- arg5 = cast duration - - local casterGuid = arg1 - local targetGuid = arg2 - local eventType = arg3 - local spellID = arg4 - - -- Check if caster is player - local _, playerGuid = UnitExists("player") - if not playerGuid or casterGuid ~= playerGuid then - return - end - - if eventType == "CAST" then - if trackedSpellIds[spellID] and targetGuid then - lastTargetGuid = targetGuid - - -- Calculate duration - local duration = GetDebuffDuration(spellID) - - -- Apply debuff after a short delay to account for travel time - local delay = 0.2 - local _, _, ping = GetNetStats() - if ping and ping > 0 and ping < 500 then - delay = 0.05 + (ping / 1000) - end - - -- Store pending cast - pendingCast = { - spellID = spellID, - targetGuid = targetGuid, - time = GetTime(), - duration = duration - delay, - } - - -- Schedule application - local applyFrame = CreateFrame("Frame") - applyFrame.elapsed = 0 - applyFrame:SetScript("OnUpdate", function() - this.elapsed = this.elapsed + arg1 - if this.elapsed >= delay then - if pendingCast.spellID == spellID and pendingCast.targetGuid == targetGuid then - ApplyDebuff(spellID, targetGuid, GetTime(), pendingCast.duration) - pendingCast = {} - end - this:SetScript("OnUpdate", nil) - end - end) - end - - elseif eventType == "START" then - if trackedSpellIds[spellID] and targetGuid then - pendingCast = { - spellID = spellID, - targetGuid = targetGuid, - time = GetTime(), - } - end - - elseif eventType == "FAIL" then - pendingCast = {} - end - - elseif event == "CHAT_MSG_SPELL_SELF_DAMAGE" then - local message = arg1 - - local patterns = { resist_pattern, immune_pattern } - -- Add melee patterns for melee classes - if playerClassName == "DRUID" or playerClassName == "ROGUE" or playerClassName == "WARRIOR" then - table.insert(patterns, missed_pattern) - table.insert(patterns, parried_pattern) - table.insert(patterns, blocked_pattern) - table.insert(patterns, dodged_pattern) - end - - for _, pattern in pairs(patterns) do - local _, _, spellName, target = string.find(message, pattern) - if spellName and target then - -- Cancel pending cast - pendingCast = {} - return - end - end - - elseif event == "CHAT_MSG_SPELL_AURA_GONE_OTHER" then - local message = arg1 - local _, _, spellName, target = string.find(message, fades_pattern) - - if spellName and target then - local lowerSpellName = string.lower(spellName) - - -- Check if this is a spell we track - if trackedSpellNamesToTextures[lowerSpellName] then - -- Find the GUID for this target and verify - for guid, debuffs in pairs(trackedDebuffs) do - if debuffs[lowerSpellName] then - if not ScanGuidForDebuff(guid, debuffs[lowerSpellName].spellID) then - RemoveDebuff(guid, lowerSpellName) - end - end - end - end - end - - elseif event == "PLAYER_COMBO_POINTS" then - previousComboPoints = currentComboPoints - currentComboPoints = GetComboPoints() - end -end) - --- ============================================================================ --- UI Creation --- ============================================================================ - -local function GetBarWidth() - return CONFIG.targetIndicatorSize + CONFIG.raidIconSize + CONFIG.padding + - CONFIG.healthBarWidth + CONFIG.padding + - (MAX_DEBUFFS_PER_ROW * (CONFIG.debuffIconSize + CONFIG.debuffIconSpacing)) -end - -local function FormatHealth(hp) - if not hp then return "" end - if hp >= 1000000 then - return string.format("%.1fm", hp / 1000000) - elseif hp >= 1000 then - return string.format("%.1fk", hp / 1000) - end - return tostring(hp) -end - -local function CreateUnitFrame(parent, index) - local frame = CreateFrame("Frame", "CleveRoidsMTTRow" .. index, parent) - frame:SetWidth(GetBarWidth()) - frame:SetHeight(CONFIG.barHeight) - frame.guid = nil - frame.hover = false - - -- First section: target indicator + raid icon - local firstSection = CreateFrame("Frame", nil, frame) - firstSection:SetPoint("LEFT", frame, "LEFT", 0, 0) - firstSection:SetWidth(CONFIG.targetIndicatorSize + CONFIG.raidIconSize + CONFIG.padding) - firstSection:SetHeight(CONFIG.barHeight) - frame.firstSection = firstSection - - -- Target indicator - local targetIndicator = firstSection:CreateTexture(nil, "OVERLAY") - targetIndicator:SetWidth(CONFIG.targetIndicatorSize) - targetIndicator:SetHeight(8) - targetIndicator:SetPoint("LEFT", firstSection, "LEFT", 0, 0) - targetIndicator:SetTexture("Interface\\AddOns\\Cursive\\img\\target-left") - targetIndicator:Hide() - frame.targetIndicator = targetIndicator - - -- Raid icon - local raidIcon = firstSection:CreateTexture(nil, "OVERLAY") - raidIcon:SetWidth(CONFIG.raidIconSize) - raidIcon:SetHeight(CONFIG.raidIconSize) - raidIcon:SetPoint("RIGHT", firstSection, "RIGHT", 0, 0) - raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons") - raidIcon:Hide() - frame.raidIcon = raidIcon - - -- Second section: health bar (clickable) - local secondSection = CreateFrame("Button", nil, frame) - secondSection:SetPoint("LEFT", firstSection, "RIGHT", 0, 0) - secondSection:SetWidth(CONFIG.healthBarWidth + CONFIG.padding) - secondSection:SetHeight(CONFIG.barHeight) - secondSection.parent = frame - frame.secondSection = secondSection - - -- Click to target - secondSection:RegisterForClicks("LeftButtonUp", "RightButtonUp") - secondSection:SetScript("OnClick", function() - if this.parent.guid then - if arg1 == "LeftButton" then - TargetUnit(this.parent.guid) - elseif arg1 == "RightButton" then - TargetUnit(this.parent.guid) - if not PlayerFrame.inCombat then - AttackTarget() - end - end - end - end) - - secondSection:SetScript("OnEnter", function() - this.parent.hover = true - if this.parent.healthBar and this.parent.healthBar.border then - this.parent.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1) - end - if this.parent.guid then - GameTooltip_SetDefaultAnchor(GameTooltip, this) - GameTooltip:SetUnit(this.parent.guid) - GameTooltip:Show() - end - end) - - secondSection:SetScript("OnLeave", function() - this.parent.hover = false - GameTooltip:Hide() - end) - - -- Health bar - local healthBar = CreateFrame("StatusBar", nil, secondSection) - healthBar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") - healthBar:SetStatusBarColor(COLORS.healthDefault[1], COLORS.healthDefault[2], COLORS.healthDefault[3], 1) - healthBar:SetMinMaxValues(0, 100) - healthBar:SetValue(100) - healthBar:SetPoint("LEFT", secondSection, "LEFT", CONFIG.padding, 0) - healthBar:SetWidth(CONFIG.healthBarWidth) - healthBar:SetHeight(CONFIG.barHeight) - frame.healthBar = healthBar - - -- Health bar backdrop - healthBar:SetBackdrop(BACKDROP_BACKGROUND) - healthBar:SetBackdropColor(0, 0, 0, 1) - - -- Health bar border - local border = CreateFrame("Frame", nil, healthBar) - border:SetBackdrop(BACKDROP_BORDER) - border:SetBackdropBorderColor(COLORS.borderNormal[1], COLORS.borderNormal[2], COLORS.borderNormal[3], 1) - border:SetPoint("TOPLEFT", healthBar, "TOPLEFT", -2, 2) - border:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", 2, -2) - healthBar.border = border - - -- HP text - local hpText = healthBar:CreateFontString(nil, "OVERLAY", "GameFontWhite") - hpText:SetPoint("TOPRIGHT", healthBar, "TOPRIGHT", -2, -2) - hpText:SetFont(STANDARD_TEXT_FONT, CONFIG.textSize, "OUTLINE") - hpText:SetJustifyH("RIGHT") - frame.hpText = hpText - - -- Name text - local nameText = healthBar:CreateFontString(nil, "OVERLAY", "GameFontWhite") - nameText:SetPoint("TOPLEFT", healthBar, "TOPLEFT", 2, -2) - nameText:SetPoint("BOTTOMRIGHT", hpText, "BOTTOMLEFT", -2, 0) - nameText:SetFont(STANDARD_TEXT_FONT, CONFIG.textSize, "OUTLINE") - nameText:SetJustifyH("LEFT") - frame.nameText = nameText - - -- Third section: debuff icons - local thirdSection = CreateFrame("Frame", nil, frame) - thirdSection:SetPoint("LEFT", secondSection, "RIGHT", 0, 0) - thirdSection:SetWidth(MAX_DEBUFFS_PER_ROW * (CONFIG.debuffIconSize + CONFIG.debuffIconSpacing)) - thirdSection:SetHeight(CONFIG.barHeight) - frame.thirdSection = thirdSection - - -- Debuff icons - frame.debuffIcons = {} - for i = 1, MAX_DEBUFFS_PER_ROW do - local icon = thirdSection:CreateTexture(nil, "OVERLAY") - icon:SetWidth(CONFIG.debuffIconSize) - icon:SetHeight(CONFIG.debuffIconSize) - icon:SetPoint("LEFT", thirdSection, "LEFT", (i - 1) * (CONFIG.debuffIconSize + CONFIG.debuffIconSpacing) + CONFIG.padding, 0) - icon:Hide() - - local timer = thirdSection:CreateFontString(nil, "OVERLAY", "GameFontNormal") - timer:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE") - timer:SetTextColor(1, 1, 1) - timer:SetAllPoints(icon) - timer:Hide() - icon.timer = timer - - frame.debuffIcons[i] = icon - end - - frame:Hide() - return frame -end - -local function CreateMainFrame() - local frame = CreateFrame("Frame", "CleveRoidsMultiTargetFrame", UIParent) - frame:SetWidth(GetBarWidth() + 10) - frame:SetHeight(CONFIG.titleHeight + (MAX_TRACKED_TARGETS * (CONFIG.barHeight + CONFIG.barSpacing)) + 10) - frame:SetPoint("CENTER", UIParent, "CENTER", 200, 100) - frame:SetBackdrop(BACKDROP_BACKGROUND) - frame:SetBackdropColor(0, 0, 0, 0.8) - frame:EnableMouse(true) - frame:SetMovable(true) - frame:RegisterForDrag("LeftButton") - - frame:SetScript("OnDragStart", function() - if isUnlocked then - this:StartMoving() - end - end) - - frame:SetScript("OnDragStop", function() - this:StopMovingOrSizing() - SaveFramePosition() - end) - - -- Title - local title = frame:CreateFontString(nil, "OVERLAY", "GameFontWhite") - title:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -2) - title:SetFont(STANDARD_TEXT_FONT, CONFIG.textSize, "OUTLINE") - title:SetText("Debuff Tracker") - title:SetTextColor(COLORS.titleText[1], COLORS.titleText[2], COLORS.titleText[3], 1) - frame.titleText = title - - -- Create unit frames - for i = 1, MAX_TRACKED_TARGETS do - local unitFrame = CreateUnitFrame(frame, i) - unitFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -(CONFIG.titleHeight + (i - 1) * (CONFIG.barHeight + CONFIG.barSpacing))) - unitFrames[i] = unitFrame - end - - frame:Hide() - return frame -end - --- ============================================================================ --- Frame Position Saving --- ============================================================================ - -local function SaveFramePosition() - if not mainFrame then return end - - local point, relativeTo, relativePoint, xOfs, yOfs = mainFrame:GetPoint() - - CleveRoidMacros = CleveRoidMacros or {} - CleveRoidMacros.multiTargetPos = { - point = point, - relativePoint = relativePoint, - x = xOfs, - y = yOfs, - } -end - -local function RestoreFramePosition() - if not mainFrame then return end - - CleveRoidMacros = CleveRoidMacros or {} - local pos = CleveRoidMacros.multiTargetPos - - if pos and pos.point then - mainFrame:ClearAllPoints() - mainFrame:SetPoint(pos.point, UIParent, pos.relativePoint or pos.point, pos.x or 0, pos.y or 0) - end -end - --- ============================================================================ --- Display Logic (Cursive-style priority) --- ============================================================================ - -local function ShouldDisplayGuid(guid) - if not UnitExists(guid) then return false end - if UnitIsDead(guid) then return false end - if not HasAnyDebuff(guid) then return false end - return true -end - -local function GetSortedDebuffs(guidDebuffs) - local debuffList = {} - for name, data in pairs(guidDebuffs) do - table.insert(debuffList, { name = name, data = data }) - end - - -- Sort by time remaining (soonest first) - table.sort(debuffList, function(a, b) - return TimeRemaining(a.data) < TimeRemaining(b.data) - end) - - return debuffList -end - -local function UpdateUnitFrame(frame, guid) - frame.guid = guid - - -- Update health bar - if frame.healthBar then - frame.healthBar:SetMinMaxValues(0, UnitHealthMax(guid) or 100) - frame.healthBar:SetValue(UnitHealth(guid) or 0) - end - - -- Update name - local name = UnitName(guid) - if name and frame.nameText then - frame.nameText:SetText(name) - end - - -- Update HP text - if frame.hpText then - local hp = UnitHealth(guid) - frame.hpText:SetText(FormatHealth(hp)) - end - - -- Update border color - if frame.healthBar and frame.healthBar.border then - if frame.hover then - frame.healthBar.border:SetBackdropBorderColor(1, 1, 1, 1) - elseif UnitAffectingCombat(guid) then - frame.healthBar.border:SetBackdropBorderColor(0.8, 0.2, 0.2, 1) - else - frame.healthBar.border:SetBackdropBorderColor(0.2, 0.2, 0.2, 1) - end - end - - -- Update target indicator - if frame.targetIndicator then - if UnitIsUnit("target", guid) then - frame.targetIndicator:Show() - else - frame.targetIndicator:Hide() - end - end - - -- Update raid icon - if frame.raidIcon then - local raidIndex = GetRaidTargetIndex(guid) - if raidIndex then - SetRaidTargetIconTexture(frame.raidIcon, raidIndex) - frame.raidIcon:Show() - else - frame.raidIcon:Hide() - end - end - - -- Update debuff icons - for i = 1, MAX_DEBUFFS_PER_ROW do - frame.debuffIcons[i]:Hide() - frame.debuffIcons[i].timer:Hide() - end - - local guidDebuffs = trackedDebuffs[guid] - if guidDebuffs then - local sortedDebuffs = GetSortedDebuffs(guidDebuffs) - for i, debuff in ipairs(sortedDebuffs) do - if i > MAX_DEBUFFS_PER_ROW then break end - - local icon = frame.debuffIcons[i] - local remaining = TimeRemaining(debuff.data) - - if remaining > 0 then - icon:SetTexture(debuff.data.texture) - icon:Show() - - -- Timer text - local timerText - if remaining < 10 then - timerText = string.format("%.1f", remaining) - else - timerText = string.format("%d", math.ceil(remaining)) - end - icon.timer:SetText(timerText) - - -- Timer color - if remaining < 3 then - icon.timer:SetTextColor(1, 0.2, 0.2) - else - icon.timer:SetTextColor(1, 1, 1) - end - icon.timer:Show() - end - end - end - - frame:Show() -end - -local function UpdateDisplay() - if isDisabled then return end - - -- Cleanup old/dead GUIDs - for guid, time in pairs(discoveredGuids) do - if not UnitExists(guid) or UnitIsDead(guid) then - RemoveGuid(guid) - elseif GetTime() - time > 900 and not UnitIsVisible(guid) then - -- 15 minutes old and not visible - RemoveGuid(guid) - end - end - - -- Collect displayable GUIDs - local displayable = {} - - -- Priority 1: Raid marked targets - for i = 8, 1, -1 do - local _, guid = UnitExists("mark" .. i) - if guid and ShouldDisplayGuid(guid) then - table.insert(displayable, guid) - end - end - - -- Priority 2: Highest HP targets (top 3) - local hpTargets = {} - for guid, _ in pairs(discoveredGuids) do - if ShouldDisplayGuid(guid) then - local found = false - for _, dg in ipairs(displayable) do - if dg == guid then found = true break end - end - if not found then - table.insert(hpTargets, { guid = guid, hp = UnitHealthMax(guid) or 0 }) - end - end - end - - table.sort(hpTargets, function(a, b) return a.hp > b.hp end) - - for i = 1, 3 do - if hpTargets[i] then - table.insert(displayable, hpTargets[i].guid) - end - end - - -- Priority 3: Remaining targets - for guid, _ in pairs(discoveredGuids) do - if table.getn(displayable) >= MAX_TRACKED_TARGETS then break end - - if ShouldDisplayGuid(guid) then - local found = false - for _, dg in ipairs(displayable) do - if dg == guid then found = true break end - end - if not found then - table.insert(displayable, guid) - end - end - end - - -- Update unit frames - local showFrame = false - for i = 1, MAX_TRACKED_TARGETS do - local frame = unitFrames[i] - local guid = displayable[i] - - if guid then - UpdateUnitFrame(frame, guid) - showFrame = true - else - frame:Hide() - end - end - - -- Show/hide main frame - if mainFrame then - if showFrame and not isDisabled then - if not isFrameVisible then - mainFrame:Show() - isFrameVisible = true - end - elseif not isUnlocked then - if isFrameVisible then - mainFrame:Hide() - isFrameVisible = false - end - end - end -end - --- ============================================================================ --- Update Loop --- ============================================================================ - -local lastUpdate = 0 - -local function OnUpdate() - local now = GetTime() - if now - lastUpdate < UPDATE_INTERVAL then return end - lastUpdate = now - - UpdateDisplay() -end - --- ============================================================================ --- Extension Load --- ============================================================================ - -function Extension.OnLoad() - if not CleveRoids.hasSuperwow then - CleveRoids.Print("MultiTargetTracker: SuperWoW required, extension disabled") - return - end - - -- Load class spells - local success, spellCount = LoadClassSpells() - if not success then - CleveRoids.Print("MultiTargetTracker: Failed to load spells, extension disabled") - return - end - - -- Create main frame - mainFrame = CreateMainFrame() - - -- Restore position - RestoreFramePosition() - - -- Restore hidden state - CleveRoidMacros = CleveRoidMacros or {} - if CleveRoidMacros.multiTargetHidden then - isDisabled = true - mainFrame:Hide() - isFrameVisible = false - end - - -- Start update loop - mainFrame:SetScript("OnUpdate", function() - OnUpdate() - end) - - CleveRoids.Print("Multi-Target Tracker loaded for " .. playerClassName .. " (" .. spellCount .. " spells). Use /cleveroid mtt for commands.") -end - --- ============================================================================ --- API Export --- ============================================================================ - -CleveRoids.MultiTargetTracker = { - Show = function() - if mainFrame then - mainFrame:Show() - isFrameVisible = true - isDisabled = false - CleveRoidMacros = CleveRoidMacros or {} - CleveRoidMacros.multiTargetHidden = nil - end - end, - - Hide = function() - if mainFrame then - mainFrame:Hide() - isFrameVisible = false - isDisabled = true - CleveRoidMacros = CleveRoidMacros or {} - CleveRoidMacros.multiTargetHidden = true - end - end, - - Toggle = function() - if mainFrame then - if isDisabled then - CleveRoids.MultiTargetTracker.Show() - else - CleveRoids.MultiTargetTracker.Hide() - end - end - end, - - IsVisible = function() - return isFrameVisible - end, - - IsDisabled = function() - return isDisabled - end, - - Unlock = function() - isUnlocked = true - if mainFrame then - mainFrame:Show() - isFrameVisible = true - if mainFrame.titleText then - mainFrame.titleText:SetText("Debuff Tracker (UNLOCKED)") - mainFrame.titleText:SetTextColor(0, 1, 0, 1) - end - end - CleveRoids.Print("Multi-Target Tracker: Unlocked - drag to move") - end, - - Lock = function() - isUnlocked = false - if mainFrame and mainFrame.titleText then - mainFrame.titleText:SetText("Debuff Tracker") - mainFrame.titleText:SetTextColor(COLORS.titleText[1], COLORS.titleText[2], COLORS.titleText[3], 1) - end - SaveFramePosition() - CleveRoids.Print("Multi-Target Tracker: Locked") - end, - - ResetPosition = function() - CleveRoidMacros = CleveRoidMacros or {} - CleveRoidMacros.multiTargetPos = nil - if mainFrame then - mainFrame:ClearAllPoints() - mainFrame:SetPoint("CENTER", UIParent, "CENTER", 200, 100) - end - CleveRoids.Print("Multi-Target Tracker: Position reset") - end, - - ClearTargets = function() - discoveredGuids = {} - trackedDebuffs = {} - CleveRoids.Print("Multi-Target Tracker: Cleared all targets") - end, -} - -_G["CleveRoids"] = CleveRoids diff --git a/Init.lua b/Init.lua index 3c90014..9558ffd 100644 --- a/Init.lua +++ b/Init.lua @@ -112,10 +112,6 @@ CleveRoids.dynamicCmds = { ["/equip"] = true, ["/equipmh"] = true, ["/equipoh"] = true, - ["/equip11"] = true, - ["/equip12"] = true, - ["/equip13"] = true, - ["/equip14"] = true, } -- Equipment swap queue system diff --git a/README.md b/README.md index 05b43d6..cabaf06 100644 --- a/README.md +++ b/README.md @@ -347,52 +347,6 @@ Works with ALL casting methods: --- -## Multi-Target Tracker - -Visual UI extension that tracks your debuffs across multiple enemy targets. Displays health bars with debuff icons and timers, styled to match Cursive/ShaguScan. - -### Features -- Tracks up to 15 targets simultaneously -- Shows health bar, name, raid icon, and debuff timers per target -- Automatic cleanup when targets die or leave combat -- Only shows YOUR debuffs (class-aware filtering) -- Click target rows to target that enemy - -### Commands - -| Command | Description | -|---------|-------------| -| `/cleveroid mtt` | Show all MTT commands | -| `/cleveroid mtt unlock` | Unlock frame for repositioning | -| `/cleveroid mtt lock` | Lock and resume auto-hide | -| `/cleveroid mtt show` | Show the tracker frame | -| `/cleveroid mtt hide` | Hide the tracker frame | -| `/cleveroid mtt toggle` | Toggle frame visibility | -| `/cleveroid mtt clear` | Clear all tracked targets | -| `/cleveroid mtt reset` | Reset frame position | -| `/cleveroid mtt debug` | Toggle debug mode | -| `/cleveroid mtt dump` | Dump tracking state | - -### Tracked Debuff Categories - -**AoE Debuffs** (multi-target tracking): - -**Proc/Special Debuffs** (item procs, taunts): - -**Personal Debuffs**: -All class-specific DoTs, debuffs, and effects from the debuff timer system are automatically tracked. - -**Shared Debuffs**: -Debuffs like Sunder Armor, Faerie Fire, and Judgements are only shown if YOU can cast them (class-aware filtering prevents showing other players' debuffs). - -### Auto-Cleanup Behavior -- Dead targets are removed immediately -- Targets with no active debuffs are removed when: - - The target leaves combat, OR - - You leave combat - ---- - ## Talent Modifiers Automatically applies talent-based duration modifications to debuff tracking. @@ -555,10 +509,6 @@ Default `[noimmune]` checks the debuff school (bleed). | /equip | * | Equip item by name/ID | | /equipmh | * | Equip main hand | | /equipoh | * | Equip off-hand | -| /equip11 | * | Equip ring slot 1 | -| /equip12 | * | Equip ring slot 2 | -| /equip13 | * | Equip trinket slot 1 | -| /equip14 | * | Equip trinket slot 2 | | /unshift | * | Cancel shapeshift form | | /cancelaura | | Cancel buff/aura | | /unbuff | | Alias for cancelaura | diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 9c5fc2d..e6c297b 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -2,7 +2,7 @@ ## Author: DWG, _brain, Torio, yani9o, Avitasia ## Title: [Super]CleveRoidMacros ## Notes: /cleveroid for settings -## Version: 1.9 +## Version: 1.8 ## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar ## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools Localization.lua @@ -35,4 +35,3 @@ Extensions\Mouseover\ag_UnitFrames.lua Extensions\Tooltip\Generic.lua -Extensions\MultiTargetTracker.lua diff --git a/Utility.lua b/Utility.lua index 301d201..8c9767f 100644 --- a/Utility.lua +++ b/Utility.lua @@ -17,30 +17,13 @@ local tonumber = tonumber local tostring = tostring local GetTime = GetTime --- GUID normalization: ensures consistent string format for table keys --- Preserves original format (including 0x prefix) for SuperWoW API compatibility +-- GUID normalization: ensure all GUIDs are strings for consistent table key lookups +-- In Lua, table["123"] is different from table[123], so we must normalize function CleveRoids.NormalizeGUID(guid) if not guid then return nil end return tostring(guid) end --- Compare two GUIDs for equality, handling format differences (0x prefix, case) --- Use this when comparing GUIDs from different sources (e.g., UnitExists vs AURA_CAST_ON_OTHER) -function CleveRoids.GUIDsMatch(guid1, guid2) - if not guid1 or not guid2 then return false end - local s1 = tostring(guid1) - local s2 = tostring(guid2) - -- Strip 0x prefix if present for comparison - if string.sub(s1, 1, 2) == "0x" or string.sub(s1, 1, 2) == "0X" then - s1 = string.sub(s1, 3) - end - if string.sub(s2, 1, 2) == "0x" or string.sub(s2, 1, 2) == "0X" then - s2 = string.sub(s2, 3) - end - -- Case-insensitive comparison - return string.lower(s1) == string.lower(s2) -end - -- Hidden tooltip for scanning spell info local SpellScanTooltip = nil @@ -614,18 +597,6 @@ lib.personalDebuffs = lib.personalDebuffs or { [12323] = 6, -- Piercing Howl - [12721] = 12, -- Deep Wound (Rank 1) - actual debuff, not talent trigger - [12722] = 12, -- Deep Wound (Rank 2) - [12723] = 12, -- Deep Wound (Rank 3) - - [355] = 3, -- Taunt - [1161] = 6, -- Challenging Shout - [694] = 6, -- Mocking Blow (Rank 1) - [7400] = 6, -- Mocking Blow (Rank 2) - [7402] = 6, -- Mocking Blow (Rank 3) - [20559] = 6, -- Mocking Blow (Rank 4) - [20560] = 6, -- Mocking Blow (Rank 5) - -- ROGUE [2094] = 10, -- Blind [21060] = 10, -- Blind (alternate?) @@ -640,17 +611,8 @@ lib.personalDebuffs = lib.personalDebuffs or { [11285] = 4, -- Gouge (Rank 4) [11286] = 4, -- Gouge (Rank 5) - -- NOTE: Rupture/Kidney Shot durations calculated by ComboPointTracker - -- Base durations here for MultiTargetTracker recognition - [1943] = 16, -- Rupture (Rank 1) - base 8s + 2s per CP (max 5CP = 16s) - [8639] = 16, -- Rupture (Rank 2) - [8640] = 16, -- Rupture (Rank 3) - [11273] = 16, -- Rupture (Rank 4) - [11274] = 16, -- Rupture (Rank 5) - [11275] = 16, -- Rupture (Rank 6) - - [408] = 6, -- Kidney Shot (Rank 1) - base 1s + 1s per CP (max 5CP = 6s) - [8643] = 6, -- Kidney Shot (Rank 2) + -- NOTE: Rupture removed - handled by ComboPointTracker (base 8s + 2s per CP) + -- NOTE: Kidney Shot removed - handled by ComboPointTracker (base 1s/2s + 1s per CP) [703] = 18, -- Garrote (Rank 1) [8631] = 18, -- Garrote (Rank 2) @@ -792,18 +754,6 @@ lib.personalDebuffs = lib.personalDebuffs or { [9824] = 18, -- Pounce Bleed (Rank 2) - triggered by Pounce 9823 [9826] = 18, -- Pounce Bleed (Rank 3) - triggered by Pounce 9827 - [9005] = 2, -- Pounce Stun (Rank 1) - [9823] = 3, -- Pounce Stun (Rank 2) - [9827] = 3, -- Pounce Stun (Rank 3) - - [99] = 30, -- Demoralizing Roar (Rank 1) - [1735] = 30, -- Demoralizing Roar (Rank 2) - [9490] = 30, -- Demoralizing Roar (Rank 3) - [9747] = 30, -- Demoralizing Roar (Rank 4) - [9898] = 30, -- Demoralizing Roar (Rank 5) - - [5209] = 6, -- Challenging Roar - -- WARLOCK [172] = 12, -- Corruption (Rank 1) [6222] = 15, -- Corruption (Rank 2) @@ -1000,10 +950,10 @@ lib.sharedDebuffs = lib.sharedDebuffs or { [11597] = 30, -- Sunder Armor (Rank 5) [6343] = 10, -- Thunder Clap (Rank 1) - [8198] = 14, -- Thunder Clap (Rank 2) - [8205] = 18, -- Thunder Clap (Rank 3) - [11580] = 22, -- Thunder Clap (Rank 4) - [11581] = 30, -- Thunder Clap (Rank 5) + [8198] = 10, -- Thunder Clap (Rank 2) + [8205] = 10, -- Thunder Clap (Rank 3) + [11580] = 10, -- Thunder Clap (Rank 4) + [11581] = 10, -- Thunder Clap (Rank 5) [1160] = 30, -- Demoralizing Shout (Rank 1) [6190] = 30, -- Demoralizing Shout (Rank 2)