Repository navigation
Expand file tree
/
Copy pathTimeline.lua
More file actions
140 lines (128 loc) · 5.61 KB
/
Copy pathTimeline.lua
File metadata and controls
140 lines (128 loc) · 5.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
-- Feeds confident predictions into Blizzard's own encounter timeline.
--
-- Why not draw our own: the game already ships the sliding-icon timeline, and
-- the player positions, orients and sizes it in Edit Mode. Blizzard explicitly
-- supports addon events outside boss fights - see the comment in
-- Blizzard_EncounterTimeline/EncounterTimeline.lua:EvaluateVisibility, "Also
-- works for custom events".
--
-- Why only *confident* predictions: the timeline has no way for an addon to say
-- a time is an estimate (`isApproximate` exists on the event info but not on the
-- request), so anything uncertain would be drawn as though it were exact. Those
-- stay on the nameplate icons, which can show "~" and "!" honestly.
CastAheadTimeline = {}
local T = CastAheadTimeline
local function Record(...)
local recorder = CastAheadRecorder
if recorder and recorder.Enabled() then recorder.Note("TL", nil, ...) end
end
-- Re-adding is the only way to change an event's time, and each add replays the
-- icon's entry animation. Below this drift it is better to leave it alone.
local RETIME_THRESHOLD = 1.0
local QUEUE_GRACE = 4.0 -- how long an overdue event lingers at the line
local function Available()
return C_EncounterTimeline and C_EncounterTimeline.AddScriptEvent
and CastAheadConfig.Enabled("timeline")
-- The player can switch the timeline off entirely; then this is a no-op
-- and the nameplate icons carry everything.
and (not C_EncounterTimeline.IsFeatureEnabled or C_EncounterTimeline.IsFeatureEnabled())
end
-- Role and effect badges the timeline draws for us, so "tank buster" and "raid
-- damage" need no text of our own - handy, because the spell name only shows in
-- vertical orientation and only when the player enabled it.
local function IconMask(advice)
local mask = Enum and Enum.EncounterEventIconmask
if not mask or not advice then return nil end
if advice.key == "TANK" then
return bit.bor(mask.TankRole or 0, mask.DeadlyEffect or 0)
elseif advice.key == "AOE" then
return bit.bor(mask.DpsRole or 0, mask.HealerRole or 0)
elseif advice.key == "KICK" then
return mask.MagicEffect
end
return nil
end
-- What the bar is labelled with. The verdict's spoken line is the right words
-- - the bar has room for a phrase, and "tank buster" says more there than the
-- nameplate icon's cramped "BUSTER" - but it is written for a voice, so it
-- arrives in lower case. The game's own entries on this bar are spell names in
-- ordinary case, so ours are capitalised to match rather than shouted.
local function TimelineName(advice, info, row)
local say = advice and advice.say
if say and say ~= "" then
return say:sub(1, 1):upper() .. say:sub(2)
end
return (info and info.name) or row.name or ""
end
local function Severity(advice)
local levels = Enum and Enum.EncounterEventSeverity
if not levels then return nil end
if advice and (advice.key == "TANK" or advice.key == "AOE") then
return levels.High
end
return levels.Medium
end
function T.Cancel(track)
if not track or not track.timelineEventID then return end
if C_EncounterTimeline and C_EncounterTimeline.CancelScriptEvent then
-- Only ever our own id. CancelAllScriptEvents would take out every other
-- addon's events too.
pcall(C_EncounterTimeline.CancelScriptEvent, track.timelineEventID)
end
Record("cancelled", track.timelineSpell)
track.timelineEventID = nil
track.timelineTargetAt = nil
end
-- The cast we predicted has started: let the event resolve rather than yanking
-- it off the timeline.
function T.Finish(track)
if not track or not track.timelineEventID then return end
if C_EncounterTimeline and C_EncounterTimeline.FinishScriptEvent then
pcall(C_EncounterTimeline.FinishScriptEvent, track.timelineEventID)
end
Record("finished", track.timelineSpell)
track.timelineEventID = nil
track.timelineTargetAt = nil
end
-- `confident` is decided by the caller: one candidate, a real measured schedule,
-- and enough observations behind it.
function T.Sync(track, targetAt, row, advice, confident)
if not track then return end
if not (confident and targetAt and row and Available()) then
T.Cancel(track)
return
end
local remaining = targetAt - GetTime()
if remaining <= 0 then
T.Cancel(track)
return
end
-- Already showing this cast at almost this time: leave it be, or the icon
-- restarts its entry animation on every recalculation.
if track.timelineEventID and track.timelineSpell == row.spell
and track.timelineTargetAt
and math.abs(track.timelineTargetAt - targetAt) < RETIME_THRESHOLD then
return
end
T.Cancel(track)
local info = C_Spell.GetSpellInfo(row.spell)
local icon = C_Spell.GetSpellTexture and C_Spell.GetSpellTexture(row.spell)
local request = {
spellID = row.spell,
iconFileID = icon or (info and info.iconID) or 136243,
duration = remaining,
maxQueueDuration = QUEUE_GRACE,
overrideName = TimelineName(advice, info, row),
severity = Severity(advice),
}
local icons = IconMask(advice)
if icons then request.icons = icons end
local ok, eventID = pcall(C_EncounterTimeline.AddScriptEvent, request)
if ok and eventID then
track.timelineEventID = eventID
track.timelineTargetAt = targetAt
track.timelineSpell = row.spell
local key = CastAheadRecorder and CastAheadRecorder.Current()
Record("added", row.spell, key and math.floor((targetAt - key.startedAt) * 1000 + 0.5) or "?")
end
end