mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 07:36:56 +00:00
993 lines
42 KiB
Lua
993 lines
42 KiB
Lua
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 = UnitClassBase("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 = C_Spell.GetSpellName(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 = UnitClassBase("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) |