Files
BulwarkFrame/core/swing.lua
T

81 lines
4.3 KiB
Lua

-- BulwarkFrame -- swing-timer arithmetic (pure).
--
-- Why we build this instead of taking a dependency: the only Turtle-tested swing timer in
-- circulation (SP_SwingTimer) resets on LOCALISED combat-log strings, which is fragile, and it
-- has no extension points for the shaman specifics we care about -- Windfury extra attacks and
-- Flurry haste. Both of those break a generic timer, so a small own module is easier to keep
-- honest than a foreign addon to patch.
--
-- Two building blocks, both confirmed:
-- * swing DURATION -- UnitAttackSpeed("player") returns (mainhand, offhand) in seconds.
-- Present in 1.12: Blizzard's own PaperDollFrame.lua:285 uses it.
-- * swing RESET -- SuperWoW's UNIT_CASTEVENT with arg3 = "MAINHAND"/"OFFHAND". Language
-- independent and fires on misses too, unlike string parsing.
-- Neither is touched here; this file is arithmetic only, so it runs under plain Lua 5.0.3.
BulwarkFrameSwing = {}
local S = BulwarkFrameSwing
local function clamp(v, lo, hi)
if v < lo then return lo end
if v > hi then return hi end
return v
end
-- hasSwung(lastSwingAt): is there a swing to draw at all?
-- 0 doubles as the "never swung" sentinel, matching how the runtime layer will initialise it.
function S.hasSwung(lastSwingAt)
return type(lastSwingAt) == "number" and lastSwingAt > 0
end
-- progress(lastSwingAt, now, speed) -> 0..1 through the current swing.
--
-- Saturates at 1 rather than wrapping: between swings (target dead, out of range, moving) the
-- marker should sit still at the end, not sweep round again as if an attack were imminent.
--
-- NOTE ON HASTE -- deliberately not decided here. When Flurry changes attack speed mid-swing it
-- is unverified whether the running swing rescales or the new speed only applies to the next
-- one. `speed` is therefore an argument: pass the value captured at swing start for the
-- "next swing only" reading, or the live value for the "rescales" reading.
-- LATENCY -- why it is an argument and not a constant.
--
-- The swing happens on the SERVER at T. The client only learns about it when the event
-- arrives, so the timestamp we record is already T + downstream. Everything derived from it is
-- therefore late by that much, and the two plausible corrections are NOT the same number:
--
-- * "show the true state" -> shift by the downstream leg alone (~rtt/2)
-- * "show the action point" -> shift by the FULL rtt, because a keypress still needs the
-- upstream leg to reach the server. The two legs ADD; they do
-- not cancel.
--
-- Which one is wanted is a display decision, so this module takes a plain `offset` in seconds
-- and applies it, rather than picking a model. Measured on this character: mean rtt 78 ms,
-- p90 82 ms, spikes to 253 ms -- i.e. 3% of a 2.6 s swing normally and 10% on a spike. Worth
-- offering, not worth hardcoding. (GetNetStats() supplies the rtt in ms, but only refreshes
-- every ~30 s in 1.12, so it tracks the baseline and not a spike.)
function S.progress(lastSwingAt, now, speed, offset)
if type(lastSwingAt) ~= "number" or type(now) ~= "number" then return 0 end
if type(speed) ~= "number" or speed <= 0 then return 0 end
if type(offset) ~= "number" then offset = 0 end
return clamp((now - lastSwingAt + offset) / speed, 0, 1)
end
-- remaining(lastSwingAt, now, speed, offset) -> seconds until the next swing, floored at 0.
function S.remaining(lastSwingAt, now, speed, offset)
if type(lastSwingAt) ~= "number" or type(now) ~= "number" then return 0 end
if type(speed) ~= "number" or speed <= 0 then return 0 end
if type(offset) ~= "number" then offset = 0 end
return clamp(speed - (now - lastSwingAt + offset), 0, speed)
end
-- latencySeconds(ms, share): turn GetNetStats' round-trip milliseconds into the offset above.
-- `share` selects the model: 0.5 for the downstream leg ("true state"), 1.0 for the full round
-- trip ("action point"). Defaults to 1.0 -- if a timer is used to decide WHEN TO PRESS, the
-- full trip is the honest number, and erring toward "press slightly early" is the harmless
-- direction: an ability queued a touch too soon still lands, one queued late misses the window.
function S.latencySeconds(ms, share)
if type(ms) ~= "number" or ms <= 0 then return 0 end
if type(share) ~= "number" then share = 1.0 end
return (ms / 1000) * share
end