diff --git a/Conditionals.lua b/Conditionals.lua index a397732..ea45209 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -1336,6 +1336,99 @@ function CleveRoids.ValidateTTE(operator, amount) return false end +-- ============================================================================ +-- CURSIVE ADDON INTEGRATION +-- ============================================================================ +-- Integrates with Cursive addon for accurate debuff time tracking +-- Cursive tracks debuffs by GUID with precise timing (accounts for Dark Harvest, etc.) + +--- Check if Cursive addon is available and enabled +--- @return boolean True if Cursive is available +function CleveRoids.HasCursive() + return type(Cursive) == "table" and + type(Cursive.curses) == "table" and + type(Cursive.curses.HasCurse) == "function" +end + +--- Require Cursive for a feature, warn once if missing +--- @param feature string Feature name for warning message +--- @return boolean True if Cursive is available +function CleveRoids.RequireCursive(feature) + if CleveRoids.HasCursive() then + return true + end + if not CleveRoids._cursiveErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [cursive] conditional requires the Cursive addon.", 1, 0.5, 0.5) + CleveRoids._cursiveErrorShown = true + end + return false +end + +--- Get time remaining on a Cursive-tracked debuff +--- @param unit string Unit to check (will be converted to GUID) +--- @param spellName string Spell name to check (will be lowercased and rank-stripped) +--- @return number|nil Time remaining in seconds, or nil if not found +function CleveRoids.GetCursiveTimeRemaining(unit, spellName) + if not CleveRoids.HasCursive() then return nil end + if not unit or not UnitExists(unit) then return nil end + + local _, guid = UnitExists(unit) + if not guid then return nil end + + -- Normalize spell name (lowercase, no rank) to match Cursive's format + local lowercaseName = Cursive.utils.GetLowercaseSpellNameNoRank(spellName) + + local curseData = Cursive.curses:GetCurseData(lowercaseName, guid) + if not curseData then return nil end + + return Cursive.curses:TimeRemaining(curseData) +end + +--- Check if unit has a Cursive-tracked debuff with optional time comparison +--- @param unit string Unit to check +--- @param spellName string Spell name to check +--- @param operator string|nil Comparison operator (>, <, =, >=, <=, ~=) +--- @param amount number|nil Time threshold in seconds +--- @return boolean True if debuff exists and passes time check +function CleveRoids.ValidateCursiveDebuff(unit, spellName, operator, amount) + if not CleveRoids.HasCursive() then return false end + if not unit or not UnitExists(unit) then return false end + + local _, guid = UnitExists(unit) + if not guid then return false end + + -- Normalize spell name for Cursive lookup + local lowercaseName = Cursive.utils.GetLowercaseSpellNameNoRank(spellName) + + -- If no operator, just check if debuff exists with any time remaining + if not operator then + return Cursive.curses:HasCurse(lowercaseName, guid, 0) == true + end + + -- With operator, check time remaining + local timeRemaining = CleveRoids.GetCursiveTimeRemaining(unit, spellName) + if not timeRemaining then return false end + + if CleveRoids.operators[operator] and amount then + return CleveRoids.comparators[operator](timeRemaining, amount) + end + + return false +end + +--- Check if ANY Cursive-tracked debuff exists on unit +--- @param unit string Unit to check +--- @return boolean True if unit has any tracked debuffs +function CleveRoids.HasAnyCursiveDebuff(unit) + if not CleveRoids.HasCursive() then return false end + if not unit or not UnitExists(unit) then return false end + + local _, guid = UnitExists(unit) + if not guid then return false end + + return Cursive.curses:HasAnyCurse(guid) == true +end + function CleveRoids.ValidateKnown(args) if not args then return false @@ -4599,6 +4692,118 @@ CleveRoids.Keywords = { end, conditionals, "notte") end, + -- ========================================================================= + -- CURSIVE ADDON INTEGRATION + -- ========================================================================= + -- Checks debuffs using Cursive's GUID-based tracking for accurate time remaining + -- Supports time comparisons: [cursive:Rake>3] [cursive:Rip<5] + -- Works with multiscan for intelligent target selection + + -- [cursive] - Check if target has ANY Cursive-tracked debuff + -- [cursive:SpellName] - Check if target has specific debuff tracked by Cursive + -- [cursive:SpellName>3] - Check if debuff has more than 3 seconds remaining + -- [cursive:SpellName<5] - Check if debuff has less than 5 seconds remaining + -- Examples: [cursive:Rake] [@focus,cursive:Rip>5] [cursive:Corruption<3] + cursive = function(conditionals) + -- Check if Cursive addon is available + if not CleveRoids.RequireCursive("cursive") then + return false + end + + local target = conditionals.target or "target" + + -- Boolean form [cursive] - check if target has ANY tracked debuff + if not conditionals.cursive or + conditionals.cursive == true or + (type(conditionals.cursive) == "table" and table.getn(conditionals.cursive) == 0) then + return CleveRoids.HasAnyCursiveDebuff(target) + end + + -- Spell name form with optional time comparison + return Multi(conditionals.cursive, function(args) + if type(args) == "string" then + -- Simple spell name check: [cursive:Rake] + return CleveRoids.ValidateCursiveDebuff(target, args, nil, nil) + elseif type(args) == "table" then + -- Time comparison: [cursive:Rake>3] or multi-comparison [cursive:Rake>3&<10] + local spellName = args.name + + -- Handle multi-comparison (e.g., >3&<10) + if args.comparisons and type(args.comparisons) == "table" then + local timeRemaining = CleveRoids.GetCursiveTimeRemaining(target, spellName) + if not timeRemaining then return false end + + -- ALL comparisons must pass (AND logic) + for _, comp in ipairs(args.comparisons) do + if not CleveRoids.operators[comp.operator] then + return false + end + if not CleveRoids.comparators[comp.operator](timeRemaining, comp.amount) then + return false + end + end + return true + end + + -- Single comparison: [cursive:Rake>3] + return CleveRoids.ValidateCursiveDebuff(target, spellName, args.operator, args.amount) + end + return false + end, conditionals, "cursive") + end, + + -- [nocursive] - Check if target does NOT have any Cursive-tracked debuff + -- [nocursive:SpellName] - Check if target does NOT have specific debuff + -- [nocursive:SpellName>3] - Check if debuff does NOT exist with >3 seconds remaining + -- (true if missing OR has <=3 seconds) + nocursive = function(conditionals) + -- Check if Cursive addon is available (return true if missing = treat as "no debuff") + if not CleveRoids.HasCursive() then + return true + end + + local target = conditionals.target or "target" + + -- Boolean form [nocursive] - check if target has NO tracked debuffs + if not conditionals.nocursive or + conditionals.nocursive == true or + (type(conditionals.nocursive) == "table" and table.getn(conditionals.nocursive) == 0) then + return not CleveRoids.HasAnyCursiveDebuff(target) + end + + -- Negated spell name form + return NegatedMulti(conditionals.nocursive, function(args) + if type(args) == "string" then + -- Simple spell name check: [nocursive:Rake] = true if Rake is missing + return not CleveRoids.ValidateCursiveDebuff(target, args, nil, nil) + elseif type(args) == "table" then + local spellName = args.name + + -- Handle multi-comparison negation + if args.comparisons and type(args.comparisons) == "table" then + local timeRemaining = CleveRoids.GetCursiveTimeRemaining(target, spellName) + -- If debuff missing, negation passes + if not timeRemaining then return true end + + -- Negated: true if ANY comparison fails + for _, comp in ipairs(args.comparisons) do + if not CleveRoids.operators[comp.operator] then + return true + end + if not CleveRoids.comparators[comp.operator](timeRemaining, comp.amount) then + return true + end + end + return false + end + + -- Single comparison negation + return not CleveRoids.ValidateCursiveDebuff(target, spellName, args.operator, args.amount) + end + return true + end, conditionals, "nocursive") + end, + -- Slam clip window conditionals for Warrior Slam rotation optimization -- Based on math: MaxSlamPercent = (SwingTimer - SlamCastTime) / SwingTimer * 100 -- Requires SP_SwingTimer addon and Nampower for cast time lookup diff --git a/Console.lua b/Console.lua index ea6e8c6..b5c0ff9 100644 --- a/Console.lua +++ b/Console.lua @@ -51,6 +51,18 @@ 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 dfe41dd..6563729 100644 --- a/Core.lua +++ b/Core.lua @@ -2794,9 +2794,9 @@ function CleveRoids.DoUse(msg) return false end -function CleveRoids.EquipBagItem(msg, offhand) +function CleveRoids.EquipBagItem(msg, slotOrOffhand) if CleveRoids.equipDebugLog then - CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' offhand=" .. tostring(offhand) .. "|r") + CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' slot=" .. tostring(slotOrOffhand) .. "|r") end if CleveRoids.equipInProgress then @@ -2806,7 +2806,13 @@ function CleveRoids.EquipBagItem(msg, offhand) return false end - local invslot = offhand and 17 or 16 + -- Accept slot number directly, or boolean for backward compatibility (false=16/MH, true=17/OH) + local invslot + if type(slotOrOffhand) == "number" then + invslot = slotOrOffhand + else + invslot = slotOrOffhand and 17 or 16 + end local API = CleveRoids.NampowerAPI -- v2.18+: Use native fast lookup for item ID or name @@ -2919,7 +2925,38 @@ function CleveRoids.EquipBagItem(msg, offhand) end end - -- PERFORMANCE: Try EquipItemByName FIRST before any lookups + -- Check if item is already equipped in the paired slot (swap case) + -- EquipItemByName doesn't handle swapping equipped items, so we must do it manually + -- Paired slots: trinkets (13<->14), weapons (16<->17), rings (11<->12) + local pairedSlots = {[13] = 14, [14] = 13, [16] = 17, [17] = 16, [11] = 12, [12] = 11} + local checkSlot = pairedSlots[invslot] + if checkSlot then + local link = GetInventoryItemLink("player", checkSlot) + if link then + local _, _, slotItemName = string_find(link, "|h%[(.-)%]|h") + if slotItemName and string_lower(slotItemName) == string_lower(msg) then + -- Found item in paired slot - swap it manually + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ffff[EquipLog] Swapping from slot " .. checkSlot .. " to slot " .. invslot .. "|r") + end + ClearCursor() + PickupInventoryItem(checkSlot) + if CursorHasItem and CursorHasItem() then + EquipCursorItem(invslot) + ClearCursor() + if CleveRoids.Items then + CleveRoids.Items[msg] = nil + CleveRoids.Items[string_lower(msg)] = nil + end + InvalidateDisplacedItem() + return true + end + ClearCursor() + end + end + end + + -- PERFORMANCE: Try EquipItemByName for bag items (fast path) -- This is the fastest path - no item lookup, no cursor operations if EquipItemByName then local ok = pcall(EquipItemByName, msg, invslot) @@ -3019,6 +3056,22 @@ local function _equipOffhandAction(msg) return CleveRoids.EquipBagItem(msg, true) end +local function _equipTrinket1Action(msg) + return CleveRoids.EquipBagItem(msg, 13) +end + +local function _equipTrinket2Action(msg) + return CleveRoids.EquipBagItem(msg, 14) +end + +local function _equipRing1Action(msg) + return CleveRoids.EquipBagItem(msg, 11) +end + +local function _equipRing2Action(msg) + return CleveRoids.EquipBagItem(msg, 12) +end + local function _unshiftAction() local currentShapeshiftIndex = CleveRoids.GetCurrentShapeshiftIndex() if currentShapeshiftIndex ~= 0 then @@ -3050,6 +3103,50 @@ function CleveRoids.DoEquipOffhand(msg) return false end +function CleveRoids.DoEquipTrinket1(msg) + local parts = CleveRoids.splitStringIgnoringQuotes(msg) + for i = 1, table.getn(parts) do + local v = string.gsub(parts[i], "^%?", "") + if CleveRoids.DoWithConditionals(v, _equipTrinket1Action, CleveRoids.FixEmptyTarget, false, _equipTrinket1Action) then + return true + end + end + return false +end + +function CleveRoids.DoEquipTrinket2(msg) + local parts = CleveRoids.splitStringIgnoringQuotes(msg) + for i = 1, table.getn(parts) do + local v = string.gsub(parts[i], "^%?", "") + if CleveRoids.DoWithConditionals(v, _equipTrinket2Action, CleveRoids.FixEmptyTarget, false, _equipTrinket2Action) then + return true + end + end + return false +end + +function CleveRoids.DoEquipRing1(msg) + local parts = CleveRoids.splitStringIgnoringQuotes(msg) + for i = 1, table.getn(parts) do + local v = string.gsub(parts[i], "^%?", "") + if CleveRoids.DoWithConditionals(v, _equipRing1Action, CleveRoids.FixEmptyTarget, false, _equipRing1Action) then + return true + end + end + return false +end + +function CleveRoids.DoEquipRing2(msg) + local parts = CleveRoids.splitStringIgnoringQuotes(msg) + for i = 1, table.getn(parts) do + local v = string.gsub(parts[i], "^%?", "") + if CleveRoids.DoWithConditionals(v, _equipRing2Action, CleveRoids.FixEmptyTarget, false, _equipRing2Action) then + return true + end + end + return false +end + function CleveRoids.DoUnshift(msg) local handled -- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation diff --git a/Init.lua b/Init.lua index 9558ffd..3c90014 100644 --- a/Init.lua +++ b/Init.lua @@ -112,6 +112,10 @@ 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 cabaf06..4ece918 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,10 @@ 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 | @@ -830,6 +834,7 @@ ClassicFocus/FocusFrame, SuperMacro, ShaguTweaks - [TWThreat](https://github.com/MarcelineVQ/TWThreat) - `[threat]` conditional - [TimeToKill](https://github.com/jrc13245/TimeToKill) - `[ttk]` and `[tte]` conditionals - [QuickHeal](https://github.com/jrc13245/QuickHeal) - `/quickheal` command +- [Cursive](https://github.com/pepopo978/Cursive) - `[cursive]` conditional for GUID-based debuff tracking --- @@ -1042,6 +1047,56 @@ Uses RLS algorithm to predict when target will die or reach execute phase (20% H --- +# Cursive Integration + +Requires [Cursive](https://github.com/pepopo978/Cursive). + +**How It Works:** +Uses Cursive's GUID-based debuff tracking for accurate time remaining on your DoTs. Unlike the standard `[debuff]` conditional, Cursive tracks by GUID so timers survive target switching, accounts for pending casts, and handles Dark Harvest reductions. + +**Conditionals:** +- `[cursive]` - Target has ANY Cursive-tracked debuff +- `[cursive:Spell]` - Target has specific debuff +- `[cursive:Spell>N]` - Debuff has more than N seconds remaining +- `[cursive:Spell3] -- Rake has more than 3 seconds remaining +[cursive:Rake<5] -- Rake has less than 5 seconds remaining +[nocursive:Rip] -- Target missing Rip +[@focus,cursive:DoT] -- Focus target has DoT +``` + +**Examples:** +```lua +-- Refresh Rake only when low on duration +#showtooltip Rake +/cast [cursive:Rake<3] Rake + +-- Apply Rip only if missing (Cursive knows about pending casts) +#showtooltip Rip +/cast [nocursive:Rip] Rip + +-- DoT spreading with multiscan +#showtooltip Rake +/cast [multiscan:nearest,nocursive:Rake] Rake + +-- Focus target DoT management +#showtooltip Corruption +/cast [@focus,cursive:Corruption<4] Corruption +``` + +**Why use Cursive over debuff?** +- **GUID-based**: Tracks by unit GUID, not unit token - survives target switching +- **Pending cast aware**: Knows about spells in flight before they hit +- **Accurate timing**: Accounts for Dark Harvest, latency compensation +- **Works at debuff cap**: Pre-registers expected debuffs + +--- + # QuickHeal Integration Requires [QuickHeal](https://github.com/jrc13245/QuickHeal). @@ -1115,6 +1170,9 @@ Use MarcelineVQ's updated [LunaUnitFrames](https://github.com/MarcelineVQ/LunaUn - [Roid-Macros](https://github.com/DennisWG/Roid-Macros) by DennisWG (DWG) - [CleverMacro](https://github.com/DanielAdolfsson/CleverMacro) by DanielAdolfsson (_brain) +## Contributors +- **Avitasia** - Cursive integration + ## License MIT