mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 15:46:56 +00:00
e017dbf515
* chat: detect whispers before the timestamp is prepended Whisper detection tests for the whisper colour code at position 1, but the timestamp is prepended first, so with timestamps enabled the code is no longer at position 1 and every whisper failed the test -- losing both the recolour and the correct chat-history entry. (cherry picked from commit 4b69d597631c422d9360a94ff129323288a28cc2) * macrotweak + libpredict: inverted install guard, misspelled globals macrotweak: _AddHistoryLine is the backup slot this block creates, so it is nil until line 18 runs. Guarding on its truthiness meant the chat-history filter never installed at all -- macro calls kept landing in chat input history. libpredict: UKNOWNBEING / UNKOWNBEING are misspelled, so both resolve to nil and the guards never matched the real UNKNOWNBEING. Neutral if the global is absent on this client, correct if present. Not needed: his libpredict already fixed the always-true 'event == "A" or "B"' condition (libs/libpredict.lua:828). (cherry picked from commit 4068110dc4134823f1c09f60ca5f8959302cd14f) * mapreveal: look the explore cache up by the key it is actually stored under explorecaches is keyed by the plain area name (line ~160), but the hover frame carried only a decorated 'mapFileName (area)' display string, so every lookup missed and the hover highlight never fired. Store the plain name alongside it and key off that; the tooltip keeps the decorated string. (cherry picked from commit 5e6fe969a03a88c68e28614548b9d44db562ab9a) * socialmod: don't clobber the friend-online match with the offline one The offline match was assigned unconditionally over the online match, so a friend coming online never had lastseen recorded. (cherry picked from commit ad927806c46042163f84bde4db1082726bedf28b) * swingtimer: off-hand weapons are inventory type 22, not 21 The off-hand slot accepts one-hand (13) and off-hand (22) weapons. Type 21 is INVTYPE_WEAPONMAINHAND and can never be equipped there, so the off-hand swing timer never recognised a real off-hand weapon. The other two swingtimer fixes are not needed here: his hunter check already uses UnitClassBase (locale-independent, better than our UnitClass second return), and his generic on-next-swing detection covers Raptor Strike without our hardcoded spell-id list. (cherry picked from commit 9ab7f5f7224d12361ca6cd5a49181cbb4bebd387) * superwow: compare the version numerically, not by exact string SUPERWOW_VERSION == "1.5" silently disables the GUID-to-name combat text hook on any release past 1.5. Currently 1.5 here, so this is forward-compat only. Not needed: the clickthrough slash commands already go through RegisterSlashCommand, which writes _G properly. (cherry picked from commit 82a37752a5782479849ce7e1304c24733c2ff97a) * api: measure a real pixel against UIParent, not the uiScale cvar The uiScale cvar caps at 1.0 while both the pixelperfect module and the firstrun slider push UIParent past it via SetScale, and it is ignored entirely while useUiScale is off, so borders came out the wrong thickness on the Huge/Large presets. Ask the frame for its effective scale instead, and guard an unparseable gxResolution. Also cache GetItemLinkByName and count its failures: the scan walked every id on each call with no memory, so an unresolvable name hitched on every tooltip hover. Raise the ceiling from 25818 to 61000 as well -- Octo/Turtle custom items live well past the vanilla range and never resolved. (cherry picked from commit dac2d3416aef85a4c4b71c0239d93d309e76e30b) * init: load pixelperfect first so GetPerfectPixel caches the right scale pixelperfect sets the UI scale that GetPerfectPixel measures against, and that value is cached on first use. Loading it 56th baked in the previous scale. (cherry picked from commit dd89a210330583f890a51a82b29e968f3ad36b34) * modules: route two global overrides through _G so they leave the sandbox pfUI.env has __index but no __newindex, so a bare global assignment inside a RegisterModule closure is written into the sandbox table and never reaches _G. unitxp's BattlefieldFrame_Show override therefore never fired the BG queue-pop notification, and RaidFrame.lua never saw GROUP_REPLACE_PARTY. The slash-command half of this fix is not needed here: RegisterSlashCommand already writes through _G. (cherry picked from commit 617c8320aca2465af62cf0ff5be7dceaed29a3b2) * bags: remove the duplicate search OnHide handler Two byte-identical frame.search OnHide handlers were installed back to back; the second overwrote the first, so the first was dead code. (cherry picked from commit e77650448b92e5d7f18ae58632e4531cb2813ea6) * buffwatch: invalidate the filter cache when a skill is listed fcache is built once per config table and never cleared, so ctrl/shift-clicking a skill onto the whitelist or blacklist had no effect until the next reload. * roll: actually capture the everyone-passed subject strfind(LOOT_ROLL_ALL_PASSED, LOOT_ROLL_PASSED) has no captures, so `everyone` was always nil, never reached the blacklist, and "Everyone has passed on: X" was recorded as a real player passing. Builds a sample from LOOT_ROLL_ALL_PASSED and runs the same LOOT_ROLL_PASSED match the scanner uses, so the subject is captured the way it will actually appear.
984 lines
41 KiB
Lua
984 lines
41 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
|
|
|
|
-- 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
|
|
|
|
-- On-next-swing spells replace the 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 result = C_Spell.IsNextMeleeSpell(spellId)
|
|
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
|
|
-- OH weapon detection: slot 17 (off hand) accepts one-hand (13) and off-hand
|
|
-- weapons (22); main-hand-only (21) can never sit there.
|
|
local OH_WEAPON_TYPES = { [13]=true, [22]=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. C_Spell.ResetsMeleeSwing
|
|
-- mirrors the server rule; spells that reset don't need freezing (they
|
|
-- reset on SPELL_GO_SELF). Subsumes the old hardcoded swingDelaySpells
|
|
-- list (no list maintenance for new Slam-style spells).
|
|
if S.mhActive then
|
|
if not C_Spell.ResetsMeleeSwing(arg1) 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
|
|
-- C_Spell.ResetsMeleeSwing mirrors the server rule (Turtle's
|
|
-- Spell::IsMeleeAttackResetSpell): InterruptFlags has AUTOATTACK and
|
|
-- AttributesEx2 lacks NOT_RESET_AUTO_ACTIONS. When the spell resets the
|
|
-- swing, snap the timers to full. Otherwise (elseif) a frozen-swing-
|
|
-- during-cast is a Slam-style cast — push the timer forward by the cast
|
|
-- duration so the bar resumes from where it paused.
|
|
if C_Spell.ResetsMeleeSwing(spellId) 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) |