81 lines
4.3 KiB
Lua
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
|