diff --git a/Compatibility/SuperMacro.lua b/Compatibility/SuperMacro.lua index 9198e3c..6b02d77 100644 --- a/Compatibility/SuperMacro.lua +++ b/Compatibility/SuperMacro.lua @@ -26,6 +26,8 @@ do -- pet petattack=true, petfollow=true, petpassive=true, petaggressive=true, petdefensive=true, petwait=true, + -- healing (requires QuickHeal addon) + quickheal=true, qh=true, } -- alias map (if CRM only registers /cancelaura, but /unbuff appears) diff --git a/Conditionals.lua b/Conditionals.lua index d1b49b2..8d35b66 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -127,6 +127,80 @@ local function NegatedMulti(t, func, conditionals, condition) end end +-- ============================================================================ +-- THREAT TRACKING (reads server data via CHAT_MSG_ADDON like TWThreat) +-- ============================================================================ + +-- Storage for threat data +CleveRoids.ThreatData = { + playerName = UnitName("player"), + threats = {}, -- [playerName] = { threat, perc, tank, melee } + lastUpdate = 0, +} + +-- Parse threat packet from server (same format as TWThreat) +-- Format: TWTv4=player1:tank:threat:perc:melee;player2:tank:threat:perc:melee;... +local function ParseThreatPacket(packet) + local threatApi = "TWTv4=" + local startPos = string.find(packet, threatApi, 1, true) + if not startPos then return end + + local playersString = string.sub(packet, startPos + string.len(threatApi)) + local playerName = CleveRoids.ThreatData.playerName + + -- Clear old data + CleveRoids.ThreatData.threats = {} + CleveRoids.ThreatData.lastUpdate = GetTime() + + -- Split by semicolon + for playerData in string.gfind(playersString, "[^;]+") do + -- Split by colon: player:tank:threat:perc:melee + local parts = {} + for part in string.gfind(playerData, "[^:]+") do + table.insert(parts, part) + end + + if parts[1] and parts[2] and parts[3] and parts[4] and parts[5] then + local name = parts[1] + local tank = parts[2] == "1" + local threat = tonumber(parts[3]) or 0 + local perc = tonumber(parts[4]) or 0 + local melee = parts[5] == "1" + + CleveRoids.ThreatData.threats[name] = { + threat = threat, + perc = perc, + tank = tank, + melee = melee, + } + end + end +end + +-- Get player's threat percentage +function CleveRoids.GetPlayerThreatPercent() + local playerName = CleveRoids.ThreatData.playerName + local data = CleveRoids.ThreatData.threats[playerName] + if data then + return data.perc + end + return nil +end + +-- Create frame to listen for threat addon messages +local threatFrame = CreateFrame("Frame", "CleveRoidsThreatFrame") +threatFrame:RegisterEvent("CHAT_MSG_ADDON") +threatFrame:SetScript("OnEvent", function() + if event == "CHAT_MSG_ADDON" then + -- arg1 = prefix, arg2 = message, arg3 = channel, arg4 = sender + if arg2 and string.find(arg2, "TWTv4=", 1, true) then + ParseThreatPacket(arg2) + end + end +end) + +-- ============================================================================ + -- pfUI debuff time helper (Vanilla 1.12.1 / Lua 5.0 safe) local function PFUI_HasLibDebuff() return type(pfUI) == "table" @@ -494,6 +568,91 @@ function CleveRoids.ValidateLevel(unit, operator, amount) return false end +--- Validates a threat percentage conditional using server threat data. +--- Usage: [threat:>80] [threat:<50] [threat:=100] +--- operator: Comparison operator (>, <, =, >=, <=, ~=) +--- amount: Threat percentage (0-100+, where 100 = will pull aggro) +--- returns: True if playerThreat [operator] amount +--- Note: Requires TWThreat addon to be running (sends threat requests to server) +function CleveRoids.ValidateThreat(operator, amount) + if not operator or not amount then return false end + + -- Get threat percentage from parsed server data + local threatpct = CleveRoids.GetPlayerThreatPercent() + + -- No threat data available + if threatpct == nil then + return false + end + + -- Compare threat percentage against threshold + if CleveRoids.operators[operator] then + return CleveRoids.comparators[operator](threatpct, amount) + end + + return false +end + +--- Validates a Time-To-Kill conditional using TimeToKill addon. +--- Usage: [ttk:<10] [ttk:>30] [ttk:=5] +--- operator: Comparison operator (>, <, =, >=, <=, ~=) +--- amount: Time in seconds until target death +--- returns: True if TTK [operator] amount +function CleveRoids.ValidateTTK(operator, amount) + if not operator or not amount then return false end + + -- Check if TimeToKill is loaded + if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTK) ~= "function" then + -- Only show error once per session + if not CleveRoids._ttkErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [ttk] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) + CleveRoids._ttkErrorShown = true + end + return false + end + + local ttk = TimeToKill.GetTTK() + if ttk == nil then + return false -- Not tracking TTK + end + + -- Compare TTK against threshold + if CleveRoids.operators[operator] then + return CleveRoids.comparators[operator](ttk, amount) + end + + return false +end + +--- Validates a Time-To-Execute conditional using TimeToKill addon. +--- Usage: [tte:<5] [tte:>10] +--- operator: Comparison operator (>, <, =, >=, <=, ~=) +--- amount: Time in seconds until target reaches 20% HP +--- returns: True if TTE [operator] amount +function CleveRoids.ValidateTTE(operator, amount) + if not operator or not amount then return false end + + -- Check if TimeToKill is loaded + if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTE) ~= "function" then + if not CleveRoids._ttkErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [tte] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) + CleveRoids._ttkErrorShown = true + end + return false + end + + local tte = TimeToKill.GetTTE() + if tte == nil then + return false -- Not tracking or already in execute phase + end + + if CleveRoids.operators[operator] then + return CleveRoids.comparators[operator](tte, amount) + end + + return false +end + function CleveRoids.ValidateKnown(args) if not args then return false @@ -3166,6 +3325,190 @@ CleveRoids.Keywords = { end, conditionals, "nostimer") end, + -- Threat percentage conditional (reads server data via CHAT_MSG_ADDON) + -- Usage: [threat:>80] - true if threat is above 80% + -- 100% = will pull aggro + -- Note: Requires TWThreat addon to request threat data from server + threat = function(conditionals) + return Multi(conditionals.threat, function(args) + if type(args) ~= "table" then return false end + + -- Handle multi-comparison (e.g., >50&<90) + if args.comparisons and type(args.comparisons) == "table" then + local threatpct = CleveRoids.GetPlayerThreatPercent() + if threatpct == nil 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](threatpct, comp.amount) then + return false + end + end + return true + end + + return CleveRoids.ValidateThreat(args.operator, args.amount) + end, conditionals, "threat") + end, + + -- Negated threat conditional + nothreat = function(conditionals) + return NegatedMulti(conditionals.nothreat, function(args) + if type(args) ~= "table" then return false end + + if args.comparisons and type(args.comparisons) == "table" then + local threatpct = CleveRoids.GetPlayerThreatPercent() + if threatpct == nil then return true end + + for _, comp in ipairs(args.comparisons) do + if not CleveRoids.operators[comp.operator] then + return true + end + if not CleveRoids.comparators[comp.operator](threatpct, comp.amount) then + return true + end + end + return false + end + + return not CleveRoids.ValidateThreat(args.operator, args.amount) + end, conditionals, "nothreat") + end, + + -- Time-To-Kill conditional (requires TimeToKill addon) + -- Usage: [ttk:<10] - true if target will die in less than 10 seconds + ttk = function(conditionals) + return Multi(conditionals.ttk, function(args) + if type(args) ~= "table" then return false end + + -- Handle multi-comparison (e.g., >5&<15) + if args.comparisons and type(args.comparisons) == "table" then + if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTK) ~= "function" then + if not CleveRoids._ttkErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [ttk] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) + CleveRoids._ttkErrorShown = true + end + return false + end + + local ttk = TimeToKill.GetTTK() + if ttk == nil then return false end + + for _, comp in ipairs(args.comparisons) do + if not CleveRoids.operators[comp.operator] then + return false + end + if not CleveRoids.comparators[comp.operator](ttk, comp.amount) then + return false + end + end + return true + end + + return CleveRoids.ValidateTTK(args.operator, args.amount) + end, conditionals, "ttk") + end, + + -- Negated TTK conditional + nottk = function(conditionals) + return NegatedMulti(conditionals.nottk, function(args) + if type(args) ~= "table" then return false end + + if args.comparisons and type(args.comparisons) == "table" then + if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTK) ~= "function" then + if not CleveRoids._ttkErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [ttk] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) + CleveRoids._ttkErrorShown = true + end + return true + end + + local ttk = TimeToKill.GetTTK() + if ttk == nil then return true end + + for _, comp in ipairs(args.comparisons) do + if not CleveRoids.operators[comp.operator] then + return true + end + if not CleveRoids.comparators[comp.operator](ttk, comp.amount) then + return true + end + end + return false + end + + return not CleveRoids.ValidateTTK(args.operator, args.amount) + end, conditionals, "nottk") + end, + + -- Time-To-Execute conditional (requires TimeToKill addon) + -- Usage: [tte:<5] - true if target will reach 20% HP in less than 5 seconds + tte = function(conditionals) + return Multi(conditionals.tte, function(args) + if type(args) ~= "table" then return false end + + if args.comparisons and type(args.comparisons) == "table" then + if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTE) ~= "function" then + if not CleveRoids._ttkErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [tte] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) + CleveRoids._ttkErrorShown = true + end + return false + end + + local tte = TimeToKill.GetTTE() + if tte == nil then return false end + + for _, comp in ipairs(args.comparisons) do + if not CleveRoids.operators[comp.operator] then + return false + end + if not CleveRoids.comparators[comp.operator](tte, comp.amount) then + return false + end + end + return true + end + + return CleveRoids.ValidateTTE(args.operator, args.amount) + end, conditionals, "tte") + end, + + -- Negated TTE conditional + notte = function(conditionals) + return NegatedMulti(conditionals.notte, function(args) + if type(args) ~= "table" then return false end + + if args.comparisons and type(args.comparisons) == "table" then + if type(TimeToKill) ~= "table" or type(TimeToKill.GetTTE) ~= "function" then + if not CleveRoids._ttkErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [tte] conditional requires the TimeToKill addon.", 1, 0.5, 0.5) + CleveRoids._ttkErrorShown = true + end + return true + end + + local tte = TimeToKill.GetTTE() + if tte == nil then return true end + + for _, comp in ipairs(args.comparisons) do + if not CleveRoids.operators[comp.operator] then + return true + end + if not CleveRoids.comparators[comp.operator](tte, comp.amount) then + return true + end + end + return false + end + + return not CleveRoids.ValidateTTE(args.operator, args.amount) + end, conditionals, "notte") + end, + -- Checks if the target uses a specific power type (mana, rage, energy) powertype = function(conditionals) local unit = conditionals.target or "target" diff --git a/Console.lua b/Console.lua index 7abba31..49bdb97 100644 --- a/Console.lua +++ b/Console.lua @@ -290,3 +290,87 @@ SlashCmdList.STOPMACRO = function(msg) CleveRoids.DoStopMacro(msg) end +-- QuickHeal with conditionals support (requires QuickHeal addon) +-- Usage: /quickheal [conditionals] [target] [type] +-- Examples: +-- /quickheal -- Smart heal (auto-select target) +-- /quickheal target -- Heal current target +-- /quickheal [combat] party -- Heal party member if in combat +-- /quickheal [mypower:>50] mt -- Heal tank if mana > 50% +-- /quickheal [threat:<80] hot -- Apply HoT if threat is low +SLASH_QUICKHEAL1 = "/quickheal" +SLASH_QUICKHEAL2 = "/qh" +SlashCmdList.QUICKHEAL = function(msg) + CleveRoids.DoQuickHeal(msg) +end + +--- Execute QuickHeal with optional conditionals +--- @param msg string The command arguments (conditionals + QuickHeal params) +function CleveRoids.DoQuickHeal(msg) + -- Check if QuickHeal addon is loaded + if type(QuickHeal) ~= "function" then + if not CleveRoids._quickHealErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The /quickheal command requires the QuickHeal addon.", 1, 0.5, 0.5) + CleveRoids._quickHealErrorShown = true + end + return + end + + msg = CleveRoids.Trim(msg or "") + + -- Check if there are conditionals + if string.find(msg, "^%[") then + -- Parse the conditionals and remaining args + local actions = CleveRoids.ParseMsg(msg) + + if not actions or table.getn(actions) == 0 then + -- No valid actions parsed, just run QuickHeal + QuickHeal() + return + end + + -- Find the first action whose conditionals pass + for i = 1, table.getn(actions) do + local action = actions[i] + if CleveRoids.TestAction(action) then + -- Conditionals passed - extract the target/type from action args + local healTarget = nil + local healType = nil + + if action.args then + -- Parse args - could be "target", "party", "mt", "hot", etc. + local args = CleveRoids.Trim(action.args) + if args ~= "" then + -- Check if it's a target or type keyword + local lowerArgs = string.lower(args) + if lowerArgs == "hot" or lowerArgs == "heal" or lowerArgs == "hs" or lowerArgs == "chainheal" then + healType = args + else + -- Assume it's a target specifier + healTarget = args + end + end + end + + -- Execute QuickHeal with parsed parameters + QuickHeal(healTarget, nil, nil, nil) + return + end + end + -- No conditions matched - don't heal + return + else + -- No conditionals, pass through to QuickHeal directly + -- Parse basic args: target and/or type + local args = msg + if args == "" then + QuickHeal() + else + -- QuickHeal accepts: Target, SpellID, extParam, forceMaxHPS + -- Common targets: player, target, targettarget, party, mt, nonmt, subgroup + -- Common types: heal, hot, hs (paladin), chainheal (shaman) + QuickHeal(args) + end + end +end + diff --git a/README.md b/README.md index e372c7e..427c0f5 100644 --- a/README.md +++ b/README.md @@ -459,6 +459,8 @@ Default `[noimmune]` checks the debuff school (bleed). | /use | * | Use item by name/ID | | /cast | * | Cast spell by name | | /stopmacro | * | Stop macro execution | +| /quickheal | * | Smart heal (QuickHeal) | +| /qh | * | Alias for /quickheal | --- @@ -525,8 +527,11 @@ Default `[noimmune]` checks the debuff school (bleed). | stance | [stance:1/2/3] | * | * | In stance # | | stat | [stat:str>=100] | * | | Player stat check | | stealth | [stealth] | | * | Stealth/Prowl | -| swingtimer | [swingtimer:<15] | * | * | Swing % elapsed | -| stimer | [stimer:>80] | * | * | Alias for swingtimer | +| swingtimer | [swingtimer:<15] | * | * | Swing % elapsed (SP_SwingTimer) | +| stimer | [stimer:>80] | * | * | Alias for swingtimer (SP_SwingTimer) | +| threat | [threat:>80] | * | * | Player threat % (TWThreat) | +| ttk | [ttk:<10] | * | * | Time to kill seconds (TimeToKill) | +| tte | [tte:<5] | * | * | Time to execute 20% HP (TimeToKill) | | swimming | [swimming] | | * | Aquatic form available | | usable | [usable:"Name"] | * | * | Spell/item usable | | zone | [zone:"Ironforge"] | * | * | In zone | @@ -636,7 +641,13 @@ agUnitFrames, Blizzard, CT_RaidAssist, CT_UnitFrames, DiscordUnitFrames, FocusFr Blizzard, Bongos, Discord Action Bars, pfUI ### Other -ClassicFocus/FocusFrame, SuperMacro, ShaguTweaks, SP_SwingTimer +ClassicFocus/FocusFrame, SuperMacro, ShaguTweaks + +### Optional Integrations +- [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer) - `[swingtimer]` conditional +- [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 --- @@ -684,6 +695,145 @@ For 3.5s weapon: --- +# Threat Integration + +Requires [TWThreat](https://github.com/MarcelineVQ/TWThreat). + +**How It Works:** +Checks your threat percentage on the current target. + +**Note:** TWThreat only provides threat data for **elite targets** when you are in a **party or raid**. + +**Threat Values:** +- 0-99% = Not tanking +- 100% = Tank/aggro holder +- 110% = Melee pull threshold +- 130% = Ranged pull threshold + +**Syntax:** +```lua +[threat:>80] -- Threat above 80% +[threat:<50] -- Threat below 50% +[nothreat:>X] -- NOT above X% +``` + +**Examples:** +```lua +-- Stop DPS when threat is high +#showtooltip Fireball +/cast [threat:<90] Fireball + +-- Fade when about to pull aggro +#showtooltip Fade +/cast [threat:>100] Fade + +-- Use threat reduction before pulling +#showtooltip Feign Death +/cast [threat:>105] Feign Death + +-- Tank: taunt when losing aggro +#showtooltip Taunt +/cast [threat:<100] Taunt +``` + +--- + +# Time-To-Kill Integration + +Requires [TimeToKill](https://github.com/jrc13245/TimeToKill). + +**How It Works:** +Uses RLS algorithm to predict when target will die or reach execute phase (20% HP). + +**Conditionals:** +- `[ttk:30] -- Target dies in more than 30 seconds +[tte:<5] -- Target reaches 20% HP in less than 5 seconds +[nottk:30] Recklessness + +-- Finish mob quickly if almost dead +#showtooltip +/cast [ttk:<5] Heroic Strike +/cast Bloodthirst + +-- Don't waste DoTs on dying targets +#showtooltip +/cast [ttk:>15] Corruption +/cast Shadow Bolt +``` + +--- + +# QuickHeal Integration + +Requires [QuickHeal](https://github.com/jrc13245/QuickHeal). + +**How It Works:** +Adds `/quickheal` (or `/qh`) command with full conditional support for smart healing. + +**Syntax:** +```lua +/quickheal -- Smart heal (auto-select target) +/quickheal target -- Heal current target +/quickheal [conditionals] X -- Heal X if conditionals pass +/qh -- Alias +``` + +**Target Options:** +- `player` - Self +- `target` - Current target +- `targettarget` - Target's target +- `party` / `subgroup` - Lowest health party member +- `mt` / `nonmt` - Main tanks or non-tanks (raid only) + +**Type Options:** +- `heal` - Regular heal (default) +- `hot` - Heal over time +- `hs` - Holy Shock (Paladin) +- `chainheal` - Chain Heal (Shaman) + +**Examples:** +```lua +-- Only heal if not in danger +#showtooltip +/quickheal [threat:<80] mt + +-- Emergency self-heal +#showtooltip +/quickheal [myhp:<30] player + +-- Smart group healing with mana check +#showtooltip +/quickheal [combat,mypower:>25] party + +-- HoT when low on mana +#showtooltip +/quickheal [mypower:<40] hot +/quickheal heal + +-- Heal tank if they're hurt +#showtooltip +/quickheal [combat] mt +``` + +--- + # HealComm Support This addon uses SuperWoW's CastSpellByName which is incompatible with standard HealComm-1.0.