From f457ac7ec4de159af1f5b13570f86aea8b3b8e15 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sat, 3 Jan 2026 16:11:13 -0500 Subject: [PATCH] check nampower version to get exact functionality for that version as well as update cursive compatibility and custom spell add cursive functions --- Extensions/CursiveCustomSpells.lua | 610 +++++++++++++++++++++++++++++ NampowerAPI.lua | 448 ++++++++++++++++----- SuperCleveRoidMacros.toc | 3 +- 3 files changed, 957 insertions(+), 104 deletions(-) create mode 100644 Extensions/CursiveCustomSpells.lua diff --git a/Extensions/CursiveCustomSpells.lua b/Extensions/CursiveCustomSpells.lua new file mode 100644 index 0000000..d2eac0d --- /dev/null +++ b/Extensions/CursiveCustomSpells.lua @@ -0,0 +1,610 @@ +--[[ + CursiveCustomSpells Extension + + Automatically injects custom spell definitions into Cursive's tracking system. + Any spell in this list will be tracked by Cursive when you cast it normally. + + Format: + [spellID] = { name = "lowercase spell name", rank = X, duration = Y }, + + Optional fields: + variableDuration = true -- Duration can be modified by talents/haste + calculateDuration = func -- Function returning dynamic duration + numTicks = N -- Number of DoT ticks (for tick tracking) + darkHarvest = true -- Affected by Dark Harvest talent +]] + +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids + +--============================================================================ +-- CUSTOM SPELL DEFINITIONS +-- Add your custom debuffs here. They will be auto-tracked when cast. +--============================================================================ + +CleveRoids.CustomCursiveSpells = CleveRoids.CustomCursiveSpells or { + --======================================================================== + -- WARRIOR DEBUFFS (from database.turtlecraft.gg) + --======================================================================== + + -- Deep Wounds (talent proc, bleed over 12 sec - ticks every 3s) + [12721] = { name = "deep wounds", rank = 1, duration = 6 }, + [12162] = { name = "deep wounds", rank = 1, duration = 6 }, + [12850] = { name = "deep wounds", rank = 2, duration = 6 }, + [12868] = { name = "deep wounds", rank = 3, duration = 6 }, + + -- Hamstring (15 sec snare, 50% movement speed) + [1715] = { name = "hamstring", rank = 1, duration = 15 }, + [7372] = { name = "hamstring", rank = 2, duration = 15 }, + [7373] = { name = "hamstring", rank = 3, duration = 15 }, + + --======================================================================== + -- TAUNTS (personal debuffs) + --======================================================================== + + -- Taunt (Warrior, 3 sec) + [355] = { name = "taunt", rank = 1, duration = 3 }, + [29060] = { name = "taunt", rank = 1, duration = 3 }, -- variant + + -- Growl (Druid Bear, 3 sec) + [6795] = { name = "growl", rank = 1, duration = 3 }, + + -- Mocking Blow (Warrior, 6 sec) + [694] = { name = "mocking blow", rank = 1, duration = 6 }, + [7400] = { name = "mocking blow", rank = 2, duration = 6 }, + [7402] = { name = "mocking blow", rank = 3, duration = 6 }, + [20559] = { name = "mocking blow", rank = 4, duration = 6 }, + [20560] = { name = "mocking blow", rank = 5, duration = 6 }, + + -- Hand of Reckoning (Paladin, 3 sec) + [62124] = { name = "hand of reckoning", rank = 1, duration = 3 }, + + --======================================================================== + -- PALADIN JUDGEMENT DEBUFFS + -- Generic 20271 is handled specially below to detect seal type + --======================================================================== + + -- Judgement of the Crusader (10 sec, +holy damage taken) + [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 }, + [20304] = { name = "judgement of the crusader", rank = 6, duration = 10 }, -- variant + [25942] = { name = "judgement of the crusader", rank = 1, duration = 10 }, -- variant + + -- Judgement of Light (10 sec, heals attackers) + [20185] = { name = "judgement of light", rank = 1, duration = 10 }, + [20344] = { name = "judgement of light", rank = 2, duration = 10 }, + [20345] = { name = "judgement of light", rank = 3, duration = 10 }, + [20346] = { name = "judgement of light", rank = 4, duration = 10 }, + + -- Judgement of Wisdom (10 sec, restores mana to attackers) + [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 }, + [51750] = { name = "judgement of wisdom", rank = 5, duration = 10 }, -- Rank 5 cast (confirmed) + [51751] = { name = "judgement of wisdom", rank = 4, duration = 10 }, -- Rank 4 cast + [51752] = { name = "judgement of wisdom", rank = 5, duration = 10 }, -- debuff effect ID + [25757] = { name = "judgement of wisdom", rank = 1, duration = 10 }, -- variant + + -- Judgement of Justice (10 sec, prevents fleeing) + [20184] = { name = "judgement of justice", rank = 1, duration = 10 }, + [25945] = { name = "judgement of justice", rank = 2, duration = 10 }, + + --======================================================================== + -- ROGUE POISONS + --======================================================================== + + -- Deadly Poison (12 sec, stacking nature DoT) + [2818] = { name = "deadly poison", rank = 1, duration = 12 }, + [2819] = { name = "deadly poison", rank = 2, duration = 12 }, + [11353] = { name = "deadly poison", rank = 3, duration = 12 }, + [11354] = { name = "deadly poison", rank = 4, duration = 12 }, + [25349] = { name = "deadly poison", rank = 5, duration = 12 }, + [3583] = { name = "deadly poison", rank = 1, duration = 12 }, -- proc effect + [10022] = { name = "deadly poison", rank = 2, duration = 12 }, -- proc effect + [13582] = { name = "deadly poison", rank = 3, duration = 12 }, -- proc effect + [21787] = { name = "deadly poison", rank = 4, duration = 12 }, -- proc effect + [21788] = { name = "deadly poison", rank = 5, duration = 12 }, -- proc effect + + -- Crippling Poison (12 sec, -50% movement speed) + [3409] = { name = "crippling poison", rank = 1, duration = 12 }, + [11201] = { name = "crippling poison", rank = 2, duration = 12 }, + [25809] = { name = "crippling poison", rank = 2, duration = 12 }, -- variant + + -- Wound Poison (15 sec, -healing received) + [13218] = { name = "wound poison", rank = 1, duration = 15 }, + [13222] = { name = "wound poison", rank = 2, duration = 15 }, + [13223] = { name = "wound poison", rank = 3, duration = 15 }, + [13224] = { name = "wound poison", rank = 4, duration = 15 }, + [25648] = { name = "wound poison", rank = 5, duration = 15 }, + + -- Mind-numbing Poison (10 sec, -casting speed) + [5760] = { name = "mind-numbing poison", rank = 1, duration = 10 }, + [8692] = { name = "mind-numbing poison", rank = 2, duration = 10 }, + [11398] = { name = "mind-numbing poison", rank = 3, duration = 10 }, + [25810] = { name = "mind-numbing poison", rank = 2, duration = 10 }, -- variant + + --======================================================================== + -- THUNDERFURY PROC DEBUFFS + --======================================================================== + + -- Thunderfury - Nature Resist Reduction (12 sec, chains to 5 targets) + [21992] = { name = "thunderfury", rank = 1, duration = 12 }, + -- Thunderfury - Attack Speed Slow (12 sec, -20% attack speed on primary target) + [27648] = { name = "thunderfury slow", rank = 1, duration = 12 }, + + --======================================================================== + -- CUSTOM/SERVER-SPECIFIC (add your own below) + --======================================================================== + -- [12345] = { name = "custom debuff", rank = 1, duration = 18 }, +} + +--============================================================================ +-- PALADIN JUDGEMENT SEAL DETECTION +-- Maps seal buff textures to judgement debuff names and their reference spellIDs +--============================================================================ + +local sealToJudgement = { + -- Seal texture -> { name, referenceSpellID (must be in trackedCurseIds), judgementTexture } + ["Interface\\Icons\\Spell_Holy_RighteousnessAura"] = { + name = "judgement of wisdom", + spellID = 20186, -- JoW rank 1 (in our custom list) + texture = "Interface\\Icons\\Spell_Holy_RighteousnessAura", + }, + ["Interface\\Icons\\Spell_Holy_HealingAura"] = { + name = "judgement of light", + spellID = 20185, -- JoL rank 1 (in our custom list) + texture = "Interface\\Icons\\Spell_Holy_HealingAura", + }, + ["Interface\\Icons\\Spell_Holy_HolySmite"] = { + name = "judgement of the crusader", + spellID = 21183, -- JotC rank 1 (in our custom list) + texture = "Interface\\Icons\\Spell_Holy_HolySmite", + }, + ["Interface\\Icons\\Spell_Holy_SealOfWrath"] = { + name = "judgement of justice", + spellID = 20184, -- JoJ rank 1 (in our custom list) + texture = "Interface\\Icons\\Spell_Holy_SealOfWrath", + }, + ["Interface\\Icons\\Ability_ThunderClap"] = { + name = "judgement of righteousness", + spellID = 20187, -- fallback, may need correct ID + texture = "Interface\\Icons\\Ability_ThunderClap", + }, + ["Interface\\Icons\\Spell_Holy_SealOfMight"] = { + name = "judgement of command", + spellID = 20467, -- fallback, may need correct ID + texture = "Interface\\Icons\\Spell_Holy_SealOfMight", + }, +} + +-- Detect active seal from player buffs and return judgement info +local function GetActiveSealJudgementInfo() + for i = 1, 32 do + local texture = GetPlayerBuffTexture(i) + if not texture then break end + if sealToJudgement[texture] then + return sealToJudgement[texture] + end + end + -- Fallback to generic judgement + return { name = "judgement", spellID = 20271, texture = "Interface\\Icons\\Spell_Holy_RighteousFury" } +end + +--============================================================================ +-- AUTOMATIC INJECTION INTO CURSIVE +-- Hooks Cursive's LoadCurses to inject our spells after every reload +--============================================================================ + +local injectedCount = 0 + +local function InjectCustomSpells() + if not Cursive or not Cursive.curses or not Cursive.curses.trackedCurseIds then + return 0 + end + + local count = 0 + for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do + -- Get texture from SpellInfo (SuperWoW API) + local name, rank, texture = SpellInfo(spellID) + if texture then + -- Always update/add (in case Cursive reloaded and cleared them) + Cursive.curses.trackedCurseIds[spellID] = { + name = data.name, + rank = data.rank or 1, + duration = data.duration, + texture = texture, + variableDuration = data.variableDuration, + calculateDuration = data.calculateDuration, + numTicks = data.numTicks, + darkHarvest = data.darkHarvest, + } + Cursive.curses.trackedCurseNamesToTextures[data.name] = texture + count = count + 1 + end + end + + injectedCount = count + return count +end + +-- Hook Cursive's LoadCurses function to inject after every reload +local function HookCursiveLoadCurses() + if not Cursive or not Cursive.curses or not Cursive.curses.LoadCurses then + return false + end + + -- Check if already hooked + if Cursive.curses._customSpellsHooked then + return true + end + + -- Store original function + local originalLoadCurses = Cursive.curses.LoadCurses + + -- Replace with hooked version + Cursive.curses.LoadCurses = function(self) + -- Call original first + originalLoadCurses(self) + + -- Then inject our custom spells + local count = InjectCustomSpells() + if count > 0 then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Injected " .. count .. " custom spells into Cursive.") + end + end + + Cursive.curses._customSpellsHooked = true + return true +end + +--============================================================================ +-- EXTENSION REGISTRATION +--============================================================================ + +local ext = CleveRoids.RegisterExtension("CursiveCustomSpells") +local hooked = false +local retryFrame = nil + +-- Retry injection periodically until successful +local function StartRetryTimer() + if retryFrame then return end + + retryFrame = CreateFrame("Frame") + local elapsed = 0 + local attempts = 0 + + retryFrame:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 + if elapsed >= 0.5 then -- Check every 0.5 seconds + elapsed = 0 + attempts = attempts + 1 + + if HookCursiveLoadCurses() then + hooked = true + InjectCustomSpells() + retryFrame:SetScript("OnUpdate", nil) + retryFrame = nil + elseif attempts >= 20 then -- Give up after 10 seconds + retryFrame:SetScript("OnUpdate", nil) + retryFrame = nil + end + end + end) +end + +ext.OnLoad = function() + -- Try to hook immediately (Cursive might already be loaded) + if HookCursiveLoadCurses() then + hooked = true + InjectCustomSpells() + else + -- Start retry timer and also register events as backup + StartRetryTimer() + ext.RegisterEvent("PLAYER_ENTERING_WORLD", "OnPlayerEnteringWorld") + end +end + +ext.OnPlayerEnteringWorld = function() + if not hooked then + if HookCursiveLoadCurses() then + hooked = true + InjectCustomSpells() + end + end + -- Don't unregister - keep checking on zone changes +end + +--============================================================================ +-- PALADIN JUDGEMENT HOOK +-- Intercepts generic Judgement (20271) and applies correct debuff name +--============================================================================ + +local JUDGEMENT_SPELL_ID = 20271 +local judgementHooked = false + +local function HookJudgementDetection() + if judgementHooked then return end + if not Cursive or not Cursive.curses then return end + + -- DO NOT register 20271 in trackedCurseIds - we handle it ourselves + -- This prevents Cursive's native handler from creating a duplicate + + -- Register our own UNIT_CASTEVENT handler + local eventFrame = CreateFrame("Frame") + eventFrame:RegisterEvent("UNIT_CASTEVENT") + eventFrame:SetScript("OnEvent", function() + local casterGuid, targetGuid, event, spellID = arg1, arg2, arg3, arg4 + + -- Only handle our Judgement casts + if event ~= "CAST" or spellID ~= JUDGEMENT_SPELL_ID then return end + + -- Check if it's the player casting + local _, playerGuid = UnitExists("player") + if casterGuid ~= playerGuid then return end + + -- Detect which seal is active and get full judgement info + local info = GetActiveSealJudgementInfo() + + -- Make sure the reference spellID is in trackedCurseIds + if not Cursive.curses.trackedCurseIds[info.spellID] then + Cursive.curses.trackedCurseIds[info.spellID] = { + name = info.name, + rank = 1, + duration = 10, + texture = info.texture, + } + end + + -- Apply the curse manually with the correct name + if not Cursive.curses.guids[targetGuid] then + Cursive.curses.guids[targetGuid] = {} + end + + Cursive.curses.guids[targetGuid][info.name] = { + rank = 1, + duration = 10, + start = GetTime(), + spellID = info.spellID, -- Use the seal-specific spellID + targetGuid = targetGuid, + currentPlayer = true, + } + + -- Add texture mapping for display + Cursive.curses.trackedCurseNamesToTextures[info.name] = info.texture + end) + + judgementHooked = true +end + +-- Hook judgement detection when Cursive is ready +local judgementRetryFrame = CreateFrame("Frame") +judgementRetryFrame:RegisterEvent("PLAYER_ENTERING_WORLD") +judgementRetryFrame:SetScript("OnEvent", function() + -- Delay slightly to ensure Cursive is fully loaded + local elapsed = 0 + judgementRetryFrame:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 + if elapsed >= 1 then + HookJudgementDetection() + judgementRetryFrame:SetScript("OnUpdate", nil) + end + end) +end) + +--============================================================================ +-- JUDGEMENT REFRESH ON MELEE HIT +-- Judgement of Light/Wisdom refresh their duration when the Paladin hits +--============================================================================ + +local judgementNames = { + ["judgement of wisdom"] = true, + ["judgement of light"] = true, + ["judgement of the crusader"] = true, + ["judgement of justice"] = true, + ["judgement of righteousness"] = true, + ["judgement of command"] = true, +} + +-- Refresh all active judgements on a target +local function RefreshJudgementsOnTarget(targetGuid) + if not Cursive or not Cursive.curses or not Cursive.curses.guids then return end + if not targetGuid or not Cursive.curses.guids[targetGuid] then return end + + local now = GetTime() + for curseName, curseData in pairs(Cursive.curses.guids[targetGuid]) do + if judgementNames[curseName] and curseData.currentPlayer then + -- Refresh the timer + curseData.start = now + end + end +end + +-- Hook melee combat events for auto attacks +local meleeRefreshFrame = CreateFrame("Frame") +meleeRefreshFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS") +meleeRefreshFrame:SetScript("OnEvent", function() + -- Auto attack hit patterns (vanilla WoW format) + -- "You hit TargetName for X damage." + -- "You crit TargetName for X damage." + local msg = arg1 + if not msg then return end + + -- Parse target name from combat message + local targetName = nil + + -- Pattern: "You hit TargetName for X" + local _, _, name = string.find(msg, "^You hit (.+) for") + if name then targetName = name end + + -- Pattern: "You crit TargetName for X" + if not targetName then + _, _, name = string.find(msg, "^You crit (.+) for") + if name then targetName = name end + end + + if not targetName then return end + + -- Check if current target matches the hit target + local currentTargetName = UnitName("target") + if currentTargetName and currentTargetName == targetName then + local _, targetGuid = UnitExists("target") + if targetGuid then + RefreshJudgementsOnTarget(targetGuid) + end + end +end) + +-- Hook UNIT_CASTEVENT for melee ability hits (Crusader Strike, etc.) +local meleeAbilityFrame = CreateFrame("Frame") +meleeAbilityFrame:RegisterEvent("UNIT_CASTEVENT") +meleeAbilityFrame:SetScript("OnEvent", function() + local casterGuid, targetGuid, event, spellID = arg1, arg2, arg3, arg4 + + -- Only handle CAST events (successful casts) + if event ~= "CAST" then return end + + -- Check if it's the player casting + local _, playerGuid = UnitExists("player") + if casterGuid ~= playerGuid then return end + + -- Skip Judgement itself (20271) - it applies, doesn't refresh + if spellID == JUDGEMENT_SPELL_ID then return end + + -- Only refresh for melee abilities (instant casts with target) + -- We can't easily detect "melee only" so refresh on any successful cast on target + if targetGuid and targetGuid ~= "" then + RefreshJudgementsOnTarget(targetGuid) + end +end) + +--============================================================================ +-- CONSOLE COMMANDS: /cleveroid cursive +--============================================================================ + +local originalConsoleHandler = CleveRoids.HandleConsoleCommand + +CleveRoids.HandleConsoleCommand = function(msg) + local cmd, args = string.match(msg or "", "^(%S+)%s*(.*)$") + cmd = cmd and string.lower(cmd) or "" + + if cmd == "cursive" then + local subcmd, subargs = string.match(args or "", "^(%S+)%s*(.*)$") + subcmd = subcmd and string.lower(subcmd) or "list" + + if subcmd == "list" then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Custom spell list:") + local count = 0 + for spellID, data in pairs(CleveRoids.CustomCursiveSpells) do + local status = "" + if Cursive and Cursive.curses and Cursive.curses.trackedCurseIds[spellID] then + status = " |cff00ff00[active]|r" + else + status = " |cffff0000[not loaded]|r" + end + DEFAULT_CHAT_FRAME:AddMessage(string.format(" [%d] %s (rank %d, %ds)%s", + spellID, data.name, data.rank or 1, data.duration, status)) + count = count + 1 + end + if count == 0 then + DEFAULT_CHAT_FRAME:AddMessage(" (none - edit CursiveCustomSpells.lua to add)") + end + DEFAULT_CHAT_FRAME:AddMessage(string.format(" Hook status: %s", + hooked and "|cff00ff00active|r" or "|cffff0000not hooked|r")) + + elseif subcmd == "add" then + local spellID, duration, customName = string.match(subargs, "^(%d+)%s+(%d+)%s*(.*)$") + spellID = tonumber(spellID) + duration = tonumber(duration) + + if not spellID or not duration then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /cleveroid cursive add [name]") + return + end + + local name, rank, texture = SpellInfo(spellID) + if not name then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not found.") + return + end + + local spellName = customName ~= "" and string.lower(customName) or string.lower(name) + + CleveRoids.CustomCursiveSpells[spellID] = { + name = spellName, + rank = 1, + duration = duration, + } + + -- Inject immediately + if Cursive and Cursive.curses then + Cursive.curses.trackedCurseIds[spellID] = { + name = spellName, + rank = 1, + duration = duration, + texture = texture, + } + Cursive.curses.trackedCurseNamesToTextures[spellName] = texture + end + + DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CursiveCustomSpells]|r Added: [%d] %s (%ds) - will auto-track on cast", + spellID, spellName, duration)) + DEFAULT_CHAT_FRAME:AddMessage(" Note: Edit CursiveCustomSpells.lua to make permanent.") + + elseif subcmd == "remove" then + local spellID = tonumber(subargs) + if not spellID then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /cleveroid cursive remove ") + return + end + + if CleveRoids.CustomCursiveSpells[spellID] then + local name = CleveRoids.CustomCursiveSpells[spellID].name + CleveRoids.CustomCursiveSpells[spellID] = nil + + if Cursive and Cursive.curses and Cursive.curses.trackedCurseIds[spellID] then + Cursive.curses.trackedCurseIds[spellID] = nil + end + + DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CursiveCustomSpells]|r Removed: [%d] %s", + spellID, name)) + else + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Error:|r Spell ID " .. spellID .. " not in custom list.") + end + + elseif subcmd == "inject" then + local count = InjectCustomSpells() + DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[CursiveCustomSpells]|r Injected %d spell(s).", count)) + + elseif subcmd == "debugseal" then + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Scanning player buffs for seals:") + for i = 1, 32 do + local texture = GetPlayerBuffTexture(i) + if not texture then break end + local match = sealToJudgement[texture] and "|cff00ff00 MATCH|r" or "" + DEFAULT_CHAT_FRAME:AddMessage(string.format(" [%d] %s%s", i, texture, match)) + end + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Known seal textures:") + for tex, data in pairs(sealToJudgement) do + DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s -> %s", tex, data.name)) + end + + else + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Commands:") + DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive list - Show custom spells and status") + DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive add [name]") + DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive remove ") + DEFAULT_CHAT_FRAME:AddMessage(" /cleveroid cursive inject - Force re-injection") + end + return + end + + if originalConsoleHandler then + originalConsoleHandler(msg) + end +end + +return ext diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 8ebd1b6..342cd60 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -15,7 +15,7 @@ - GetEquippedItems / GetEquippedItem - Equipment inspection - GetBagItems / GetBagItem - Bag contents inspection - GetCastInfo - Detailed cast/channel/GCD information - - GetSpellIdCooldown / GetItemIdCooldown - Detailed cooldown info + - GetSpellIdCooldown / GetItemIdCooldown - Detailed cooldown info with item metadata Trinket/Item Functions (v2.20+): - GetTrinkets - Enumerate equipped and bagged trinkets @@ -35,11 +35,15 @@ Utility Functions (v2.22+): - DisenchantAll - Auto-disenchant items by ID/name or quality + Cooldown Detail Tables (v2.23+): + - GetSpellIdCooldown/GetItemIdCooldown now include item metadata: + itemId, itemHasActiveSpell, itemActiveSpellId + Settings Integration: - Reads from NampowerSettings addon when available - Falls back to CVars when addon not present - Current version: v2.22.0 + Current version: v2.23.3 ]] local _G = _G or getfenv(0) @@ -53,17 +57,34 @@ end local API = CleveRoids.NampowerAPI -------------------------------------------------------------------------------- --- VERSION DETECTION +-- VERSION DETECTION AND FEATURE FLAGS -------------------------------------------------------------------------------- +-- Cached version info (set during initialization) +API._version = { major = 0, minor = 0, patch = 0, initialized = false } + -- Get Nampower version as major, minor, patch -- Returns 0, 0, 0 if Nampower not installed function API.GetVersion() + if API._version.initialized then + return API._version.major, API._version.minor, API._version.patch + end + if GetNampowerVersion then local major, minor, patch = GetNampowerVersion() - return major or 0, minor or 0, patch or 0 + API._version.major = major or 0 + API._version.minor = minor or 0 + API._version.patch = patch or 0 end - return 0, 0, 0 + API._version.initialized = true + + return API._version.major, API._version.minor, API._version.patch +end + +-- Get version as a single comparable number (e.g., 2.18.3 -> 2018003) +function API.GetVersionNumber() + local major, minor, patch = API.GetVersion() + return (major * 1000000) + (minor * 1000) + patch end -- Check if installed version meets minimum requirement @@ -75,70 +96,191 @@ function API.HasMinimumVersion(reqMajor, reqMinor, reqPatch) elseif major == reqMajor then if minor > reqMinor then return true - elseif minor == reqMinor and patch >= reqPatch then + elseif minor == reqMinor and patch >= (reqPatch or 0) then return true end end return false end --- Feature flags based on version -API.features = { - -- Base Nampower features (v2.0+) - hasNampower = (QueueSpellByName ~= nil), - hasSpellQueue = (QueueSpellByName ~= nil), - hasIsSpellInRange = (IsSpellInRange ~= nil), - hasGetCurrentCastingInfo = (GetCurrentCastingInfo ~= nil), - hasGetSpellIdForName = (GetSpellIdForName ~= nil), +-- Version requirements for each feature group +-- Format: { major, minor, patch, globalFunction (optional verification) } +API.VERSION_REQUIREMENTS = { + -- v2.0+ - Base spell queuing + ["SpellQueue"] = { 2, 0, 0, "QueueSpellByName" }, + ["GetCurrentCastingInfo"] = { 2, 0, 0, "GetCurrentCastingInfo" }, - -- Extended API (v2.8+) - hasGetSpellRec = (GetSpellRec ~= nil), - hasGetSpellRecField = (GetSpellRecField ~= nil), - hasGetItemStats = (GetItemStats ~= nil), - hasGetItemStatsField = (GetItemStatsField ~= nil), - hasGetUnitData = (GetUnitData ~= nil), - hasGetUnitField = (GetUnitField ~= nil), - hasGetSpellModifiers = (GetSpellModifiers ~= nil), + -- v2.8+ - DBC data access + ["GetSpellRec"] = { 2, 8, 0, "GetSpellRec" }, + ["GetSpellRecField"] = { 2, 8, 0, "GetSpellRecField" }, + ["GetItemStats"] = { 2, 8, 0, "GetItemStats" }, + ["GetItemStatsField"] = { 2, 8, 0, "GetItemStatsField" }, + ["GetUnitData"] = { 2, 8, 0, "GetUnitData" }, + ["GetUnitField"] = { 2, 8, 0, "GetUnitField" }, + ["GetSpellModifiers"] = { 2, 8, 0, "GetSpellModifiers" }, - -- Enhanced spell functions (accept name/"spellId:number") - hasEnhancedSpellFunctions = false, -- Detected at runtime + -- v2.11+ - GetNampowerVersion itself (meta-feature) + ["GetNampowerVersion"] = { 2, 11, 0, "GetNampowerVersion" }, - -- v2.18+ APIs (inventory/equipment/cast info) - hasFindPlayerItemSlot = (FindPlayerItemSlot ~= nil), - hasGetEquippedItems = (GetEquippedItems ~= nil), - hasGetEquippedItem = (GetEquippedItem ~= nil), - hasGetBagItems = (GetBagItems ~= nil), - hasGetBagItem = (GetBagItem ~= nil), - hasGetCastInfo = (GetCastInfo ~= nil), - hasGetSpellIdCooldown = (GetSpellIdCooldown ~= nil), - hasGetItemIdCooldown = (GetItemIdCooldown ~= nil), - hasChannelStopCastingNextTick = (ChannelStopCastingNextTick ~= nil), + -- v2.12+ - Enhanced spell functions and range/usable checks + ["EnhancedSpellFunctions"] = { 2, 12, 0 }, -- No single function to verify + ["IsSpellInRange"] = { 2, 12, 0, "IsSpellInRange" }, + ["IsSpellUsable"] = { 2, 12, 0, "IsSpellUsable" }, + ["GetSpellIdForName"] = { 2, 12, 0, "GetSpellIdForName" }, + ["GetSpellNameAndRankForId"]= { 2, 12, 0, "GetSpellNameAndRankForId" }, + ["GetSpellSlotTypeIdForName"]={ 2, 12, 0, "GetSpellSlotTypeIdForName" }, + ["CastSpellByNameNoQueue"] = { 2, 12, 0, "CastSpellByNameNoQueue" }, + ["QueueScript"] = { 2, 12, 0, "QueueScript" }, - -- v2.20+ APIs (trinkets, item usage, enhanced cooldowns) - hasGetTrinkets = (GetTrinkets ~= nil), - hasGetTrinketCooldown = (GetTrinketCooldown ~= nil), - hasUseTrinket = (UseTrinket ~= nil), - hasUseItemIdOrName = (UseItemIdOrName ~= nil), + -- v2.13+ - QueueSpellByName with target parameter + ["QueueSpellWithTarget"] = { 2, 13, 0 }, -- Same function, new parameter - -- v2.22+ APIs (utility functions) - hasDisenchantAll = (DisenchantAll ~= nil), + -- v2.14+ - GetItemLevel standalone function + ["GetItemLevel"] = { 2, 14, 0, "GetItemLevel" }, + + -- v2.18+ - Inventory, equipment, detailed cast/cooldown info + ["FindPlayerItemSlot"] = { 2, 18, 0, "FindPlayerItemSlot" }, + ["GetEquippedItems"] = { 2, 18, 0, "GetEquippedItems" }, + ["GetEquippedItem"] = { 2, 18, 0, "GetEquippedItem" }, + ["GetBagItems"] = { 2, 18, 0, "GetBagItems" }, + ["GetBagItem"] = { 2, 18, 0, "GetBagItem" }, + ["GetCastInfo"] = { 2, 18, 0, "GetCastInfo" }, + ["GetSpellIdCooldown"] = { 2, 18, 0, "GetSpellIdCooldown" }, + ["GetItemIdCooldown"] = { 2, 18, 0, "GetItemIdCooldown" }, + ["ChannelStopCastingNextTick"]={ 2, 18, 0, "ChannelStopCastingNextTick" }, + + -- v2.20+ - Trinket API, item usage, copy parameter support + ["GetTrinkets"] = { 2, 20, 0, "GetTrinkets" }, + ["GetTrinketCooldown"] = { 2, 20, 0, "GetTrinketCooldown" }, + ["UseTrinket"] = { 2, 20, 0, "UseTrinket" }, + ["UseItemIdOrName"] = { 2, 20, 0, "UseItemIdOrName" }, + ["CopyParameter"] = { 2, 20, 0 }, -- Table copy support + + -- v2.22+ - Utility functions + ["DisenchantAll"] = { 2, 22, 0, "DisenchantAll" }, + + -- v2.23+ - Enhanced bag queries, cooldown item metadata + ["GetBagItemsWithIndex"] = { 2, 23, 0 }, -- GetBagItems(bagIndex) parameter + ["CooldownItemMetadata"] = { 2, 23, 0 }, -- itemId, itemHasActiveSpell, itemActiveSpellId } --- Detect if enhanced spell functions are available (accept name/spellId:number) +-- Check if a specific feature is available +-- Uses both version check AND function existence verification +function API.HasFeature(featureName) + local req = API.VERSION_REQUIREMENTS[featureName] + if not req then + return false + end + + -- Check version requirement + if not API.HasMinimumVersion(req[1], req[2], req[3]) then + return false + end + + -- If a global function is specified, verify it exists + if req[4] then + return _G[req[4]] ~= nil + end + + return true +end + +-- Get the minimum version required for a feature +-- Returns major, minor, patch or nil if feature unknown +function API.GetFeatureVersion(featureName) + local req = API.VERSION_REQUIREMENTS[featureName] + if req then + return req[1], req[2], req[3] + end + return nil +end + +-- Feature flags (populated during initialization) +-- These provide quick boolean lookups after init +API.features = {} + +-- Initialize all feature flags based on version detection +local function InitializeFeatures() + local f = API.features + + -- Initialize version cache first + API.GetVersion() + + -- Base Nampower detection (v2.0+) + f.hasNampower = API.HasFeature("SpellQueue") + f.hasSpellQueue = f.hasNampower + f.hasGetCurrentCastingInfo = API.HasFeature("GetCurrentCastingInfo") + + -- v2.8+ DBC data access + f.hasGetSpellRec = API.HasFeature("GetSpellRec") + f.hasGetSpellRecField = API.HasFeature("GetSpellRecField") + f.hasGetItemStats = API.HasFeature("GetItemStats") + f.hasGetItemStatsField = API.HasFeature("GetItemStatsField") + f.hasGetUnitData = API.HasFeature("GetUnitData") + f.hasGetUnitField = API.HasFeature("GetUnitField") + f.hasGetSpellModifiers = API.HasFeature("GetSpellModifiers") + + -- v2.11+ GetNampowerVersion + f.hasGetNampowerVersion = API.HasFeature("GetNampowerVersion") + + -- v2.12+ Enhanced spell functions + f.hasEnhancedSpellFunctions = API.HasFeature("EnhancedSpellFunctions") + f.hasIsSpellInRange = API.HasFeature("IsSpellInRange") + f.hasIsSpellUsable = API.HasFeature("IsSpellUsable") + f.hasGetSpellIdForName = API.HasFeature("GetSpellIdForName") + f.hasGetSpellNameAndRankForId = API.HasFeature("GetSpellNameAndRankForId") + f.hasGetSpellSlotTypeIdForName = API.HasFeature("GetSpellSlotTypeIdForName") + f.hasCastSpellByNameNoQueue = API.HasFeature("CastSpellByNameNoQueue") + f.hasQueueScript = API.HasFeature("QueueScript") + + -- v2.13+ QueueSpellByName with target + f.hasQueueSpellWithTarget = API.HasFeature("QueueSpellWithTarget") + + -- v2.14+ GetItemLevel + f.hasGetItemLevel = API.HasFeature("GetItemLevel") + + -- v2.18+ Inventory/Equipment/Cast info + f.hasFindPlayerItemSlot = API.HasFeature("FindPlayerItemSlot") + f.hasGetEquippedItems = API.HasFeature("GetEquippedItems") + f.hasGetEquippedItem = API.HasFeature("GetEquippedItem") + f.hasGetBagItems = API.HasFeature("GetBagItems") + f.hasGetBagItem = API.HasFeature("GetBagItem") + f.hasGetCastInfo = API.HasFeature("GetCastInfo") + f.hasGetSpellIdCooldown = API.HasFeature("GetSpellIdCooldown") + f.hasGetItemIdCooldown = API.HasFeature("GetItemIdCooldown") + f.hasChannelStopCastingNextTick = API.HasFeature("ChannelStopCastingNextTick") + + -- v2.20+ Trinket/Item API + f.hasGetTrinkets = API.HasFeature("GetTrinkets") + f.hasGetTrinketCooldown = API.HasFeature("GetTrinketCooldown") + f.hasUseTrinket = API.HasFeature("UseTrinket") + f.hasUseItemIdOrName = API.HasFeature("UseItemIdOrName") + f.hasCopyParameter = API.HasFeature("CopyParameter") + + -- v2.22+ Utility functions + f.hasDisenchantAll = API.HasFeature("DisenchantAll") + + -- v2.23+ Enhanced features + f.hasGetBagItemsWithIndex = API.HasFeature("GetBagItemsWithIndex") + f.hasCooldownItemMetadata = API.HasFeature("CooldownItemMetadata") + + -- Runtime detection for enhanced spell functions (verify by testing) + if f.hasEnhancedSpellFunctions and GetSpellTexture then + local success, result = pcall(function() + return GetSpellTexture("Attack") + end) + f.hasEnhancedSpellFunctions = success and (result ~= nil) + end +end + +-- Legacy function for compatibility (now just calls InitializeFeatures) local function DetectEnhancedSpellFunctions() - if not GetSpellTexture then return false end - - -- Try calling GetSpellTexture with a spell name - -- If it doesn't error and returns something, enhanced functions are available - local success, result = pcall(function() - -- Use "Attack" which should exist for all characters - return GetSpellTexture("Attack") - end) - - API.features.hasEnhancedSpellFunctions = success and (result ~= nil) return API.features.hasEnhancedSpellFunctions end +-- Initialize features immediately on load +InitializeFeatures() + -------------------------------------------------------------------------------- -- NAMPOWER SETTINGS ACCESS -------------------------------------------------------------------------------- @@ -280,20 +422,28 @@ end API.spellRecCache = {} -- Get full spell record data --- Returns nil if spell not found or API unavailable +-- Returns nil if spell not found or API unavailable (requires v2.8+) -- Pass copy=1 to get an independent copy safe for storage (v2.20+) -- WARNING: Without copy, table references are reused - extract values immediately! function API.GetSpellRecord(spellId, copy) if not spellId or spellId == 0 then return nil end + -- Requires v2.8+ for GetSpellRec + if not API.features.hasGetSpellRec then + return nil + end + -- Check cache first (only for non-copy requests) if not copy and API.spellRecCache[spellId] then return API.spellRecCache[spellId] end - -- Use native function if available + -- Copy parameter requires v2.20+ + local useCopy = copy and API.features.hasCopyParameter and copy or nil + + -- Use native function if GetSpellRec then - local rec = GetSpellRec(spellId, copy) + local rec = GetSpellRec(spellId, useCopy) if rec then if not copy then API.spellRecCache[spellId] = rec @@ -306,19 +456,25 @@ function API.GetSpellRecord(spellId, copy) end -- Get a specific field from spell record --- Returns nil if not found, raises error if field name invalid (native behavior) +-- Returns nil if not found (requires v2.8+) -- Pass copy=1 to get an independent copy for array fields (v2.20+) function API.GetSpellField(spellId, fieldName, copy) if not spellId or spellId == 0 then return nil end - -- Use native function if available (more efficient for single field) + -- Requires v2.8+ for GetSpellRecField + if not API.features.hasGetSpellRecField then + return nil + end + + -- Copy parameter requires v2.20+ + local useCopy = copy and API.features.hasCopyParameter and copy or nil + + -- Use native function (more efficient for single field) if GetSpellRecField then - local success, result = pcall(GetSpellRecField, spellId, fieldName, copy) + local success, result = pcall(GetSpellRecField, spellId, fieldName, useCopy) if success then return result end - -- If it errors on invalid field name, let it propagate - -- But if spell just not found, return nil return nil end @@ -537,19 +693,28 @@ end API.itemStatsCache = {} -- Get full item stats data +-- Requires v2.8+ for GetItemStats -- Pass copy=1 to get an independent copy safe for storage (v2.20+) -- WARNING: Without copy, table references are reused - extract values immediately! function API.GetItemRecord(itemId, copy) if not itemId or itemId == 0 then return nil end + -- Requires v2.8+ for GetItemStats + if not API.features.hasGetItemStats then + return nil + end + -- Check cache first (only for non-copy requests) if not copy and API.itemStatsCache[itemId] then return API.itemStatsCache[itemId] end - -- Use native function if available (wrapped in pcall for safety) + -- Copy parameter requires v2.20+ + local useCopy = copy and API.features.hasCopyParameter and copy or nil + + -- Use native function if GetItemStats then - local ok, stats = pcall(GetItemStats, itemId, copy) + local ok, stats = pcall(GetItemStats, itemId, useCopy) if ok and stats then if not copy then API.itemStatsCache[itemId] = stats @@ -562,13 +727,22 @@ function API.GetItemRecord(itemId, copy) end -- Get a specific field from item stats +-- Requires v2.8+ for GetItemStatsField -- Pass copy=1 to get an independent copy for array fields (v2.20+) function API.GetItemField(itemId, fieldName, copy) if not itemId or itemId == 0 then return nil end - -- Use native function if available (more efficient) + -- Requires v2.8+ for GetItemStatsField + if not API.features.hasGetItemStatsField then + return nil + end + + -- Copy parameter requires v2.20+ + local useCopy = copy and API.features.hasCopyParameter and copy or nil + + -- Use native function (more efficient) if GetItemStatsField then - local success, result = pcall(GetItemStatsField, itemId, fieldName, copy) + local success, result = pcall(GetItemStatsField, itemId, fieldName, useCopy) if success then return result end @@ -585,13 +759,15 @@ function API.GetItemField(itemId, fieldName, copy) end -- Get item level +-- Uses standalone GetItemLevel (v2.14+) or falls back to GetItemStatsField (v2.8+) function API.GetItemLevel(itemId) - -- Use dedicated function if available - if GetItemLevel then + -- Use dedicated function if available (v2.14+) + if API.features.hasGetItemLevel and GetItemLevel then local success, result = pcall(GetItemLevel, itemId) if success then return result end end + -- Fallback to GetItemField (v2.8+) return API.GetItemField(itemId, "itemLevel") end @@ -671,30 +847,48 @@ function API.GetItemEquipSlot(itemId) end -------------------------------------------------------------------------------- --- UNIT DATA API (GetUnitData / GetUnitField) +-- UNIT DATA API (GetUnitData / GetUnitField) - Requires v2.8+ -------------------------------------------------------------------------------- -- Get full unit data (no caching - unit data changes frequently) +-- Requires v2.8+ for GetUnitData -- Pass copy=1 to get an independent copy safe for storage (v2.20+) -- WARNING: Without copy, table references are reused - extract values immediately! function API.GetUnitRecord(unitToken, copy) if not unitToken then return nil end + -- Requires v2.8+ for GetUnitData + if not API.features.hasGetUnitData then + return nil + end + + -- Copy parameter requires v2.20+ + local useCopy = copy and API.features.hasCopyParameter and copy or nil + if GetUnitData then - return GetUnitData(unitToken, copy) + return GetUnitData(unitToken, useCopy) end return nil end -- Get a specific unit field +-- Requires v2.8+ for GetUnitField -- Pass copy=1 to get an independent copy for array fields (v2.20+) function API.GetUnitFieldValue(unitToken, fieldName, copy) if not unitToken then return nil end - -- Use native function if available (more efficient) + -- Requires v2.8+ for GetUnitField + if not API.features.hasGetUnitField then + return nil + end + + -- Copy parameter requires v2.20+ + local useCopy = copy and API.features.hasCopyParameter and copy or nil + + -- Use native function (more efficient) if GetUnitField then - local success, result = pcall(GetUnitField, unitToken, fieldName, copy) + local success, result = pcall(GetUnitField, unitToken, fieldName, useCopy) if success then return result end @@ -750,6 +944,7 @@ end -------------------------------------------------------------------------------- -- Find an item in player's inventory by ID or name +-- Requires v2.18+ for native FindPlayerItemSlot (with Lua fallback) -- Returns: bagIndex, slot (see wiki for bag index meanings) -- bagIndex nil + slot = equipped item (slot is equipment slot 0-18) -- bagIndex + slot = item in bag @@ -758,7 +953,7 @@ function API.FindPlayerItemSlot(itemIdOrName) if not itemIdOrName then return nil, nil end -- Use native function if available (v2.18+) - if FindPlayerItemSlot then + if API.features.hasFindPlayerItemSlot and FindPlayerItemSlot then return FindPlayerItemSlot(itemIdOrName) end @@ -806,12 +1001,13 @@ function API.FindPlayerItemSlot(itemIdOrName) end -- Get all equipped items for a unit +-- Requires v2.18+ for native GetEquippedItems (with Lua fallback for player only) -- Returns table with slot indices (0-18) as keys, item info tables as values function API.GetEquippedItems(unitToken) unitToken = unitToken or "player" -- Use native function if available (v2.18+) - if GetEquippedItems then + if API.features.hasGetEquippedItems and GetEquippedItems then return GetEquippedItems(unitToken) end @@ -838,12 +1034,13 @@ function API.GetEquippedItems(unitToken) end -- Get equipped item info for a specific slot +-- Requires v2.18+ for native GetEquippedItem (with Lua fallback for player only) -- Slot numbers: 1=Head, 2=Neck, 3=Shoulder... 16=MainHand, 17=OffHand, 18=Ranged function API.GetEquippedItem(unitToken, slot) unitToken = unitToken or "player" -- Use native function if available (v2.18+) - if GetEquippedItem then + if API.features.hasGetEquippedItem and GetEquippedItem then return GetEquippedItem(unitToken, slot) end @@ -865,15 +1062,45 @@ function API.GetEquippedItem(unitToken, slot) return nil end --- Get all items in all bags --- Returns nested table: bagIndex -> { slot -> itemInfo } -function API.GetBagItems() +-- Get all items in bags, or items in a specific bag +-- Requires v2.18+ for native GetBagItems (with Lua fallback) +-- bagIndex parameter requires v2.23+ +-- If bagIndex is nil: returns nested table: bagIndex -> { slot -> itemInfo } +-- If bagIndex is specified: returns single table: slot -> itemInfo for that bag only +-- Bag indices: 0=backpack, 1-4=bags, -1=bank slots, 5-10=bank bags, -2=keyring +function API.GetBagItems(bagIndex) -- Use native function if available (v2.18+) - if GetBagItems then - return GetBagItems() + if API.features.hasGetBagItems and GetBagItems then + -- bagIndex parameter requires v2.23+ + if bagIndex ~= nil and not API.features.hasGetBagItemsWithIndex then + -- Fall through to Lua fallback for single-bag query on older versions + else + return GetBagItems(bagIndex) + end end -- Fallback: manual enumeration + if bagIndex ~= nil then + -- Single bag query + local bagContents = {} + local numSlots = GetContainerNumSlots(bagIndex) or 0 + for slot = 1, numSlots do + local link = GetContainerItemLink(bagIndex, slot) + if link then + local _, _, itemId = string.find(link, "item:(%d+)") + local _, count = GetContainerItemInfo(bagIndex, slot) + if itemId then + bagContents[slot] = { + itemId = tonumber(itemId), + stackCount = count or 1, + } + end + end + end + return bagContents + end + + -- All bags query local bags = {} for bag = 0, 4 do local numSlots = GetContainerNumSlots(bag) @@ -899,9 +1126,10 @@ function API.GetBagItems() end -- Get item info for a specific bag slot +-- Requires v2.18+ for native GetBagItem (with Lua fallback) function API.GetBagItem(bagIndex, slot) -- Use native function if available (v2.18+) - if GetBagItem then + if API.features.hasGetBagItem and GetBagItem then return GetBagItem(bagIndex, slot) end @@ -1171,12 +1399,13 @@ end -------------------------------------------------------------------------------- -- Get detailed information about current cast/channel +-- Requires v2.18+ for native GetCastInfo (with GetCurrentCastingInfo fallback for v2.0+) -- Returns table with: castId, spellId, guid, castType, castStartS, castEndS, -- castRemainingMs, castDurationMs, gcdEndS, gcdRemainingMs -- Returns nil if no active cast function API.GetCastInfo() -- Use native function if available (v2.18+) - if GetCastInfo then + if API.features.hasGetCastInfo and GetCastInfo then local ok, result = pcall(GetCastInfo) if ok then return result @@ -1184,8 +1413,8 @@ function API.GetCastInfo() -- If pcall failed, fall through to fallback end - -- Fallback: use GetCurrentCastingInfo (less detailed) - if GetCurrentCastingInfo then + -- Fallback: use GetCurrentCastingInfo (less detailed, v2.0+) + if API.features.hasGetCurrentCastingInfo and GetCurrentCastingInfo then local castId, visId, autoId, casting, channeling, onswing, autoattack = GetCurrentCastingInfo() if castId and castId > 0 then return { @@ -1228,15 +1457,17 @@ end -------------------------------------------------------------------------------- -- Get detailed cooldown information for a spell +-- Requires v2.18+ for native GetSpellIdCooldown (with GetSpellCooldown fallback) -- Returns table with: isOnCooldown, cooldownRemainingMs, -- individual cooldown: individualStartS, individualDurationMs, individualRemainingMs, isOnIndividualCooldown -- category cooldown: categoryId, categoryStartS, categoryDurationMs, categoryRemainingMs, isOnCategoryCooldown -- GCD: gcdCategoryId, gcdCategoryStartS, gcdCategoryDurationMs, gcdCategoryRemainingMs, isOnGcdCategoryCooldown +-- item metadata (v2.23+): itemId, itemHasActiveSpell, itemActiveSpellId function API.GetSpellCooldownInfo(spellId) if not spellId or spellId == 0 then return nil end -- Use native function if available (v2.18+) - if GetSpellIdCooldown then + if API.features.hasGetSpellIdCooldown and GetSpellIdCooldown then return GetSpellIdCooldown(spellId) end @@ -1259,11 +1490,14 @@ function API.GetSpellCooldownInfo(spellId) end -- Get detailed cooldown information for an item +-- Requires v2.18+ for native GetItemIdCooldown (with GetItemCooldown fallback) +-- Returns same table structure as GetSpellCooldownInfo +-- v2.23+ adds: itemId, itemHasActiveSpell, itemActiveSpellId function API.GetItemCooldownInfo(itemId) if not itemId or itemId == 0 then return nil end -- Use native function if available (v2.18+) - if GetItemIdCooldown then + if API.features.hasGetItemIdCooldown and GetItemIdCooldown then return GetItemIdCooldown(itemId) end @@ -1309,9 +1543,10 @@ end -------------------------------------------------------------------------------- -- Stop channeling early on the next tick +-- Requires v2.18+ for ChannelStopCastingNextTick -- Only works if NP_QueueChannelingSpells is enabled function API.StopChannelNextTick() - if ChannelStopCastingNextTick then + if API.features.hasChannelStopCastingNextTick and ChannelStopCastingNextTick then ChannelStopCastingNextTick() return true end @@ -1329,11 +1564,13 @@ API.TRINKET_SLOT = { } -- Get all trinkets (equipped and in bags) +-- Requires v2.20+ for native GetTrinkets (with Lua fallback) -- Returns table with: itemId, trinketName, texture, itemLevel, bagIndex (nil=equipped), slotIndex --- Pass copy=1 to get an independent copy safe for storage +-- Pass copy=1 to get an independent copy safe for storage (v2.20+) function API.GetTrinkets(copy) -- Use native function if available (v2.20+) - if GetTrinkets then + if API.features.hasGetTrinkets and GetTrinkets then + -- Copy parameter supported in v2.20+ return GetTrinkets(copy) end @@ -1397,13 +1634,14 @@ function API.GetTrinkets(copy) end -- Get cooldown for an equipped trinket +-- Requires v2.20+ for native GetTrinketCooldown (with Lua fallback) -- slot: 1 or 13 = first trinket, 2 or 14 = second trinket -- OR item ID (number) or item name (string) to match -- Returns cooldown detail table (same as GetSpellIdCooldown/GetItemIdCooldown) -- Returns -1 if no matching trinket is equipped function API.GetTrinketCooldown(slot) -- Use native function if available (v2.20+) - if GetTrinketCooldown then + if API.features.hasGetTrinketCooldown and GetTrinketCooldown then return GetTrinketCooldown(slot) end @@ -1462,13 +1700,14 @@ function API.GetTrinketCooldown(slot) end -- Use an equipped trinket +-- Requires v2.20+ for native UseTrinket (with Lua fallback) -- slot: 1 or 13 = first trinket, 2 or 14 = second trinket -- OR item ID (number) or item name (string) to match -- target: optional unit token or GUID -- Returns: 1 if used, 0 if use failed, -1 if trinket not found function API.UseTrinket(slot, target) -- Use native function if available (v2.20+) - if UseTrinket then + if API.features.hasUseTrinket and UseTrinket then return UseTrinket(slot, target) end @@ -1522,12 +1761,13 @@ end -------------------------------------------------------------------------------- -- Use an item by ID or name +-- Requires v2.20+ for native UseItemIdOrName (with Lua fallback) -- itemIdOrName: item ID (number) or item name (string) -- target: optional unit token or GUID -- Returns: 1 if used successfully, 0 if not found or use failed function API.UseItemIdOrName(itemIdOrName, target) -- Use native function if available (v2.20+) - if UseItemIdOrName then + if API.features.hasUseItemIdOrName and UseItemIdOrName then return UseItemIdOrName(itemIdOrName, target) end @@ -1544,8 +1784,9 @@ end -------------------------------------------------------------------------------- -- Automatically disenchant items in inventory +-- Requires v2.22+ for DisenchantAll (no fallback available) -- Mode 1: DisenchantAll(itemIdOrName, [includeSoulbound]) - specific item by ID/name --- Mode 2: DisenchantAll(quality, [includeSoulbound]) - "greens" or "blues" +-- Mode 2: DisenchantAll(quality, [includeSoulbound]) - "greens", "blues", or "greens|blues" (v2.23+) -- includeSoulbound: pass 1 to include soulbound items (default: 0) -- Returns: 1 if first disenchant succeeded, 0 if no items found or failed -- @@ -1554,7 +1795,7 @@ end -- Only affects bags 0-4 (not bank). function API.DisenchantAll(itemIdOrNameOrQuality, includeSoulbound) -- Use native function if available (v2.22+) - if DisenchantAll then + if API.features.hasDisenchantAll and DisenchantAll then return DisenchantAll(itemIdOrNameOrQuality, includeSoulbound) end @@ -1675,9 +1916,10 @@ API.MODIFIER_MULTIPLE_VALUE = 27 API.MODIFIER_RESIST_DISPEL_CHANCE = 28 -- Get spell modifiers +-- Requires v2.8+ for GetSpellModifiers -- Returns: flatMod, percentMod, hasModifier function API.GetModifiers(spellId, modifierType) - if not GetSpellModifiers then + if not API.features.hasGetSpellModifiers or not GetSpellModifiers then return 0, 0, false end @@ -2187,7 +2429,7 @@ end -- Parameters: -- spellName: The spell name (with optional rank) -- target: Optional target unit (for SuperWoW CastSpellByName) --- forceQueue: If true, always use QueueSpellByName (if available) +-- forceQueue: If true, always use QueueSpellByName (if available, v2.0+) -- forceNoQueue: If true, never use QueueSpellByName function API.SmartCast(spellName, target, forceQueue, forceNoQueue) if not spellName then return false end @@ -2208,10 +2450,10 @@ function API.SmartCast(spellName, target, forceQueue, forceNoQueue) -- QueueSpellByName in older Nampower versions doesn't support target parameter -- Only Nampower v2.13+ supports QueueSpellByName with target local hasNonStandardTarget = target and target ~= "target" - local canQueueWithTarget = API.HasMinimumVersion(2, 13, 0) + local canQueueWithTarget = API.features.hasQueueSpellWithTarget -- Cast the spell - if useQueue and QueueSpellByName then + if useQueue and API.features.hasSpellQueue and QueueSpellByName then if hasNonStandardTarget then if canQueueWithTarget then -- Nampower v2.13+ supports target parameter in QueueSpellByName @@ -2242,12 +2484,12 @@ function API.SmartCast(spellName, target, forceQueue, forceNoQueue) return false end --- Cast without queuing (uses CastSpellByNameNoQueue if available) +-- Cast without queuing (uses CastSpellByNameNoQueue if available, v2.12+) function API.CastNoQueue(spellName, target) if not spellName then return false end - -- Use Nampower's no-queue function if available - if CastSpellByNameNoQueue then + -- Use Nampower's no-queue function if available (v2.12+) + if API.features.hasCastSpellByNameNoQueue and CastSpellByNameNoQueue then CastSpellByNameNoQueue(spellName) return true end @@ -2265,11 +2507,11 @@ function API.CastNoQueue(spellName, target) return false end --- Force queue a spell (uses QueueSpellByName directly) +-- Force queue a spell (uses QueueSpellByName directly, v2.0+) function API.ForceQueue(spellName) if not spellName then return false end - if QueueSpellByName then + if API.features.hasSpellQueue and QueueSpellByName then QueueSpellByName(spellName) return true end @@ -2283,11 +2525,11 @@ function API.ForceQueue(spellName) return false end --- Queue a script to run after current cast (uses QueueScript if available) +-- Queue a script to run after current cast (uses QueueScript if available, v2.12+) function API.QueueScript(script, priority) if not script then return false end - if QueueScript then + if API.features.hasQueueScript and QueueScript then QueueScript(script, priority or 1) return true end diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index e6c297b..ece5fec 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -3,7 +3,7 @@ ## Title: [Super]CleveRoidMacros ## Notes: /cleveroid for settings ## Version: 1.8 -## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar +## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar, Cursive ## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools Localization.lua Init.lua @@ -34,4 +34,5 @@ Extensions\Mouseover\sRaidFrames.lua Extensions\Mouseover\ag_UnitFrames.lua Extensions\Tooltip\Generic.lua +Extensions\CursiveCustomSpells.lua