mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 15:46:56 +00:00
b11deceb26
Fixed ResetSwingTimers scope error causing nil call in OnUpdate
620 lines
25 KiB
Lua
620 lines
25 KiB
Lua
pfUI:RegisterModule("swingtimer", "vanilla:tbc", function ()
|
|
local rawborder, border = GetBorderSize()
|
|
|
|
-- HitInfo flags (EVENTS.md)
|
|
local HITINFO_LEFTSWING = 4 -- 0x4: Off-hand attack
|
|
local HITINFO_NOACTION = 65536 -- 0x10000: server did not advance the swing clock
|
|
|
|
-- SPELL_QUEUE_EVENT codes (EVENTS.md)
|
|
local ON_SWING_QUEUED = 0
|
|
local ON_SWING_QUEUE_POPPED = 1
|
|
|
|
-- Swing state
|
|
local swingState = {
|
|
mainhand = { speed = 0, nextSwing = 0, swinging = false },
|
|
offhand = { speed = 0, nextSwing = 0, swinging = false },
|
|
ranged = { speed = 0, nextSwing = 0, swinging = false },
|
|
}
|
|
|
|
-- Ranged spell IDs that trigger the ranged swing timer (replaces MH)
|
|
local RANGED_SPELLIDS = {
|
|
[75] = true, -- Auto Shot (Hunter)
|
|
[2764] = true, -- Throw (Warrior/Rogue)
|
|
}
|
|
|
|
-- Create container frame
|
|
pfUI.swingtimer = CreateFrame("Frame", "pfSwingTimer", UIParent)
|
|
pfUI.swingtimer:SetFrameStrata("MEDIUM")
|
|
pfUI.swingtimer:Hide()
|
|
|
|
-- Read config once at load into locals
|
|
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"
|
|
|
|
-- Parse color strings "r,g,b,a" into components
|
|
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"
|
|
|
|
-- Store default MH color for HS/Cleave restore
|
|
local mhDefaultR, mhDefaultG, mhDefaultB = mhR, mhG, mhB
|
|
|
|
-- Mainhand bar
|
|
pfUI.swingtimer.mainhand = CreateFrame("StatusBar", "pfSwingTimerMainhand", UIParent)
|
|
pfUI.swingtimer.mainhand:SetPoint("CENTER", UIParent, "CENTER", 0, -100)
|
|
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 "")
|
|
|
|
CreateBackdrop(pfUI.swingtimer.mainhand)
|
|
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 "")
|
|
|
|
CreateBackdrop(pfUI.swingtimer.offhand)
|
|
CreateBackdropShadow(pfUI.swingtimer.offhand)
|
|
|
|
-- Ranged bar (bow/gun/crossbow - triggered by SPELL_GO_SELF for Auto Shot / Throw)
|
|
-- Hunter uses a special "close from outside->in, open inside->out" animation
|
|
-- instead of a normal left->right StatusBar fill.
|
|
pfUI.swingtimer.ranged = CreateFrame("Frame", "pfSwingTimerRanged", UIParent)
|
|
pfUI.swingtimer.ranged:SetPoint("CENTER", UIParent, "CENTER", 0, -120)
|
|
pfUI.swingtimer.ranged:SetWidth(sw_width)
|
|
pfUI.swingtimer.ranged:SetHeight(sw_height)
|
|
pfUI.swingtimer.ranged:Hide()
|
|
|
|
-- Phase 1: left half, anchored to CENTER (right edge fixed), shrinks leftward = outside->in
|
|
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)
|
|
|
|
-- Phase 1: right half, anchored to CENTER (left edge fixed), shrinks rightward = outside->in
|
|
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)
|
|
|
|
-- Phase 2: warning color, anchored CENTER, grows outward
|
|
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 "")
|
|
|
|
CreateBackdrop(pfUI.swingtimer.ranged)
|
|
CreateBackdropShadow(pfUI.swingtimer.ranged)
|
|
|
|
-- HS/Cleave queue state
|
|
local hsQueued = false
|
|
local cleaveQueued = false
|
|
local isWarrior = false
|
|
local cachedHSSlots = {}
|
|
local cachedCleaveSlots = {}
|
|
local useSpellQueueEvent = false
|
|
|
|
-- Heroic Strike spell IDs (all ranks)
|
|
local hsSpellIDs = {
|
|
[78] = true, [284] = true, [285] = true, [1608] = true,
|
|
[11564] = true, [11565] = true, [11566] = true, [11567] = true,
|
|
[25286] = true,
|
|
}
|
|
-- Cleave spell IDs (all ranks)
|
|
local cleaveSpellIDs = {
|
|
[845] = true, [7369] = true, [11608] = true, [11609] = true,
|
|
[20569] = true,
|
|
}
|
|
|
|
local function RebuildQueueSlotCache()
|
|
if not isWarrior or not sw_hsqueue or useSpellQueueEvent then return end
|
|
|
|
cachedHSSlots = {}
|
|
cachedCleaveSlots = {}
|
|
|
|
for slot = 1, 120 do
|
|
local tex = GetActionTexture(slot)
|
|
local name = GetActionText(slot)
|
|
|
|
if tex then
|
|
if string.find(tex, "Ability_Rogue_Ambush") then
|
|
table.insert(cachedHSSlots, slot)
|
|
elseif string.find(tex, "Ability_Warrior_Cleave") then
|
|
table.insert(cachedCleaveSlots, slot)
|
|
end
|
|
end
|
|
|
|
if name then
|
|
local lower = string.lower(name)
|
|
if lower == "heroic strike" or lower == "heroicstrike" or lower == "hs" then
|
|
table.insert(cachedHSSlots, slot)
|
|
elseif lower == "cleave" then
|
|
table.insert(cachedCleaveSlots, slot)
|
|
end
|
|
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
|
|
|
|
local function IsHSOrCleaveQueued()
|
|
if not sw_hsqueue or not isWarrior then return false, false end
|
|
if useSpellQueueEvent then
|
|
return hsQueued, cleaveQueued
|
|
end
|
|
return CheckQueuedAction(cachedHSSlots), CheckQueuedAction(cachedCleaveSlots)
|
|
end
|
|
|
|
UpdateMovable(pfUI.swingtimer.mainhand)
|
|
UpdateMovable(pfUI.swingtimer.ranged)
|
|
|
|
-- VERSION B TEST: HasOffhandWeapon() removed.
|
|
-- The original used GetItemInfo() to detect OH weapon type, but GetItemInfo()
|
|
-- returns nil on first login before the item cache is populated, causing
|
|
-- offhand.speed to stay 0. Now we just read offhandAttackTime directly,
|
|
-- same as the old working version.
|
|
-- Check offhand slot for an actual weapon. GetItemInfo may return nil on first
|
|
-- login (item cache not yet populated), so we return nil in that case to signal
|
|
-- "unknown" rather than false, allowing the caller to keep the previous value.
|
|
-- inventoryType 13 = INVTYPE_WEAPONOFFHAND, 21 = INVTYPE_WEAPON (one-hand, dual wieldable)
|
|
-- Shields = 14, held-in-hand = 23, everything else = no swing
|
|
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
|
|
local s = GetItemStats and GetItemStats(id)
|
|
if not s then return false end
|
|
return OH_WEAPON_TYPES[s.inventoryType] == true
|
|
end
|
|
|
|
local function UpdateWeaponSpeeds()
|
|
if not GetUnitField then return end
|
|
|
|
local mhSpeed = GetUnitField("player", "baseAttackTime")
|
|
local ohSpeed = GetUnitField("player", "offhandAttackTime")
|
|
|
|
if mhSpeed and mhSpeed > 0 then
|
|
swingState.mainhand.speed = mhSpeed / 1000
|
|
end
|
|
|
|
if HasOffhandWeapon() and ohSpeed and ohSpeed > 0 then
|
|
swingState.offhand.speed = ohSpeed / 1000
|
|
else
|
|
swingState.offhand.speed = 0
|
|
end
|
|
|
|
local raSpeed = GetUnitField("player", "rangedAttackTime")
|
|
if raSpeed and raSpeed > 0 then
|
|
swingState.ranged.speed = raSpeed / 1000
|
|
else
|
|
swingState.ranged.speed = 0
|
|
end
|
|
end
|
|
|
|
local function StartSwing(isOffhand)
|
|
local now = GetTime()
|
|
|
|
-- always refresh speeds to catch haste buffs/debuffs
|
|
UpdateWeaponSpeeds()
|
|
|
|
-- dual-wield guard: if MH swing just started (<100ms ago) and this isn't
|
|
-- flagged as offhand, it's likely an OH event with missing flag
|
|
if not isOffhand and swingState.offhand.speed > 0 then
|
|
local mhAge = now - (swingState.mainhand.nextSwing - swingState.mainhand.speed)
|
|
if swingState.mainhand.swinging and mhAge > 0 and mhAge < 0.1 then
|
|
isOffhand = true
|
|
end
|
|
end
|
|
|
|
if isOffhand and swingState.offhand.speed > 0 then
|
|
-- Continue from previous nextSwing to avoid timer jumps
|
|
local base = (swingState.offhand.swinging and swingState.offhand.nextSwing > (now - 0.5)) and swingState.offhand.nextSwing or now
|
|
swingState.offhand.nextSwing = base + swingState.offhand.speed
|
|
swingState.offhand.swinging = true
|
|
if sw_showoh then pfUI.swingtimer.offhand:Show() end
|
|
else
|
|
-- Continue from previous nextSwing to avoid timer jumps on HS/normal swing
|
|
local base = (swingState.mainhand.swinging and swingState.mainhand.nextSwing > (now - 0.5)) and swingState.mainhand.nextSwing or now
|
|
swingState.mainhand.nextSwing = base + swingState.mainhand.speed
|
|
swingState.mainhand.swinging = true
|
|
pfUI.swingtimer.mainhand:Show()
|
|
end
|
|
|
|
pfUI.swingtimer:Show()
|
|
end
|
|
|
|
local function StartRangedSwing()
|
|
if not sw_showranged then return end
|
|
UpdateWeaponSpeeds()
|
|
if swingState.ranged.speed <= 0 then return end
|
|
-- Ranged replaces MH: cancel mainhand swing
|
|
swingState.mainhand.swinging = false
|
|
pfUI.swingtimer.mainhand:Hide()
|
|
swingState.ranged.nextSwing = GetTime() + swingState.ranged.speed
|
|
swingState.ranged.swinging = true
|
|
|
|
if isHunter then
|
|
-- Hunter: left/right halves anchored to CENTER, shrink outside->in
|
|
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
|
|
-- Non-Hunter: left anchored to TOPLEFT, grows left->right like MH/OH
|
|
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 ResetSwingTimers()
|
|
swingState.mainhand.swinging = false
|
|
swingState.offhand.swinging = false
|
|
swingState.ranged.swinging = false
|
|
pfUI.swingtimer.mainhand:Hide()
|
|
pfUI.swingtimer.offhand:Hide()
|
|
pfUI.swingtimer.ranged:Hide()
|
|
pfUI.swingtimer:Hide()
|
|
end
|
|
|
|
local swingThrottle = 0
|
|
pfUI.swingtimer:SetScript("OnUpdate", function()
|
|
swingThrottle = swingThrottle + arg1
|
|
if swingThrottle < 0.016 then return end
|
|
swingThrottle = 0
|
|
local now = GetTime()
|
|
local anyActive = false
|
|
|
|
-- Execute delayed regen reset (only if combat hasn't resumed)
|
|
if swingState.regenResetPending and now >= swingState.regenResetPending then
|
|
swingState.regenResetPending = nil
|
|
ResetSwingTimers()
|
|
end
|
|
|
|
local curR, curG, curB = mhDefaultR, mhDefaultG, mhDefaultB
|
|
if sw_hsqueue and 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
|
|
|
|
if swingState.mainhand.swinging then
|
|
local remaining = swingState.mainhand.nextSwing - now
|
|
|
|
if remaining <= 0 then
|
|
swingState.mainhand.swinging = false
|
|
pfUI.swingtimer.mainhand:Hide()
|
|
else
|
|
local progress = 1 - (remaining / swingState.mainhand.speed)
|
|
pfUI.swingtimer.mainhand:SetValue(progress)
|
|
pfUI.swingtimer.mainhand:SetStatusBarColor(curR, curG, curB, mhA)
|
|
if sw_showtext then
|
|
pfUI.swingtimer.mainhand.text:SetText(string.format("%.1f", remaining))
|
|
end
|
|
anyActive = true
|
|
end
|
|
end
|
|
|
|
if sw_showoh and swingState.offhand.swinging then
|
|
local remaining = swingState.offhand.nextSwing - now
|
|
|
|
if remaining <= 0 then
|
|
swingState.offhand.swinging = false
|
|
pfUI.swingtimer.offhand:Hide()
|
|
else
|
|
local progress = 1 - (remaining / swingState.offhand.speed)
|
|
pfUI.swingtimer.offhand:SetValue(progress)
|
|
if sw_showtext then
|
|
pfUI.swingtimer.offhand.text:SetText(string.format("%.1f", remaining))
|
|
end
|
|
anyActive = true
|
|
end
|
|
elseif not sw_showoh then
|
|
pfUI.swingtimer.offhand:Hide()
|
|
end
|
|
|
|
if sw_showranged and swingState.ranged.swinging then
|
|
local remaining = swingState.ranged.nextSwing - now
|
|
|
|
if remaining <= 0 then
|
|
swingState.ranged.swinging = false
|
|
pfUI.swingtimer.ranged:Hide()
|
|
else
|
|
if isHunter then
|
|
-- Hunter ranged animation: two phases
|
|
-- Phase 1 (speed-0.5s): bar visible full, shrinks from outside->in toward center (normal color)
|
|
-- Phase 2 (0.5s): bar grows from center outward (warning color)
|
|
local DEADZONE = 0.5
|
|
local halfW = sw_width / 2
|
|
|
|
if remaining > DEADZONE then
|
|
-- Phase 1: left/right halves shrink from outside->in toward center
|
|
local elapsed = swingState.ranged.speed - remaining
|
|
local phase1dur = swingState.ranged.speed - DEADZONE
|
|
local p = elapsed / phase1dur -- 0 = full, 1 = gone
|
|
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
|
|
-- Phase 2: warning color grows from center->outside
|
|
local p = 1 - (remaining / DEADZONE) -- 0 = nothing, 1 = full
|
|
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
|
|
else
|
|
-- Non-Hunter (Warrior Throw, Rogue): simple left->right fill like MH/OH
|
|
local progress = 1 - (remaining / swingState.ranged.speed)
|
|
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()
|
|
end
|
|
if sw_showtext then
|
|
if isHunter and remaining <= 0.5 then
|
|
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", remaining))
|
|
elseif isHunter then
|
|
-- Show time until deadzone starts, not full remaining
|
|
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", remaining - 0.5))
|
|
else
|
|
pfUI.swingtimer.ranged.text:SetText(string.format("%.1f", remaining))
|
|
end
|
|
end
|
|
anyActive = true
|
|
end
|
|
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)
|
|
|
|
local events = CreateFrame("Frame")
|
|
events:RegisterEvent("AUTO_ATTACK_SELF")
|
|
events:RegisterEvent("AUTO_ATTACK_OTHER")
|
|
events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
events:RegisterEvent("UNIT_INVENTORY_CHANGED")
|
|
events:RegisterEvent("PLAYER_REGEN_DISABLED")
|
|
events:RegisterEvent("PLAYER_REGEN_ENABLED")
|
|
events:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
|
|
events:RegisterEvent("UNIT_DIED")
|
|
events:RegisterEvent("SPELL_QUEUE_EVENT")
|
|
|
|
-- Use libdebuff SPELL_GO_SELF hook instead of registering the event separately
|
|
pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {}
|
|
pfUI.libdebuff_spell_go_hooks["swingtimer"] = function(spellId)
|
|
if RANGED_SPELLIDS[spellId] then
|
|
StartRangedSwing()
|
|
elseif hsSpellIDs[spellId] then
|
|
hsQueued = false; cleaveQueued = false
|
|
StartSwing(false)
|
|
elseif cleaveSpellIDs[spellId] then
|
|
hsQueued = false; cleaveQueued = false
|
|
StartSwing(false)
|
|
end
|
|
end
|
|
|
|
-- Use libdebuff SPELL_CAST_EVENT hook instead of registering the event separately
|
|
pfUI.libdebuff_spell_cast_hooks = pfUI.libdebuff_spell_cast_hooks or {}
|
|
pfUI.libdebuff_spell_cast_hooks["swingtimer"] = function(success, spellId)
|
|
if success ~= 1 then return end
|
|
if hsSpellIDs[spellId] then
|
|
hsQueued = true; cleaveQueued = false
|
|
elseif cleaveSpellIDs[spellId] then
|
|
cleaveQueued = true; hsQueued = false
|
|
end
|
|
end
|
|
|
|
local playerGUID = nil
|
|
|
|
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
|
|
-- HITINFO_NOACTION: server did not advance the swing clock, ignore
|
|
if noAction then return end
|
|
StartSwing(isOffhand)
|
|
|
|
elseif event == "AUTO_ATTACK_OTHER" then
|
|
if not swingState.mainhand.swinging then return end
|
|
local targetGuid = arg2
|
|
if not targetGuid or not playerGUID then return end
|
|
if targetGuid ~= playerGUID then return end
|
|
local victimState = arg5 or 0
|
|
if victimState == 3 then
|
|
local now = GetTime()
|
|
local remaining = swingState.mainhand.nextSwing - now
|
|
local reduction = swingState.mainhand.speed * 0.4
|
|
local minRemaining = swingState.mainhand.speed * 0.2
|
|
local newRemaining = remaining - reduction
|
|
if newRemaining < minRemaining then newRemaining = minRemaining end
|
|
if newRemaining < remaining then
|
|
swingState.mainhand.nextSwing = now + newRemaining
|
|
end
|
|
end
|
|
|
|
elseif event == "SPELL_QUEUE_EVENT" then
|
|
local eventCode = arg1 or -1
|
|
local spellId = arg2 or 0
|
|
if eventCode == ON_SWING_QUEUED then
|
|
useSpellQueueEvent = true
|
|
if hsSpellIDs[spellId] then
|
|
hsQueued = true; cleaveQueued = false
|
|
elseif cleaveSpellIDs[spellId] then
|
|
cleaveQueued = true; hsQueued = false
|
|
else
|
|
end
|
|
elseif eventCode == ON_SWING_QUEUE_POPPED then
|
|
hsQueued = false; cleaveQueued = false
|
|
else
|
|
end
|
|
|
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
|
local _, class = UnitClass("player")
|
|
isWarrior = (class == "WARRIOR")
|
|
local _, guid = UnitExists("player")
|
|
playerGUID = guid
|
|
UpdateWeaponSpeeds()
|
|
RebuildQueueSlotCache()
|
|
|
|
elseif event == "UNIT_INVENTORY_CHANGED" then
|
|
if arg1 and arg1 ~= "player" then return end
|
|
UpdateWeaponSpeeds()
|
|
if swingState.offhand.speed == 0 then
|
|
swingState.offhand.swinging = false
|
|
pfUI.swingtimer.offhand:Hide()
|
|
end
|
|
if swingState.ranged.speed == 0 then
|
|
swingState.ranged.swinging = false
|
|
pfUI.swingtimer.ranged:Hide()
|
|
end
|
|
|
|
elseif event == "ACTIONBAR_SLOT_CHANGED" then
|
|
RebuildQueueSlotCache()
|
|
|
|
elseif event == "PLAYER_REGEN_DISABLED" then
|
|
swingState.regenResetPending = nil -- cancel delayed reset, combat resumed
|
|
UpdateWeaponSpeeds()
|
|
|
|
elseif event == "PLAYER_REGEN_ENABLED" then
|
|
-- Delay reset to avoid false resets between mobs in raids
|
|
-- If combat resumes within 2s (PLAYER_REGEN_DISABLED), cancel the reset
|
|
swingState.regenResetPending = GetTime() + 2.0
|
|
hsQueued = false
|
|
cleaveQueued = false
|
|
|
|
elseif event == "UNIT_DIED" then
|
|
-- Only reset if the player themselves died
|
|
local guid = arg1
|
|
if not guid then return end
|
|
if guid == playerGUID then
|
|
ResetSwingTimers()
|
|
end
|
|
end
|
|
end)
|
|
|
|
UpdateWeaponSpeeds()
|
|
end) |