mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 07:36:56 +00:00
61c2f996fa
Pressing Esc (or re-pressing) to cancel a queued Heroic Strike / Cleave / Maul left the swing bar stuck in its queued color. The color is set on the on-swing press and only cleared on nampower's ON_SWING_QUEUE_POPPED, which fires solely when a queued-behind on-swing resolves; nampower's cancel path touches no on-swing state and emits no event, so the flag never cleared. Reconcile the event flag against the client's IsCurrentAction, which it does clear on cancel: ReconcileQueued drops the flag once the client has confirmed the ability as current and then stops showing it (the true->false transition). It only acts once current has been seen, so a nampower-initiated cast the client never flags as current keeps its color until its own pop/resolve -- preserving the reason the event-driven path exists. RebuildQueueSlotCache now caches Maul slots and runs for druids, and no longer bails in event mode so the caches stay fresh for reconciliation.
994 lines
42 KiB
Lua
994 lines
42 KiB
Lua
pfUI:RegisterNewModule("swingtimer", "Swing Timer")
|
|
pfUI:RegisterModule("swingtimer", function ()
|
|
local rawborder, border = GetBorderSize()
|
|
|
|
-- HitInfo flags (EVENTS.md)
|
|
local HITINFO_LEFTSWING = 4 -- 4: Off-hand attack
|
|
local HITINFO_NOACTION = 65536 -- 65536: server did not advance the swing clock
|
|
|
|
-- SPELL_QUEUE_EVENT codes (EVENTS.md)
|
|
local ON_SWING_QUEUED = 0
|
|
local ON_SWING_QUEUE_POPPED = 1
|
|
|
|
-- Spell.dbc bits used to mirror server-side swing-reset rules.
|
|
local FLAG_AUTOATTACK = tonumber("0x08", 16) -- SPELL_INTERRUPT_FLAG_AUTOATTACK
|
|
local ATTR_KEEP_SWINGS = tonumber("0x20000", 16) -- SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS
|
|
local ATTR_ON_NEXT_SWING = tonumber("0x04", 16) -- SPELL_ATTR_ON_NEXT_SWING
|
|
|
|
-- Consolidate state into a table to avoid Lua 5.0 upvalue limit (32 max)
|
|
local S = {
|
|
mhTimer = 0, mhTimerMax = 1,
|
|
ohTimer = 0, ohTimerMax = 1,
|
|
raTimer = 0, raTimerMax = 1,
|
|
mhSpeed = 0, ohSpeed = 0, raSpeed = 0,
|
|
mhActive = false, ohActive = false, raActive = false,
|
|
lastMhMarkerX = -1, lastOhMarkerX = -1, lastRaMarkerX = -1,
|
|
autoAttackActive = false,
|
|
inCombat = false,
|
|
pendingCastSpellId = nil,
|
|
mhFrozenAt = nil,
|
|
hsQueued = false, cleaveQueued = false, maulQueued = false,
|
|
hsSeenCurrent = false, cleaveSeenCurrent = false, maulSeenCurrent = false,
|
|
isWarrior = false,
|
|
isDruid = false,
|
|
cachedHSSlots = {}, cachedCleaveSlots = {}, cachedMaulSlots = {},
|
|
useSpellQueueEvent = false,
|
|
swingThrottle = 0,
|
|
onSwingCache = {},
|
|
}
|
|
|
|
-- Wand "Shoot" runs on the ranged bar but is INDEPENDENT of the melee
|
|
-- swing clock — casters melee-weave between mainhand swings and wand
|
|
-- fires, both timers tick concurrently. Every other ranged-auto-attack
|
|
-- (Hunter Auto Shot, any future auto-repeat ranged spell) replaces MH.
|
|
local WAND_SHOOT_SPELLID = 5019
|
|
local THROW_SPELLID = 2764 -- one-shot ranged, not auto-repeat
|
|
|
|
-- ATTR_ON_NEXT_SWING: spell replaces next auto-attack swing.
|
|
-- Covers Raptor Strike, Maul, Mongoose Bite, Holy Strike, etc. automatically.
|
|
local function IsOnSwingSpell(spellId)
|
|
if S.onSwingCache[spellId] ~= nil then return S.onSwingCache[spellId] end
|
|
local attr = GetSpellRecField(spellId, "attributes") or 0
|
|
local result = bit.band(attr, ATTR_ON_NEXT_SWING) ~= 0
|
|
S.onSwingCache[spellId] = result
|
|
return result
|
|
end
|
|
|
|
-- HS / Cleave / Maul are detected dynamically via IsOnSwingSpell (the
|
|
-- SPELL_ATTR_ON_NEXT_SWING bit) + name comparison against the rank-1
|
|
-- canonical names cached below — no per-rank ID maintenance.
|
|
|
|
-- Read config
|
|
local sw_width = tonumber(C.unitframes.swingtimerwidth) or 200
|
|
local sw_height = tonumber(C.unitframes.swingtimerheight) or 12
|
|
local sw_texture = C.unitframes.swingtimertexture or "Interface\\AddOns\\pfUI\\img\\bar"
|
|
local sw_showtext = C.unitframes.swingtimertext ~= "0"
|
|
local sw_showlabel = C.unitframes.swingtimerlabel ~= "0"
|
|
local sw_showoh = C.unitframes.swingtimeroffhand ~= "0"
|
|
local sw_showranged = C.unitframes.swingtimerranged ~= "0"
|
|
local sw_fontsize = tonumber(C.unitframes.swingtimerfontsize) or 12
|
|
local sw_hsqueue = C.unitframes.swingtimerhsqueue ~= "0"
|
|
local sw_showspeed = C.unitframes.swingtimerattackspeed == "1"
|
|
|
|
local function ParseColor(str, dr, dg, db, da)
|
|
if not str or str == "" then return dr, dg, db, da end
|
|
local _, _, r, g, b, a = string.find(str, "([%d%.]+),([%d%.]+),([%d%.]+),([%d%.]+)")
|
|
if r then
|
|
return tonumber(r) or dr, tonumber(g) or dg, tonumber(b) or db, tonumber(a) or da
|
|
end
|
|
return dr, dg, db, da
|
|
end
|
|
|
|
local mhR, mhG, mhB, mhA = ParseColor(C.unitframes.swingtimermhcolor, 0.8, 0.3, 0.3, 1)
|
|
local ohR, ohG, ohB, ohA = ParseColor(C.unitframes.swingtimerohcolor, 0.3, 0.8, 0.3, 1)
|
|
local raR, raG, raB, raA = ParseColor(C.unitframes.swingtimerrangedcolor, 0.3, 0.6, 1.0, 1)
|
|
local rwR, rwG, rwB, rwA = ParseColor(C.unitframes.swingtimerrangedwarncolor, 0.9, 0.0, 0.0, 1)
|
|
local isHunter = UnitClass("player") == "Hunter"
|
|
local mhDefaultR, mhDefaultG, mhDefaultB = mhR, mhG, mhB
|
|
|
|
|
|
|
|
-- Create container frame
|
|
pfUI.swingtimer = CreateFrame("Frame", "pfSwingTimer", UIParent)
|
|
pfUI.swingtimer:SetFrameStrata("MEDIUM")
|
|
pfUI.swingtimer:Hide()
|
|
|
|
-- Mainhand bar
|
|
pfUI.swingtimer.mainhand = CreateFrame("StatusBar", "pfSwingTimerMainhand", UIParent)
|
|
pfUI.swingtimer.mainhand:SetPoint("BOTTOM", UIParent, "CENTER", -189, 103)
|
|
pfUI.swingtimer.mainhand:SetWidth(sw_width)
|
|
pfUI.swingtimer.mainhand:SetHeight(sw_height)
|
|
pfUI.swingtimer.mainhand:SetMinMaxValues(0, 1)
|
|
pfUI.swingtimer.mainhand:SetValue(0)
|
|
pfUI.swingtimer.mainhand:SetStatusBarTexture(sw_texture)
|
|
pfUI.swingtimer.mainhand:SetStatusBarColor(mhR, mhG, mhB, mhA)
|
|
pfUI.swingtimer.mainhand:Hide()
|
|
|
|
pfUI.swingtimer.mainhand.text = pfUI.swingtimer.mainhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.mainhand.text:SetPoint("CENTER", pfUI.swingtimer.mainhand, "CENTER", 0, 0)
|
|
pfUI.swingtimer.mainhand.text:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.mainhand.text:SetTextColor(1, 1, 1, 1)
|
|
pfUI.swingtimer.mainhand.text:SetText("")
|
|
if not sw_showtext then pfUI.swingtimer.mainhand.text:Hide() end
|
|
|
|
pfUI.swingtimer.mainhand.label = pfUI.swingtimer.mainhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.mainhand.label:SetPoint("RIGHT", pfUI.swingtimer.mainhand, "LEFT", -4, 0)
|
|
pfUI.swingtimer.mainhand.label:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.mainhand.label:SetTextColor(0.8, 0.8, 0.8, 1)
|
|
pfUI.swingtimer.mainhand.label:SetText(sw_showlabel and "MH" or "")
|
|
|
|
pfUI.swingtimer.mainhand.speed = pfUI.swingtimer.mainhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.mainhand.speed:SetPoint("LEFT", pfUI.swingtimer.mainhand, "RIGHT", 4, 0)
|
|
pfUI.swingtimer.mainhand.speed:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.mainhand.speed:SetTextColor(0.8, 0.8, 0.8, 1)
|
|
pfUI.swingtimer.mainhand.speed:SetText("")
|
|
if not sw_showspeed then pfUI.swingtimer.mainhand.speed:Hide() end
|
|
|
|
CreateBackdrop(pfUI.swingtimer.mainhand)
|
|
|
|
|
|
pfUI.swingtimer.mainhand.marker = pfUI.swingtimer.mainhand:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.mainhand.marker:SetTexture(1, 1, 1, 1.0)
|
|
pfUI.swingtimer.mainhand.marker:SetWidth(2)
|
|
pfUI.swingtimer.mainhand.marker:SetHeight(sw_height)
|
|
pfUI.swingtimer.mainhand.marker:Hide()
|
|
|
|
-- Left glow: fades from transparent to white (right edge = marker)
|
|
pfUI.swingtimer.mainhand.markerGlowL = pfUI.swingtimer.mainhand:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.mainhand.markerGlowL:SetTexture(1, 1, 1, 1)
|
|
pfUI.swingtimer.mainhand.markerGlowL:SetGradientAlpha("HORIZONTAL", 1, 1, 1, 0, 1, 1, 1, 0.35)
|
|
pfUI.swingtimer.mainhand.markerGlowL:SetWidth(10)
|
|
pfUI.swingtimer.mainhand.markerGlowL:SetHeight(sw_height)
|
|
pfUI.swingtimer.mainhand.markerGlowL:Hide()
|
|
|
|
-- Right glow: fades from white to transparent (left edge = marker)
|
|
pfUI.swingtimer.mainhand.markerGlowR = pfUI.swingtimer.mainhand:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.mainhand.markerGlowR:SetTexture(1, 1, 1, 1)
|
|
pfUI.swingtimer.mainhand.markerGlowR:SetGradientAlpha("HORIZONTAL", 1, 1, 1, 0.35, 1, 1, 1, 0)
|
|
pfUI.swingtimer.mainhand.markerGlowR:SetWidth(10)
|
|
pfUI.swingtimer.mainhand.markerGlowR:SetHeight(sw_height)
|
|
pfUI.swingtimer.mainhand.markerGlowR:Hide()
|
|
|
|
CreateBackdropShadow(pfUI.swingtimer.mainhand)
|
|
|
|
-- Offhand bar
|
|
pfUI.swingtimer.offhand = CreateFrame("StatusBar", "pfSwingTimerOffhand", UIParent)
|
|
pfUI.swingtimer.offhand:SetPoint("TOP", pfUI.swingtimer.mainhand, "BOTTOM", 0, -4)
|
|
pfUI.swingtimer.offhand:SetWidth(sw_width)
|
|
pfUI.swingtimer.offhand:SetHeight(sw_height)
|
|
pfUI.swingtimer.offhand:SetMinMaxValues(0, 1)
|
|
pfUI.swingtimer.offhand:SetValue(0)
|
|
pfUI.swingtimer.offhand:SetStatusBarTexture(sw_texture)
|
|
pfUI.swingtimer.offhand:SetStatusBarColor(ohR, ohG, ohB, ohA)
|
|
pfUI.swingtimer.offhand:Hide()
|
|
|
|
pfUI.swingtimer.offhand.text = pfUI.swingtimer.offhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.offhand.text:SetPoint("CENTER", pfUI.swingtimer.offhand, "CENTER", 0, 0)
|
|
pfUI.swingtimer.offhand.text:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.offhand.text:SetTextColor(1, 1, 1, 1)
|
|
pfUI.swingtimer.offhand.text:SetText("")
|
|
if not sw_showtext then pfUI.swingtimer.offhand.text:Hide() end
|
|
|
|
pfUI.swingtimer.offhand.label = pfUI.swingtimer.offhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.offhand.label:SetPoint("RIGHT", pfUI.swingtimer.offhand, "LEFT", -4, 0)
|
|
pfUI.swingtimer.offhand.label:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.offhand.label:SetTextColor(0.8, 0.8, 0.8, 1)
|
|
pfUI.swingtimer.offhand.label:SetText(sw_showlabel and "OH" or "")
|
|
|
|
pfUI.swingtimer.offhand.speed = pfUI.swingtimer.offhand:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.offhand.speed:SetPoint("LEFT", pfUI.swingtimer.offhand, "RIGHT", 4, 0)
|
|
pfUI.swingtimer.offhand.speed:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.offhand.speed:SetTextColor(0.8, 0.8, 0.8, 1)
|
|
pfUI.swingtimer.offhand.speed:SetText("")
|
|
if not sw_showspeed then pfUI.swingtimer.offhand.speed:Hide() end
|
|
|
|
CreateBackdrop(pfUI.swingtimer.offhand)
|
|
|
|
|
|
pfUI.swingtimer.offhand.marker = pfUI.swingtimer.offhand:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.offhand.marker:SetTexture(1, 1, 1, 1.0)
|
|
pfUI.swingtimer.offhand.marker:SetWidth(2)
|
|
pfUI.swingtimer.offhand.marker:SetHeight(sw_height)
|
|
pfUI.swingtimer.offhand.marker:Hide()
|
|
|
|
-- Left glow: fades from transparent to white (right edge = marker)
|
|
pfUI.swingtimer.offhand.markerGlowL = pfUI.swingtimer.offhand:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.offhand.markerGlowL:SetTexture(1, 1, 1, 1)
|
|
pfUI.swingtimer.offhand.markerGlowL:SetGradientAlpha("HORIZONTAL", 1, 1, 1, 0, 1, 1, 1, 0.35)
|
|
pfUI.swingtimer.offhand.markerGlowL:SetWidth(10)
|
|
pfUI.swingtimer.offhand.markerGlowL:SetHeight(sw_height)
|
|
pfUI.swingtimer.offhand.markerGlowL:Hide()
|
|
|
|
-- Right glow: fades from white to transparent (left edge = marker)
|
|
pfUI.swingtimer.offhand.markerGlowR = pfUI.swingtimer.offhand:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.offhand.markerGlowR:SetTexture(1, 1, 1, 1)
|
|
pfUI.swingtimer.offhand.markerGlowR:SetGradientAlpha("HORIZONTAL", 1, 1, 1, 0.35, 1, 1, 1, 0)
|
|
pfUI.swingtimer.offhand.markerGlowR:SetWidth(10)
|
|
pfUI.swingtimer.offhand.markerGlowR:SetHeight(sw_height)
|
|
pfUI.swingtimer.offhand.markerGlowR:Hide()
|
|
|
|
CreateBackdropShadow(pfUI.swingtimer.offhand)
|
|
|
|
-- Ranged bar
|
|
pfUI.swingtimer.ranged = CreateFrame("Frame", "pfSwingTimerRanged", UIParent)
|
|
pfUI.swingtimer.ranged:SetPoint("BOTTOM", UIParent, "CENTER", -189, 83)
|
|
pfUI.swingtimer.ranged:SetWidth(sw_width)
|
|
pfUI.swingtimer.ranged:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged:Hide()
|
|
|
|
pfUI.swingtimer.ranged.left = pfUI.swingtimer.ranged:CreateTexture(nil, "ARTWORK")
|
|
pfUI.swingtimer.ranged.left:SetTexture(sw_texture)
|
|
pfUI.swingtimer.ranged.left:SetPoint("RIGHT", pfUI.swingtimer.ranged, "CENTER", 0, 0)
|
|
pfUI.swingtimer.ranged.left:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged.left:SetWidth(sw_width / 2)
|
|
pfUI.swingtimer.ranged.left:SetTexCoord(0, 0.5, 0, 1)
|
|
pfUI.swingtimer.ranged.left:SetVertexColor(raR, raG, raB, raA)
|
|
|
|
pfUI.swingtimer.ranged.right = pfUI.swingtimer.ranged:CreateTexture(nil, "ARTWORK")
|
|
pfUI.swingtimer.ranged.right:SetTexture(sw_texture)
|
|
pfUI.swingtimer.ranged.right:SetPoint("LEFT", pfUI.swingtimer.ranged, "CENTER", 0, 0)
|
|
pfUI.swingtimer.ranged.right:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged.right:SetWidth(sw_width / 2)
|
|
pfUI.swingtimer.ranged.right:SetTexCoord(0.5, 1, 0, 1)
|
|
pfUI.swingtimer.ranged.right:SetVertexColor(raR, raG, raB, raA)
|
|
|
|
pfUI.swingtimer.ranged.warn = pfUI.swingtimer.ranged:CreateTexture(nil, "ARTWORK")
|
|
pfUI.swingtimer.ranged.warn:SetTexture(sw_texture)
|
|
pfUI.swingtimer.ranged.warn:SetPoint("CENTER", pfUI.swingtimer.ranged, "CENTER", 0, 0)
|
|
pfUI.swingtimer.ranged.warn:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged.warn:SetWidth(1)
|
|
pfUI.swingtimer.ranged.warn:SetVertexColor(rwR, rwG, rwB, rwA)
|
|
pfUI.swingtimer.ranged.warn:Hide()
|
|
|
|
pfUI.swingtimer.ranged.text = pfUI.swingtimer.ranged:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.ranged.text:SetPoint("CENTER", pfUI.swingtimer.ranged, "CENTER", 0, 0)
|
|
pfUI.swingtimer.ranged.text:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.ranged.text:SetTextColor(1, 1, 1, 1)
|
|
pfUI.swingtimer.ranged.text:SetText("")
|
|
if not sw_showtext then pfUI.swingtimer.ranged.text:Hide() end
|
|
|
|
pfUI.swingtimer.ranged.label = pfUI.swingtimer.ranged:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.ranged.label:SetPoint("RIGHT", pfUI.swingtimer.ranged, "LEFT", -4, 0)
|
|
pfUI.swingtimer.ranged.label:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.ranged.label:SetTextColor(0.8, 0.8, 0.8, 1)
|
|
pfUI.swingtimer.ranged.label:SetText(sw_showlabel and "Ra" or "")
|
|
|
|
pfUI.swingtimer.ranged.speed = pfUI.swingtimer.ranged:CreateFontString("Status", "DIALOG", "GameFontNormal")
|
|
pfUI.swingtimer.ranged.speed:SetPoint("LEFT", pfUI.swingtimer.ranged, "RIGHT", 4, 0)
|
|
pfUI.swingtimer.ranged.speed:SetFont(pfUI.font_default, sw_fontsize, "OUTLINE")
|
|
pfUI.swingtimer.ranged.speed:SetTextColor(0.8, 0.8, 0.8, 1)
|
|
pfUI.swingtimer.ranged.speed:SetText("")
|
|
if not sw_showspeed then pfUI.swingtimer.ranged.speed:Hide() end
|
|
|
|
CreateBackdrop(pfUI.swingtimer.ranged)
|
|
|
|
|
|
pfUI.swingtimer.ranged.marker = pfUI.swingtimer.ranged:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.ranged.marker:SetTexture(1, 1, 1, 1.0)
|
|
pfUI.swingtimer.ranged.marker:SetWidth(2)
|
|
pfUI.swingtimer.ranged.marker:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged.marker:Hide()
|
|
|
|
-- Left glow: fades from transparent to white (right edge = marker)
|
|
pfUI.swingtimer.ranged.markerGlowL = pfUI.swingtimer.ranged:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.ranged.markerGlowL:SetTexture(1, 1, 1, 1)
|
|
pfUI.swingtimer.ranged.markerGlowL:SetGradientAlpha("HORIZONTAL", 1, 1, 1, 0, 1, 1, 1, 0.35)
|
|
pfUI.swingtimer.ranged.markerGlowL:SetWidth(10)
|
|
pfUI.swingtimer.ranged.markerGlowL:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged.markerGlowL:Hide()
|
|
|
|
-- Right glow: fades from white to transparent (left edge = marker)
|
|
pfUI.swingtimer.ranged.markerGlowR = pfUI.swingtimer.ranged:CreateTexture(nil, "OVERLAY")
|
|
pfUI.swingtimer.ranged.markerGlowR:SetTexture(1, 1, 1, 1)
|
|
pfUI.swingtimer.ranged.markerGlowR:SetGradientAlpha("HORIZONTAL", 1, 1, 1, 0.35, 1, 1, 1, 0)
|
|
pfUI.swingtimer.ranged.markerGlowR:SetWidth(10)
|
|
pfUI.swingtimer.ranged.markerGlowR:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged.markerGlowR:Hide()
|
|
|
|
CreateBackdropShadow(pfUI.swingtimer.ranged)
|
|
|
|
UpdateMovable(pfUI.swingtimer.mainhand)
|
|
UpdateMovable(pfUI.swingtimer.ranged)
|
|
|
|
-- OH weapon detection
|
|
local OH_WEAPON_TYPES = { [13]=true, [21]=true }
|
|
local function HasOffhandWeapon()
|
|
local l = GetInventoryItemLink("player", 17)
|
|
if not l then return false end
|
|
local _, _, id = string.find(l, "item:(%d+)")
|
|
id = tonumber(id)
|
|
if not id then return false end
|
|
return OH_WEAPON_TYPES[C_Item.GetItemInventoryTypeByID(id)] == true
|
|
end
|
|
|
|
local function UpdateWeaponSpeeds()
|
|
local ms, os = UnitAttackSpeed("player")
|
|
S.mhSpeed = (ms and ms > 0) and ms or S.mhSpeed
|
|
S.ohSpeed = (HasOffhandWeapon() and os and os > 0) and os or 0
|
|
local rs = UnitRangedDamage("player")
|
|
S.raSpeed = (rs and rs > 0) and rs or 0
|
|
end
|
|
|
|
-- Reset MH countdown to full speed (server confirmed swing)
|
|
local function ResetMH()
|
|
if S.mhFrozenAt then
|
|
S.mhFrozenAt = nil
|
|
end
|
|
UpdateWeaponSpeeds()
|
|
pfUI.swingtimer.mhGraceAt = nil -- cancel any pending hide
|
|
S.mhTimerMax = S.mhSpeed
|
|
S.mhTimer = S.mhSpeed
|
|
S.mhActive = true
|
|
pfUI.swingtimer.mainhand:Show()
|
|
pfUI.swingtimer:Show()
|
|
end
|
|
|
|
-- Reset OH countdown to full speed
|
|
local function ResetOH()
|
|
UpdateWeaponSpeeds()
|
|
if S.ohSpeed <= 0 then return end
|
|
pfUI.swingtimer.ohGraceAt = nil -- cancel any pending hide
|
|
S.ohTimerMax = S.ohSpeed
|
|
S.ohTimer = S.ohSpeed
|
|
S.ohActive = true
|
|
if sw_showoh then pfUI.swingtimer.offhand:Show() end
|
|
pfUI.swingtimer:Show()
|
|
end
|
|
|
|
-- Reset ranged countdown. replaceMH=true (Hunter Auto Shot, Throw) stops
|
|
-- the melee swing clock while ranged ticks; replaceMH=false (wand Shoot)
|
|
-- leaves it running for melee weaving.
|
|
local function ResetRanged(replaceMH)
|
|
if not sw_showranged then return end
|
|
UpdateWeaponSpeeds()
|
|
if S.raSpeed <= 0 then return end
|
|
if replaceMH then
|
|
S.mhActive = false
|
|
pfUI.swingtimer.mainhand:Hide()
|
|
end
|
|
S.raTimerMax = S.raSpeed
|
|
S.raTimer = S.raSpeed
|
|
S.raActive = true
|
|
|
|
if isHunter then
|
|
pfUI.swingtimer.ranged.left:ClearAllPoints()
|
|
pfUI.swingtimer.ranged.left:SetPoint("RIGHT", pfUI.swingtimer.ranged, "CENTER", 0, 0)
|
|
pfUI.swingtimer.ranged.left:SetWidth(sw_width / 2)
|
|
pfUI.swingtimer.ranged.left:SetTexCoord(0, 0.5, 0, 1)
|
|
pfUI.swingtimer.ranged.right:SetWidth(sw_width / 2)
|
|
pfUI.swingtimer.ranged.right:SetTexCoord(0.5, 1, 0, 1)
|
|
else
|
|
pfUI.swingtimer.ranged.left:ClearAllPoints()
|
|
pfUI.swingtimer.ranged.left:SetPoint("TOPLEFT", pfUI.swingtimer.ranged, "TOPLEFT", 0, 0)
|
|
pfUI.swingtimer.ranged.left:SetWidth(0.1)
|
|
pfUI.swingtimer.ranged.left:Hide()
|
|
pfUI.swingtimer.ranged.left:SetTexCoord(0, 0, 0, 1)
|
|
pfUI.swingtimer.ranged.right:SetWidth(0.1)
|
|
pfUI.swingtimer.ranged.right:Hide()
|
|
end
|
|
pfUI.swingtimer.ranged.left:SetVertexColor(raR, raG, raB, raA)
|
|
pfUI.swingtimer.ranged.right:SetVertexColor(raR, raG, raB, raA)
|
|
pfUI.swingtimer.ranged.warn:SetWidth(1)
|
|
pfUI.swingtimer.ranged.warn:Hide()
|
|
pfUI.swingtimer.ranged:Show()
|
|
pfUI.swingtimer:Show()
|
|
end
|
|
|
|
local function ResetAll()
|
|
S.mhActive = false
|
|
S.ohActive = false
|
|
S.raActive = false
|
|
S.mhTimer = 0
|
|
S.ohTimer = 0
|
|
S.raTimer = 0
|
|
pfUI.swingtimer.mhGraceAt = nil
|
|
pfUI.swingtimer.ohGraceAt = nil
|
|
pfUI.swingtimer.mainhand:Hide()
|
|
pfUI.swingtimer.offhand:Hide()
|
|
pfUI.swingtimer.ranged:Hide()
|
|
pfUI.swingtimer:Hide()
|
|
end
|
|
|
|
-- HS/Cleave/Maul helpers. Canonical rank-1 spellIDs resolve to the
|
|
-- localized spell name once, so per-spell classification is rank- and
|
|
-- locale-independent without per-rank ID hardcoding (every rank of
|
|
-- Heroic Strike etc. returns the same name).
|
|
local HS_NAME = C_Spell.GetSpellName(78) -- Heroic Strike (Rank 1)
|
|
local CLEAVE_NAME = C_Spell.GetSpellName(845) -- Cleave (Rank 1)
|
|
local MAUL_NAME = C_Spell.GetSpellName(6807) -- Maul (Rank 1)
|
|
|
|
-- Classify an on-next-swing spell as "hs" / "cleave" / "maul" / nil.
|
|
-- IsOnSwingSpell gates the family (ATTR_ON_NEXT_SWING bit 0x04) so
|
|
-- unrelated spells return nil cheaply.
|
|
local function ClassifyOnSwingSpell(spellId)
|
|
if not IsOnSwingSpell(spellId) then return nil end
|
|
local name = C_Spell.GetSpellName(spellId)
|
|
if name == HS_NAME then return "hs"
|
|
elseif name == CLEAVE_NAME then return "cleave"
|
|
elseif name == MAUL_NAME then return "maul"
|
|
end
|
|
end
|
|
|
|
-- Set the queue flags from a ClassifyOnSwingSpell result. nil = no-op
|
|
-- (preserves prior flag state when the queued spell isn't one we
|
|
-- color-code, matching the legacy table-lookup behavior).
|
|
local function SetQueuedKind(kind)
|
|
if not kind then return end
|
|
S.hsQueued = (kind == "hs")
|
|
S.cleaveQueued = (kind == "cleave")
|
|
S.maulQueued = (kind == "maul")
|
|
S.hsSeenCurrent, S.cleaveSeenCurrent, S.maulSeenCurrent = false, false, false
|
|
end
|
|
|
|
local function RebuildQueueSlotCache()
|
|
if not sw_hsqueue then return end
|
|
S.cachedHSSlots = {}
|
|
S.cachedCleaveSlots = {}
|
|
S.cachedMaulSlots = {}
|
|
if not (S.isWarrior or S.isDruid) then return end
|
|
for slot = 1, 120 do
|
|
local kind, id = GetActionInfo(slot)
|
|
local name
|
|
if kind == "spell" then
|
|
name = GetSpellInfo(id)
|
|
elseif kind == "macro" then
|
|
name = GetMacroSpell(id)
|
|
end
|
|
if name == HS_NAME then
|
|
table.insert(S.cachedHSSlots, slot)
|
|
elseif name == CLEAVE_NAME then
|
|
table.insert(S.cachedCleaveSlots, slot)
|
|
elseif name == MAUL_NAME then
|
|
table.insert(S.cachedMaulSlots, slot)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function CheckQueuedAction(slotList)
|
|
for i = 1, table.getn(slotList) do
|
|
if IsCurrentAction(slotList[i]) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Reconcile a stale event-driven queue flag against the client's current
|
|
-- action. Not every de-queue emits a SPELL_QUEUE pop — pressing Esc or
|
|
-- re-pressing to cancel an on-swing spell doesn't — so the flag alone stays
|
|
-- set. IsCurrentAction, which the client clears on cancel, is the reconciling
|
|
-- signal, but only after it has confirmed the ability as current at least once
|
|
-- (`seen`): a nampower-initiated cast may never flip IsCurrentAction, and must
|
|
-- keep its color until its own pop/resolve rather than be cleared early.
|
|
-- Returns the updated (queued, seen).
|
|
local function ReconcileQueued(queued, slots, seen)
|
|
if not queued then return false, false end
|
|
if CheckQueuedAction(slots) then return true, true end
|
|
if seen then return false, false end -- was current, now gone -> cancelled
|
|
return true, false -- never confirmed current -> keep
|
|
end
|
|
|
|
local function IsHSOrCleaveQueued()
|
|
if not sw_hsqueue or not S.isWarrior then return false, false end
|
|
if S.useSpellQueueEvent then
|
|
S.hsQueued, S.hsSeenCurrent =
|
|
ReconcileQueued(S.hsQueued, S.cachedHSSlots, S.hsSeenCurrent)
|
|
S.cleaveQueued, S.cleaveSeenCurrent =
|
|
ReconcileQueued(S.cleaveQueued, S.cachedCleaveSlots, S.cleaveSeenCurrent)
|
|
return S.hsQueued, S.cleaveQueued
|
|
end
|
|
return CheckQueuedAction(S.cachedHSSlots), CheckQueuedAction(S.cachedCleaveSlots)
|
|
end
|
|
|
|
-- OnUpdate: countdown all timers with delta, then render
|
|
pfUI.swingtimer:SetScript("OnUpdate", function()
|
|
S.swingThrottle = S.swingThrottle + arg1
|
|
local swingDelay = pfUI.throttle and pfUI.throttle:Get("swingtimer") or 0.02
|
|
if S.swingThrottle < swingDelay then return end
|
|
local delta = S.swingThrottle
|
|
S.swingThrottle = 0
|
|
|
|
-- (out-of-combat hide handled naturally when timers expire)
|
|
|
|
local anyActive = false
|
|
|
|
-- Tick timers down. When a timer expires we give a short grace period before
|
|
-- hiding the bar, to bridge the 1-2 frame gap until AUTO_ATTACK_SELF arrives.
|
|
-- If AUTO_ATTACK_SELF arrives during the grace period it resets the timer
|
|
-- normally and the grace timer is cleared. If not (e.g. auto-attack was
|
|
-- turned off), the bar hides after the grace period ends.
|
|
local GRACE = 0.15 -- seconds to wait after timer hits 0 before hiding
|
|
|
|
if S.mhActive then
|
|
-- Don't tick while frozen (swing-delay spell like Slam is being cast)
|
|
if not S.mhFrozenAt then
|
|
S.mhTimer = S.mhTimer - delta
|
|
end
|
|
if S.mhTimer <= 0 then
|
|
S.mhTimer = 0
|
|
if S.mhFrozenAt then
|
|
-- Swing-delay spell cast in progress: bar holds at 0, skip grace/hide.
|
|
-- SPELL_GO handler will unfreeze and add cast duration to timer.
|
|
elseif not pfUI.swingtimer.mhGraceAt then
|
|
pfUI.swingtimer.mhGraceAt = GetTime() + GRACE
|
|
elseif GetTime() >= pfUI.swingtimer.mhGraceAt then
|
|
pfUI.swingtimer.mhGraceAt = nil
|
|
if not S.inCombat or not S.autoAttackActive then
|
|
S.mhActive = false
|
|
pfUI.swingtimer.mainhand:Hide()
|
|
S.lastMhMarkerX = -1
|
|
pfUI.swingtimer.mainhand.marker:Hide()
|
|
pfUI.swingtimer.mainhand.markerGlowL:Hide()
|
|
pfUI.swingtimer.mainhand.markerGlowR:Hide()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if S.ohActive then
|
|
S.ohTimer = S.ohTimer - delta
|
|
if S.ohTimer <= 0 then
|
|
S.ohTimer = 0
|
|
if not pfUI.swingtimer.ohGraceAt then
|
|
pfUI.swingtimer.ohGraceAt = GetTime() + GRACE
|
|
elseif GetTime() >= pfUI.swingtimer.ohGraceAt then
|
|
pfUI.swingtimer.ohGraceAt = nil
|
|
if not S.inCombat or not S.autoAttackActive then
|
|
S.ohActive = false
|
|
pfUI.swingtimer.offhand:Hide()
|
|
S.lastOhMarkerX = -1
|
|
pfUI.swingtimer.offhand.marker:Hide()
|
|
pfUI.swingtimer.offhand.markerGlowL:Hide()
|
|
pfUI.swingtimer.offhand.markerGlowR:Hide()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if S.raActive then
|
|
S.raTimer = S.raTimer - delta
|
|
if S.raTimer <= 0 then
|
|
S.raTimer = 0
|
|
S.raActive = false
|
|
pfUI.swingtimer.ranged:Hide()
|
|
end
|
|
end
|
|
|
|
-- HS/Cleave color
|
|
local curR, curG, curB = mhDefaultR, mhDefaultG, mhDefaultB
|
|
if sw_hsqueue then
|
|
if S.isDruid and S.useSpellQueueEvent then
|
|
S.maulQueued, S.maulSeenCurrent =
|
|
ReconcileQueued(S.maulQueued, S.cachedMaulSlots, S.maulSeenCurrent)
|
|
end
|
|
if S.maulQueued and S.isDruid then
|
|
curR, curG, curB = 1.0, 0.55, 0.0 -- orange for druid maul queue
|
|
elseif S.isWarrior then
|
|
local hs, cl = IsHSOrCleaveQueued()
|
|
if cl then
|
|
curR, curG, curB = 0.2, 0.9, 0.2
|
|
elseif hs then
|
|
curR, curG, curB = 0.9, 0.9, 0.2
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Render MH
|
|
if S.mhActive then
|
|
local progress = 1 - (S.mhTimer / S.mhTimerMax)
|
|
pfUI.swingtimer.mainhand:SetValue(progress)
|
|
if S.mhTimer <= 0 then
|
|
-- Timer expired: hide marker (no gap at right edge)
|
|
if S.lastMhMarkerX ~= -1 then
|
|
S.lastMhMarkerX = -1
|
|
pfUI.swingtimer.mainhand.marker:Hide()
|
|
pfUI.swingtimer.mainhand.markerGlowL:Hide()
|
|
pfUI.swingtimer.mainhand.markerGlowR:Hide()
|
|
end
|
|
else
|
|
local mhMarkerX = progress * sw_width
|
|
if mhMarkerX < 1 then mhMarkerX = 1 end
|
|
if mhMarkerX > sw_width - 2 then mhMarkerX = sw_width - 2 end
|
|
if mhMarkerX ~= S.lastMhMarkerX then
|
|
S.lastMhMarkerX = mhMarkerX
|
|
pfUI.swingtimer.mainhand.marker:SetPoint("LEFT", pfUI.swingtimer.mainhand, "LEFT", mhMarkerX - 1, 0)
|
|
pfUI.swingtimer.mainhand.markerGlowL:SetPoint("RIGHT", pfUI.swingtimer.mainhand.marker, "LEFT", 0, 0)
|
|
pfUI.swingtimer.mainhand.markerGlowR:SetPoint("LEFT", pfUI.swingtimer.mainhand.marker, "RIGHT", 0, 0)
|
|
pfUI.swingtimer.mainhand.marker:Show()
|
|
pfUI.swingtimer.mainhand.markerGlowL:Show()
|
|
pfUI.swingtimer.mainhand.markerGlowR:Show()
|
|
end
|
|
end
|
|
pfUI.swingtimer.mainhand:SetStatusBarColor(curR, curG, curB, mhA)
|
|
if sw_showtext then
|
|
pfUI.swingtimer.mainhand.text:SetText(string.format("%.1f", math.floor(S.mhTimer * 10) / 10))
|
|
end
|
|
if sw_showspeed and S.mhSpeed > 0 then
|
|
pfUI.swingtimer.mainhand.speed:SetText(string.format("%.2f", S.mhSpeed))
|
|
end
|
|
anyActive = true
|
|
end
|
|
|
|
-- Render OH
|
|
if sw_showoh and S.ohActive then
|
|
local progress = 1 - (S.ohTimer / S.ohTimerMax)
|
|
pfUI.swingtimer.offhand:SetValue(progress)
|
|
if S.ohTimer <= 0 then
|
|
if S.lastOhMarkerX ~= -1 then
|
|
S.lastOhMarkerX = -1
|
|
pfUI.swingtimer.offhand.marker:Hide()
|
|
pfUI.swingtimer.offhand.markerGlowL:Hide()
|
|
pfUI.swingtimer.offhand.markerGlowR:Hide()
|
|
end
|
|
else
|
|
local ohMarkerX = progress * sw_width
|
|
if ohMarkerX < 1 then ohMarkerX = 1 end
|
|
if ohMarkerX > sw_width - 2 then ohMarkerX = sw_width - 2 end
|
|
if ohMarkerX ~= S.lastOhMarkerX then
|
|
S.lastOhMarkerX = ohMarkerX
|
|
pfUI.swingtimer.offhand.marker:SetPoint("LEFT", pfUI.swingtimer.offhand, "LEFT", ohMarkerX - 1, 0)
|
|
pfUI.swingtimer.offhand.markerGlowL:SetPoint("RIGHT", pfUI.swingtimer.offhand.marker, "LEFT", 0, 0)
|
|
pfUI.swingtimer.offhand.markerGlowR:SetPoint("LEFT", pfUI.swingtimer.offhand.marker, "RIGHT", 0, 0)
|
|
pfUI.swingtimer.offhand.marker:Show()
|
|
pfUI.swingtimer.offhand.markerGlowL:Show()
|
|
pfUI.swingtimer.offhand.markerGlowR:Show()
|
|
end
|
|
end
|
|
if sw_showtext then
|
|
pfUI.swingtimer.offhand.text:SetText(string.format("%.1f", math.floor(S.ohTimer * 10) / 10))
|
|
end
|
|
if sw_showspeed and S.ohSpeed > 0 then
|
|
pfUI.swingtimer.offhand.speed:SetText(string.format("%.2f", S.ohSpeed))
|
|
end
|
|
anyActive = true
|
|
elseif not sw_showoh then
|
|
pfUI.swingtimer.offhand:Hide()
|
|
end
|
|
|
|
-- Render Ranged
|
|
if sw_showranged and S.raActive then
|
|
local remaining = S.raTimer
|
|
if isHunter then
|
|
local DEADZONE = 0.5
|
|
local halfW = sw_width / 2
|
|
if remaining > DEADZONE then
|
|
local elapsed = S.raTimerMax - remaining
|
|
local phase1dur = S.raTimerMax - DEADZONE
|
|
local p = elapsed / phase1dur
|
|
local w = halfW * (1 - p)
|
|
if w < 1 then w = 1 end
|
|
pfUI.swingtimer.ranged.left:Show()
|
|
pfUI.swingtimer.ranged.left:SetWidth(w)
|
|
pfUI.swingtimer.ranged.left:SetTexCoord(0, (1 - p) * 0.5, 0, 1)
|
|
pfUI.swingtimer.ranged.left:SetVertexColor(raR, raG, raB, raA)
|
|
pfUI.swingtimer.ranged.right:Show()
|
|
pfUI.swingtimer.ranged.right:SetWidth(w)
|
|
pfUI.swingtimer.ranged.right:SetTexCoord(1 - (1 - p) * 0.5, 1, 0, 1)
|
|
pfUI.swingtimer.ranged.right:SetVertexColor(raR, raG, raB, raA)
|
|
pfUI.swingtimer.ranged.warn:Hide()
|
|
else
|
|
local p = 1 - (remaining / DEADZONE)
|
|
local w = sw_width * p
|
|
if w < 1 then w = 1 end
|
|
pfUI.swingtimer.ranged.left:Hide()
|
|
pfUI.swingtimer.ranged.right:Hide()
|
|
pfUI.swingtimer.ranged.warn:SetWidth(w)
|
|
pfUI.swingtimer.ranged.warn:Show()
|
|
end
|
|
if sw_showtext then
|
|
if remaining <= 0.5 then
|
|
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", math.floor(remaining * 10) / 10))
|
|
else
|
|
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", math.floor((remaining - 0.5) * 10) / 10))
|
|
end
|
|
end
|
|
else
|
|
local progress = 1 - (remaining / S.raTimerMax)
|
|
local w = sw_width * progress
|
|
if w < 1 then w = 1 end
|
|
pfUI.swingtimer.ranged.left:Show()
|
|
pfUI.swingtimer.ranged.left:SetWidth(w)
|
|
pfUI.swingtimer.ranged.left:SetTexCoord(0, progress, 0, 1)
|
|
pfUI.swingtimer.ranged.right:Hide()
|
|
pfUI.swingtimer.ranged.warn:Hide()
|
|
if sw_showtext then
|
|
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", math.floor(remaining * 10) / 10))
|
|
end
|
|
end
|
|
if sw_showspeed and S.raSpeed > 0 then
|
|
pfUI.swingtimer.ranged.speed:SetText(string.format("%.2f", S.raSpeed))
|
|
end
|
|
local raProgress = 1 - (S.raTimer / S.raTimerMax)
|
|
local raMarkerX = raProgress * sw_width
|
|
if raMarkerX < 1 then raMarkerX = 1 end
|
|
if raMarkerX > sw_width - 2 then raMarkerX = sw_width - 2 end
|
|
if raMarkerX ~= S.lastRaMarkerX then
|
|
S.lastRaMarkerX = raMarkerX
|
|
if not isHunter then
|
|
pfUI.swingtimer.ranged.marker:SetPoint("LEFT", pfUI.swingtimer.ranged, "LEFT", raMarkerX - 1, 0)
|
|
pfUI.swingtimer.ranged.marker:Show()
|
|
pfUI.swingtimer.ranged.markerGlowL:SetPoint("RIGHT", pfUI.swingtimer.ranged.marker, "LEFT", 0, 0)
|
|
pfUI.swingtimer.ranged.markerGlowR:SetPoint("LEFT", pfUI.swingtimer.ranged.marker, "RIGHT", 0, 0)
|
|
pfUI.swingtimer.ranged.markerGlowL:Show()
|
|
pfUI.swingtimer.ranged.markerGlowR:Show()
|
|
else
|
|
pfUI.swingtimer.ranged.marker:Hide()
|
|
pfUI.swingtimer.ranged.markerGlowL:Hide()
|
|
pfUI.swingtimer.ranged.markerGlowR:Hide()
|
|
end
|
|
end
|
|
anyActive = true
|
|
elseif not sw_showranged then
|
|
pfUI.swingtimer.ranged:Hide()
|
|
end
|
|
|
|
if not anyActive then
|
|
if not pfUI.swingtimer.mainhand:IsShown()
|
|
and not pfUI.swingtimer.offhand:IsShown()
|
|
and not pfUI.swingtimer.ranged:IsShown() then
|
|
this:Hide()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- SPELL_START_SELF: fires only for cast-time spells, never instants
|
|
local spellStartFrame = CreateFrame("Frame")
|
|
spellStartFrame:RegisterEvent("SPELL_START_SELF")
|
|
spellStartFrame:SetScript("OnEvent", function()
|
|
if not (arg1 and arg1 > 0) then return end
|
|
S.pendingCastSpellId = arg1
|
|
-- Freeze the swing timer for cast-time spells that DON'T reset auto-
|
|
-- attack on completion (Slam, Hammer of Wrath on Turtle, etc.) — those
|
|
-- let the swing resume from where it paused. Detect dynamically via the
|
|
-- absent AUTOATTACK interrupt flag (8); spells with that bit reset on
|
|
-- SPELL_GO_SELF so freezing isn't necessary. Subsumes the old hardcoded
|
|
-- swingDelaySpells list (no list maintenance for new Slam-style spells).
|
|
if S.mhActive then
|
|
local iflags = GetSpellRecField(arg1, "interruptFlags") or 0
|
|
if bit.band(iflags, FLAG_AUTOATTACK) == 0 then
|
|
S.mhFrozenAt = GetTime()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Cast cancel/fail/interrupt: unfreeze swing timer if a delay spell was interrupted
|
|
local spellFailFrame = CreateFrame("Frame")
|
|
spellFailFrame:RegisterEvent("SPELLCAST_FAILED")
|
|
spellFailFrame:RegisterEvent("SPELLCAST_INTERRUPTED")
|
|
spellFailFrame:SetScript("OnEvent", function()
|
|
if S.mhFrozenAt then
|
|
S.mhFrozenAt = nil
|
|
end
|
|
S.pendingCastSpellId = nil
|
|
end)
|
|
|
|
-- SPELL_GO hook via libdebuff
|
|
pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {}
|
|
pfUI.libdebuff_spell_go_hooks["swingtimer"] = function(spellId)
|
|
-- C_Spell.IsRangedAutoAttackSpell catches both Auto Shot (75) and
|
|
-- wand Shoot (5019) via Spell.dbc's AUTO_REPEAT attribute (covers
|
|
-- any future auto-repeat ranged spell automatically). Wand is the
|
|
-- one independent of the MH swing — everything else replaces it.
|
|
-- Throw isn't auto-repeat (single-shot) so it's handled explicitly.
|
|
if C_Spell.IsRangedAutoAttackSpell(spellId) then
|
|
ResetRanged(spellId ~= WAND_SHOOT_SPELLID)
|
|
return
|
|
elseif spellId == THROW_SPELLID then
|
|
ResetRanged(true)
|
|
return
|
|
elseif IsOnSwingSpell(spellId) then
|
|
-- On-next-swing ability (HS / Cleave / Maul / Raptor Strike / etc.)
|
|
-- — the swing fires as the spell consumes it. Drop the queued color.
|
|
S.hsQueued = false; S.cleaveQueued = false; S.maulQueued = false
|
|
ResetMH()
|
|
else
|
|
-- Mirror the server rule for "does this spell reset the auto-attack
|
|
-- swing" (Spell::IsMeleeAttackResetSpell in Turtle's core):
|
|
-- InterruptFlags has SPELL_INTERRUPT_FLAG_AUTOATTACK (0x08)
|
|
-- AND AttributesEx2 lacks NOT_RESET_AUTO_ACTIONS (0x20000).
|
|
-- If neither path resets and we're holding a frozen-swing-during-cast
|
|
-- (mhFrozenAt set by SPELL_START_SELF for non-AUTOATTACK spells), this
|
|
-- is a Slam-style cast — push the timer forward by the cast duration
|
|
-- so the bar resumes from where it paused.
|
|
local iflags = GetSpellRecField(spellId, "interruptFlags") or 0
|
|
if bit.band(iflags, FLAG_AUTOATTACK) ~= 0
|
|
and bit.band(GetSpellRecField(spellId, "attributesEx2") or 0, ATTR_KEEP_SWINGS) == 0 then
|
|
if S.mhActive and S.mhSpeed > 0 then
|
|
UpdateWeaponSpeeds()
|
|
S.mhTimerMax = S.mhSpeed
|
|
S.mhTimer = S.mhSpeed
|
|
end
|
|
if S.ohActive and S.ohSpeed > 0 then
|
|
S.ohTimerMax = S.ohSpeed
|
|
S.ohTimer = S.ohSpeed
|
|
end
|
|
elseif S.mhFrozenAt then
|
|
local castDuration = GetTime() - S.mhFrozenAt
|
|
S.mhTimer = S.mhTimer + castDuration
|
|
S.mhTimerMax = S.mhTimerMax + castDuration
|
|
S.mhFrozenAt = nil
|
|
end
|
|
end
|
|
S.pendingCastSpellId = nil
|
|
end
|
|
|
|
-- SPELL_CAST_EVENT hook: HS/Cleave/Maul queue tracking
|
|
pfUI.libdebuff_spell_cast_hooks = pfUI.libdebuff_spell_cast_hooks or {}
|
|
pfUI.libdebuff_spell_cast_hooks["swingtimer"] = function(success, spellId)
|
|
local kind = ClassifyOnSwingSpell(spellId)
|
|
if not kind then return end
|
|
S.useSpellQueueEvent = true
|
|
SetQueuedKind(kind)
|
|
end
|
|
|
|
|
|
local events = CreateFrame("Frame")
|
|
events:RegisterEvent("AUTO_ATTACK_SELF")
|
|
events:RegisterEvent("AUTO_ATTACK_OTHER")
|
|
events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
events:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") -- ClassicAPI: per-slot, equipment-only
|
|
events:RegisterEvent("PLAYER_REGEN_DISABLED")
|
|
events:RegisterEvent("PLAYER_REGEN_ENABLED")
|
|
events:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
|
|
events:RegisterEvent("UNIT_DIED")
|
|
events:RegisterEvent("SPELL_QUEUE_EVENT")
|
|
events:RegisterEvent("START_AUTOATTACK")
|
|
events:RegisterEvent("STOP_AUTOATTACK")
|
|
|
|
events:SetScript("OnEvent", function()
|
|
if event == "AUTO_ATTACK_SELF" then
|
|
local hitInfo = arg4 or 0
|
|
local isOffhand = bit.band(hitInfo, HITINFO_LEFTSWING) ~= 0
|
|
local noAction = bit.band(hitInfo, HITINFO_NOACTION) ~= 0
|
|
-- NOACTION means server did not advance swing clock (dodge/parry/miss).
|
|
-- Only skip if the timer is already running - if it's not active yet
|
|
-- (first swing ever), we still want to start it so the bar appears.
|
|
if noAction then
|
|
if isOffhand and S.ohActive then return end
|
|
if not isOffhand and S.mhActive then return end
|
|
end
|
|
|
|
-- Extra attack detection: if timer still has >20% remaining for that hand,
|
|
-- the server did NOT reset the swing clock -> this is an extra attack, skip.
|
|
-- Use 20% here (SP_SwingTimer's ShouldResetTimer threshold).
|
|
-- Exception: if timer is already at 0 (expired), always accept.
|
|
if isOffhand then
|
|
local pct = S.ohActive and (S.ohTimer / S.ohTimerMax) or 0
|
|
if S.ohActive and S.ohTimer > 0 and pct > 0.20 then
|
|
return
|
|
end
|
|
ResetOH()
|
|
else
|
|
local pct = S.mhActive and (S.mhTimer / S.mhTimerMax) or 0
|
|
if S.mhActive and S.mhTimer > 0 and pct > 0.20 then
|
|
return
|
|
end
|
|
ResetMH()
|
|
end
|
|
|
|
elseif event == "AUTO_ATTACK_OTHER" then
|
|
-- Parry haste: enemy attacked the player and player parried
|
|
local targetGuid = arg2
|
|
if not targetGuid or not IsPlayerGuid(targetGuid) then return end
|
|
local victimState = arg5 or 0
|
|
-- VICTIMSTATE_PARRY = 3
|
|
-- Vanilla: parry reduces the NEXT swing timer by 40% of weapon speed,
|
|
-- minimum 20% of weapon speed remaining (SP_SwingTimer approach)
|
|
if victimState == 3 then
|
|
-- Apply to whichever swing comes next (smallest % remaining = closest to firing)
|
|
if S.ohActive and S.ohSpeed > 0 and (S.ohTimer / S.ohTimerMax) < (S.mhTimer / S.mhTimerMax) then
|
|
local minimum = S.ohSpeed * 0.20
|
|
if S.ohTimer > minimum then
|
|
local reduct = S.ohSpeed * 0.40
|
|
local before = S.ohTimer
|
|
S.ohTimer = S.ohTimer - reduct
|
|
if S.ohTimer < minimum then S.ohTimer = minimum end
|
|
end
|
|
elseif S.mhActive and S.mhSpeed > 0 then
|
|
local minimum = S.mhSpeed * 0.20
|
|
if S.mhTimer > minimum then
|
|
local reduct = S.mhSpeed * 0.40
|
|
local before = S.mhTimer
|
|
S.mhTimer = S.mhTimer - reduct
|
|
if S.mhTimer < minimum then S.mhTimer = minimum end
|
|
end
|
|
else
|
|
end
|
|
end
|
|
|
|
elseif event == "SPELL_QUEUE_EVENT" then
|
|
local eventCode = arg1 or -1
|
|
local spellId = arg2 or 0
|
|
if eventCode == ON_SWING_QUEUED then
|
|
S.useSpellQueueEvent = true
|
|
SetQueuedKind(ClassifyOnSwingSpell(spellId))
|
|
elseif eventCode == ON_SWING_QUEUE_POPPED then
|
|
S.hsQueued = false; S.cleaveQueued = false; S.maulQueued = false
|
|
end
|
|
|
|
elseif event == "START_AUTOATTACK" then
|
|
S.autoAttackActive = true
|
|
|
|
elseif event == "STOP_AUTOATTACK" then
|
|
S.autoAttackActive = false
|
|
|
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
|
local _, class = UnitClass("player")
|
|
S.isWarrior = (class == "WARRIOR")
|
|
S.isDruid = (class == "DRUID")
|
|
UpdateWeaponSpeeds()
|
|
RebuildQueueSlotCache()
|
|
|
|
elseif event == "PLAYER_EQUIPMENT_CHANGED" then
|
|
-- arg1 = changed slot; only weapon slots (main/off/ranged) affect swing speed
|
|
if not INVSLOTS_EQUIPABLE_IN_COMBAT[arg1] then return end
|
|
UpdateWeaponSpeeds()
|
|
if S.ohSpeed == 0 then
|
|
S.ohActive = false
|
|
pfUI.swingtimer.offhand:Hide()
|
|
end
|
|
if S.raSpeed == 0 then
|
|
S.raActive = false
|
|
pfUI.swingtimer.ranged:Hide()
|
|
end
|
|
|
|
elseif event == "ACTIONBAR_SLOT_CHANGED" then
|
|
RebuildQueueSlotCache()
|
|
|
|
elseif event == "PLAYER_REGEN_DISABLED" then
|
|
S.inCombat = true
|
|
UpdateWeaponSpeeds()
|
|
|
|
elseif event == "PLAYER_REGEN_ENABLED" then
|
|
S.inCombat = false
|
|
S.hsQueued = false
|
|
S.cleaveQueued = false
|
|
S.maulQueued = false
|
|
|
|
elseif event == "UNIT_DIED" then
|
|
if IsPlayerGuid(arg1) then
|
|
ResetAll()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- -------------------------------------------------------------------------
|
|
-- Public API for external addons (e.g. SCRM)
|
|
-- Only available when the swingtimer module is loaded.
|
|
-- Usage:
|
|
-- if pfUI and pfUI.swingtimer and pfUI.swingtimer.api then
|
|
-- local api = pfUI.swingtimer.api
|
|
-- local mhTimer = api.GetMHTimer()
|
|
-- end
|
|
-- -------------------------------------------------------------------------
|
|
pfUI.swingtimer.api = {
|
|
-- Remaining time in seconds until next swing
|
|
GetMHTimer = function() return S.mhTimer end,
|
|
GetOHTimer = function() return S.ohTimer end,
|
|
GetRangedTimer = function() return S.raTimer end,
|
|
|
|
-- Weapon speed (full swing duration in seconds)
|
|
GetMHSpeed = function() return S.mhSpeed end,
|
|
GetOHSpeed = function() return S.ohSpeed end,
|
|
GetRangedSpeed = function() return S.raSpeed end,
|
|
|
|
-- Whether each timer is currently running
|
|
IsMHActive = function() return S.mhActive end,
|
|
IsOHActive = function() return S.ohActive end,
|
|
IsRangedActive = function() return S.raActive end,
|
|
|
|
-- Swing progress 0.0 (just reset) to 1.0 (about to swing)
|
|
GetMHProgress = function()
|
|
return S.mhActive and S.mhTimerMax > 0 and (1 - S.mhTimer / S.mhTimerMax) or 0
|
|
end,
|
|
GetOHProgress = function()
|
|
return S.ohActive and S.ohTimerMax > 0 and (1 - S.ohTimer / S.ohTimerMax) or 0
|
|
end,
|
|
GetRangedProgress = function()
|
|
return S.raActive and S.raTimerMax > 0 and (1 - S.raTimer / S.raTimerMax) or 0
|
|
end,
|
|
|
|
-- Warrior HS/Cleave queue state
|
|
IsHSQueued = function() return S.hsQueued end,
|
|
IsCleaveQueued = function() return S.cleaveQueued end,
|
|
IsMaulQueued = function() return S.maulQueued end,
|
|
}
|
|
|
|
UpdateWeaponSpeeds()
|
|
end) |