pfUI:RegisterModule("swingtimer", "vanilla:tbc", 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 -- 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, isWarrior = false, cachedHSSlots = {}, cachedCleaveSlots = {}, useSpellQueueEvent = false, playerGUID = nil, swingThrottle = 0, onSwingCache = {}, } -- Ranged spell IDs local RANGED_SPELLIDS = { [75] = true, -- Auto Shot (Hunter) [2764] = true, -- Throw (Warrior/Rogue) } -- Slam: has cast time but does NOT reset the swing timer (just delays it). -- We explicitly ignore these in SPELL_GO so they fall through to no-op. local slamSpellIDs = { [1464] = true, [8820] = true, [11604] = true, [11605] = true, } -- SPELL_ATTR_ON_NEXT_SWING (bit 2, value 4): spell replaces next auto-attack swing. -- Covers Raptor Strike, Maul, Mongoose Bite, Holy Strike, etc. automatically. local ATTR_ON_NEXT_SWING = 4 local function IsOnSwingSpell(spellId) if S.onSwingCache[spellId] ~= nil then return S.onSwingCache[spellId] end local rec = GetSpellRec(spellId) local result = rec and bit.band(rec.attributes, ATTR_ON_NEXT_SWING) ~= 0 or false S.onSwingCache[spellId] = result return result end -- Heroic Strike spell IDs (all ranks) local hsSpellIDs = { [78] = true, [284] = true, [285] = true, [1608] = true, [11564] = true, [11565] = true, [11566] = true, [11567] = true, } -- Cleave spell IDs (all ranks) local cleaveSpellIDs = { [845] = true, [7369] = true, [11608] = true, [11609] = true, [20569] = true, } -- 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("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 "") 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("CENTER", UIParent, "CENTER", 0, -120) 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 local s = GetItemStats and GetItemStats(id) if not s then return false end return OH_WEAPON_TYPES[s.inventoryType] == 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 local function ResetRanged() if not sw_showranged then return end UpdateWeaponSpeeds() if S.raSpeed <= 0 then return end -- Ranged replaces MH bar S.mhActive = false pfUI.swingtimer.mainhand:Hide() 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 helpers local function RebuildQueueSlotCache() if not S.isWarrior or not sw_hsqueue or S.useSpellQueueEvent then return end S.cachedHSSlots = {} S.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(S.cachedHSSlots, slot) elseif string.find(tex, "Ability_Warrior_Cleave") then table.insert(S.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(S.cachedHSSlots, slot) elseif lower == "cleave" then table.insert(S.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 S.isWarrior then return false, false end if S.useSpellQueueEvent then 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 S.mhTimer = S.mhTimer - delta if S.mhTimer <= 0 then S.mhTimer = 0 if S.mhFrozenAt then -- Spell with interruptFlags froze the swing: bar holds at 0, skip grace/hide. -- ResetMH() will clear mhFrozenAt when AUTO_ATTACK_SELF arrives. 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 and 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 -- 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 arg1 and arg1 > 0 then S.pendingCastSpellId = arg1 end 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) if RANGED_SPELLIDS[spellId] then ResetRanged() return elseif slamSpellIDs[spellId] then -- Slam delays auto-attack but does NOT reset the swing timer. Ignore. S.pendingCastSpellId = nil return elseif hsSpellIDs[spellId] or IsOnSwingSpell(spellId) then S.hsQueued = false; S.cleaveQueued = false ResetMH() elseif cleaveSpellIDs[spellId] then S.hsQueued = false; S.cleaveQueued = false ResetMH() else -- Any spell with interruptFlags > 0 resets the swing timer -- (Moonfire, Faerie Fire, Wrath, Starfire etc. - NOT Insect Swarm which has flags=0) local _rec = GetSpellRec(spellId) if _rec and _rec.interruptFlags and _rec.interruptFlags > 0 then if S.mhActive and S.mhSpeed > 0 then UpdateWeaponSpeeds() S.mhTimerMax = S.mhSpeed S.mhTimer = S.mhSpeed end end end S.pendingCastSpellId = nil end -- SPELL_CAST_EVENT hook: HS/Cleave queue tracking 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 S.hsQueued = true; S.cleaveQueued = false elseif cleaveSpellIDs[spellId] then S.cleaveQueued = true; S.hsQueued = false 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") 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 if noAction then return 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 S.playerGUID then return end if targetGuid ~= S.playerGUID 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 if hsSpellIDs[spellId] then S.hsQueued = true; S.cleaveQueued = false elseif cleaveSpellIDs[spellId] then S.cleaveQueued = true; S.hsQueued = false end elseif eventCode == ON_SWING_QUEUE_POPPED then S.hsQueued = false; S.cleaveQueued = 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.playerGUID = GetUnitGUID("player") UpdateWeaponSpeeds() RebuildQueueSlotCache() elseif event == "UNIT_INVENTORY_CHANGED" then if arg1 and arg1 ~= "player" 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 elseif event == "UNIT_DIED" then if arg1 and arg1 == S.playerGUID then ResetAll() end end end) UpdateWeaponSpeeds() end)