From 0a0cd055c4d181e817e6bbf7f97156054e4ee809 Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Sun, 1 Nov 2026 21:57:24 -0400 Subject: [PATCH] Swing Timer: color the main-hand bar for queued Heroic Strike, Cleave and Maul Ported from pfUI's swing timer. UNIT_SPELLCAST_SENT with C_Spell.IsNextMeleeSpell and the spell name sets the queue; it clears on UNIT_SPELLCAST_SUCCEEDED or _FAILED, on leaving combat, or when the ability's action button goes dark after being lit (Esc or a second press un-queues it without an event). Heroic Strike is light blue instead of pfUI's yellow, which would not stand out on the gold bar; Cleave green and Maul orange as in pfUI. Out-of-range gray still wins. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7 --- CHANGELOG.md | 1 + mods/swing-timer.lua | 126 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cda7c3..385c814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This fork is based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ ### Added - **Swing Timer** (Action Bar, off by default): main-hand and off-hand swing timers styled like the classic cast bar, with the weapon's icon on the left. They sit just above the cast bar; hold Ctrl+Shift+Alt to drag them, right-click to reset. A bar turns gray while the target is out of reach. +- **Swing Timer queue colors:** the main-hand bar shows a queued Heroic Strike in light blue and Cleave in green (warriors), and Maul in orange (druids). It goes back to gold when the swing goes off, is refused, or is cancelled. - **Wand Timer** (Action Bar, off by default): the time until your next wand shot in the same style. It also times Auto Shot for bows, guns and crossbows, and turns red during the last half second, when moving would cancel the shot. - Both are ported from brues-code/pfUI's swing timer and need ClassicAPI v1.15.10 or newer (`PLAYER_SWING`). diff --git a/mods/swing-timer.lua b/mods/swing-timer.lua index 8a66acc..e063af9 100644 --- a/mods/swing-timer.lua +++ b/mods/swing-timer.lua @@ -38,8 +38,15 @@ local COLORS = { ranged = { .3, .6, 1 }, far = { .5, .5, .5 }, -- target out of range windup = { 1, 0, 0 }, -- Auto Shot wind-up: don't move + -- queued on-next-swing abilities, on the main-hand bar + hs = { .4, .8, 1 }, -- Heroic Strike: light blue (gold would hide pfUI's yellow) + cleave = { .2, .9, .2 }, -- Cleave: green, as in pfUI + maul = { 1, .55, 0 }, -- Maul: orange, as in pfUI } +-- Rank 1 spell IDs; every rank shares the localized name. +local QUEUE_SPELLS = { hs = 78, cleave = 845, maul = 6807 } + local SLOTS = { mainhand = 16, offhand = 17, ranged = 18 } local ORDER = { "mainhand", "offhand", "ranged" } local PREVIEW = { mainhand = T["Main Hand"], offhand = T["Off Hand"], ranged = T["Ranged"] } @@ -51,8 +58,72 @@ local S = { hunter = false, preview = false, elapsed = 0, far = {}, bars = {}, + -- Heroic Strike / Cleave / Maul queue + queue = false, -- this class can queue one of them + queued = nil, -- "hs", "cleave", "maul" or nil + queuedSeen = false, -- its action button has been seen lit + queueNames = {}, -- localized name -> kind + queueSlots = {}, -- kind -> action slots holding it + nextMelee = {}, -- spellID -> kind or false } +-- Classify a spell ID as "hs", "cleave", "maul" or nil. +local function QueueKind(spellID) + if not spellID or spellID <= 0 then return end + local known = S.nextMelee[spellID] + if known ~= nil then return known or nil end + + local kind = false + if _G.C_Spell.IsNextMeleeSpell(spellID) then + kind = S.queueNames[_G.C_Spell.GetSpellName(spellID) or ""] or false + end + S.nextMelee[spellID] = kind + return kind or nil +end + +-- Find the action buttons that hold each ability. A queued ability lights +-- its button (IsCurrentAction); Esc or a second press un-queues it without +-- any event, so the lit buttons are how a cancel is noticed. +local function ScanQueueSlots() + S.queueSlots = { hs = {}, cleave = {}, maul = {} } + if not S.queue then return end + + for slot = 1, 120 do + local kind, id = GetActionInfo(slot) + local name + if kind == "spell" then + name = _G.C_Spell.GetSpellName(id) + elseif kind == "macro" then + name = GetMacroSpell(id) + end + local queueKind = name and S.queueNames[name] + if queueKind then table.insert(S.queueSlots[queueKind], slot) end + end +end + +local function ClearQueue() + S.queued = nil + S.queuedSeen = false +end + +-- Keep the queued color while a button of that ability is lit. Once seen +-- lit and then dark, it was cancelled. Never seen lit (cast from a macro or +-- keybind not on the bars): keep it until the spell goes off or fails. +local function ReconcileQueue() + if not S.queued then return end + local slots = S.queueSlots[S.queued] + if not slots then return end + + for i = 1, table.getn(slots) do + if IsCurrentAction(slots[i]) then + S.queuedSeen = true + return + end + end + + if S.queuedSeen then ClearQueue() end +end + local function Supported() if not _G.C_SwingTimer or not _G.Enum or not _G.Enum.PlayerSwingType then return end local utils = _G.C_EventUtils @@ -174,6 +245,10 @@ local function Tick(bar, delta, now) end local r, g, b = unpack(COLORS[bar.key]) + if bar.key == "mainhand" and S.queued then + ReconcileQueue() + if S.queued then r, g, b = unpack(COLORS[S.queued]) end + end if bar.key == "ranged" and S.hunter and bar.timer > 0 and bar.timer <= AUTOSHOT_WINDUP then r, g, b = unpack(COLORS.windup) end @@ -368,6 +443,26 @@ local function CreateCore() S.combat = true elseif event == "PLAYER_REGEN_ENABLED" then S.combat = false + ClearQueue() + + elseif event == "UNIT_SPELLCAST_SENT" then + -- arg1 unit, arg4 spellID: Heroic Strike, Cleave or Maul queued + if arg1 == "player" and S.queue then + local kind = QueueKind(arg4) + if kind then + S.queued = kind + S.queuedSeen = false + end + end + + elseif event == "UNIT_SPELLCAST_SUCCEEDED" or event == "UNIT_SPELLCAST_FAILED" then + -- arg1 unit, arg3 spellID: the queued swing went off or was refused + if arg1 == "player" and S.queued and QueueKind(arg3) then + ClearQueue() + end + + elseif event == "ACTIONBAR_SLOT_CHANGED" then + if S.queue then ScanQueueSlots() end elseif event == "PLAYER_EQUIPMENT_CHANGED" or event == "UNIT_INVENTORY_CHANGED" then if event == "PLAYER_EQUIPMENT_CHANGED" and arg1 ~= 16 and arg1 ~= 17 and arg1 ~= 18 then return end @@ -383,6 +478,7 @@ local function CreateCore() SetPreview(ModifiersDown() and true or false) elseif event == "PLAYER_DEAD" then + ClearQueue() ResetAll() elseif event == "PLAYER_ENTERING_WORLD" then @@ -430,11 +526,41 @@ local function EnableRangeCheck(bar) S.far[bar.swingType] = _G.C_SwingTimer.IsTargetWithinSwingRange(bar.swingType) == false end +-- Heroic Strike / Cleave (warriors) and Maul (druids) queue colors. +local function EnableQueueColors() + local spell = _G.C_Spell + if not spell or not spell.IsNextMeleeSpell or not spell.GetSpellName or not GetActionInfo then return end + + local _, class = UnitClass("player") + local kinds = class == "WARRIOR" and { "hs", "cleave" } or class == "DRUID" and { "maul" } + if not kinds then return end + + for i = 1, table.getn(kinds) do + local name = spell.GetSpellName(QUEUE_SPELLS[kinds[i]]) + if name then S.queueNames[name] = kinds[i] end + end + S.queue = true + ScanQueueSlots() + + -- player-only where ClassicAPI can filter: SUCCEEDED fires for every unit + local events = S.events + local unitEvents = { "UNIT_SPELLCAST_SENT", "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_FAILED" } + for i = 1, table.getn(unitEvents) do + if events.RegisterUnitEvent then + events:RegisterUnitEvent(unitEvents[i], "player") + else + events:RegisterEvent(unitEvents[i]) + end + end + events:RegisterEvent("ACTIONBAR_SLOT_CHANGED") +end + swing.enable = function(self) if not CreateCore() then return end S.melee = true EnableRangeCheck(S.bars.mainhand) EnableRangeCheck(S.bars.offhand) + EnableQueueColors() end wand.enable = function(self)