From b82d06d061e894479d94020a5705b3334dd9e4c5 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:28:24 -0500 Subject: [PATCH] Add [totem] and [nototem] conditionals A totem is a summoned creature, not an aura, so nothing in the addon could see one. Strength of Earth and Mana Spring could be faked through [nomybuff], but Searing, Magma, Fire Nova, Tremor and Grounding put nothing on the player at all, and those are the ones worth conditioning a recast on. There was no workaround. ClassicAPI backports the TBC totem bar (src/totem/Tracker.cpp), tracking the player's own summons from their SMSG_SPELL_GO and classifying them by the summon spell's SUMMON_TOTEM_SLOT1..4 effect, so Turtle's custom totems self-classify. Ungated like GLOBAL_MOUSE_*: the tracker landed in v1.10.0, far below the v1.15.0 floor the addon already refuses to run below. [totem:X] takes an element (fire/earth/water/air), a slot number, or the totem's own name -- ResolveTotemSlot tries the slot table first, then matches X against the name standing in each slot, so [totem:Searing_Totem] asks for that totem rather than whatever holds the fire slot. Bare [totem] takes the action as its argument the way [mybuff] does, making /cast [nototem] Searing Totem a complete recast macro. Comparisons follow the aura precedent exactly: an empty slot reads -1, the same "missing counts as least" ValidateAura uses, so [totem:Searing_Totem<5] passes while the totem is expiring AND while it is absent -- one clause, no [nototem] needed. A totem is either up or it isn't, so #N stack comparisons read 1 for a standing totem. The parser needed nothing: its name/operator/amount capture is generic, so multi-comparisons ([totem:X>2&<8]), OR groups and the De Morgan flip all came for free. Occupancy is read from GetTotemInfo's totemName, never from the time left. GetTotemInfo's first return is TOOL presence -- whether the player carries the slot's Earth/Fire/Water/Air Totem item -- not whether a totem is out, and a summon spell with no SpellDuration row reports 0 seconds, which is "up with no timer", not "absent". Icon refresh needs two feeds. PLAYER_TOTEM_UPDATE covers a slot changing hands (dropped, expired, killed, recalled). It does not fire as a totem counts toward zero, and unlike an aura there is no ambient event stream to ride -- UNIT_AURA is what quietly keeps [mybuff:X 0 then return true end + end + return false +end + +-- [totem:X] / [nototem:X], shaped like the aura validators: X is a plain name or +-- a parsed comparison entry, comparisons read seconds left, and `#N` stack +-- comparisons read 1 for a standing totem -- a totem is either up or it isn't. +-- +-- An empty slot reads -1 on both axes, the same "missing counts as least" the +-- aura path uses, so [totem:Searing_Totem<5] passes while the totem is expiring +-- AND while it is absent: one clause for the whole recast macro. +local function ValidateTotem(args) + if not args then return false end + + if type(args) ~= "table" then + args = { name = args } + end + + local remaining, stacks = -1, -1 + local slot = ResolveTotemSlot(args.name) + if slot then + -- Occupancy comes from the name, not from the time left: a totem whose + -- summon spell carries no SpellDuration row reads 0 seconds, and that is + -- "up with no timer", not "absent". + local _, totemName = GetTotemInfo(slot) + if totemName and totemName ~= "" then + stacks = 1 + remaining = GetTotemTimeLeft(slot) + end + end + + local ops = CleveRoids.operators + local cmp = CleveRoids.comparators + + -- Multi-comparison (e.g. >2&<8) - ALL must pass + if args.comparisons and type(args.comparisons) == "table" then + for _, comp in ipairs(args.comparisons) do + if not ops[comp.operator] then return false end + local value = comp.checkStacks and stacks or remaining + if not cmp[comp.operator](value, comp.amount) then return false end + end + return true + end + + if not args.amount and not args.operator and not args.checkStacks then + return stacks == 1 + elseif args.amount and ops[args.operator] then + return cmp[args.operator](args.checkStacks and stacks or remaining, args.amount) + else + return false + end +end + -- A list of Conditionals and their functions to validate them CleveRoids.Keywords = { -- [button:N] — true while mouse button N is held (1=Left, 2=Right, 3=Middle, @@ -5267,6 +5365,23 @@ CleveRoids.Keywords = { end, conditionals, "nobutton") end, + -- [totem:X] — X names a totem-bar slot (fire/earth/water/air, or 1-4) or the + -- totem itself ([totem:Searing_Totem]). Bare [totem] takes the action as its + -- argument, as [mybuff] does, so /cast [nototem] Searing Totem is the whole + -- recast macro. Time and stack comparisons follow the aura precedent; see + -- ValidateTotem. + totem = function(conditionals) + return Multi(conditionals.totem, function(v) + return ValidateTotem(v) + end, conditionals, "totem") + end, + + nototem = function(conditionals) + return NegatedMulti(conditionals.nototem, function(v) + return not ValidateTotem(v) + end, conditionals, "nototem") + end, + exists = function(conditionals) return UnitExists(conditionals.target) end, @@ -9200,6 +9315,7 @@ CleveRoids.STATIC_CONDITIONALS = { mod = true, nomod = true, keydown = true, nokeydown = true, button = true, nobutton = true, + totem = true, nototem = true, swimming = true, noswimming = true, swim = true, noswim = true, indoors = true, noindoors = true, outdoors = true, nooutdoors = true, rooted = true, norooted = true, diff --git a/Core.lua b/Core.lua index d5c1953..82b62a7 100644 --- a/Core.lua +++ b/Core.lua @@ -4190,6 +4190,21 @@ function CleveRoids.OnUpdate(self) end end + -- A totem's timer runs down with nothing behind it: PLAYER_TOTEM_UPDATE fires + -- when a slot changes hands, never as one counts toward zero, so a + -- [totem:X= (CR.nextTotemPoll or 0) then + CR.nextTotemPoll = time + 1 + if CR.AnyTotemTimerRunning() then + CR.isActionUpdateQueued = true + end + end + -- Check the saved variable to decide which update mode to use. if CRM.realtime == 1 then -- Realtime Mode: Force an update on every throttled tick for maximum responsiveness. @@ -4522,6 +4537,10 @@ end CleveRoids.Frame:RegisterEvent("GLOBAL_MOUSE_DOWN") CleveRoids.Frame:RegisterEvent("GLOBAL_MOUSE_UP") +-- ClassicAPI totem-bar tracking, for [totem:X] icon refresh. Ungated for the +-- same reason. +CleveRoids.Frame:RegisterEvent("PLAYER_TOTEM_UPDATE") + -- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua -- which has the complete implementation including the INTERCEPT path for all commands @@ -5589,6 +5608,14 @@ function CleveRoids.Frame:GLOBAL_MOUSE_UP() CleveRoids.isActionUpdateQueued = true end +-- ClassicAPI PLAYER_TOTEM_UPDATE: arg1 = the slot (1 Fire .. 4 Air) whose totem +-- was dropped, expired, killed or recalled. The slot goes unread for the same +-- reason the mouse handlers ignore their button -- a [totem:X] macro may name +-- any slot, and [nototem:X] flips on any of them. +function CleveRoids.Frame:PLAYER_TOTEM_UPDATE() + CleveRoids.QueueActionUpdate() +end + -- Base SendChatMessage captured at first hook; used to break a hook cycle. local baseSendChatMessage = SendChatMessage local sendingChatMessage = false