Repository navigation
Expand file tree
/
Copy pathLibGlobalCooldown.lua
More file actions
251 lines (222 loc) · 8.24 KB
/
Copy pathLibGlobalCooldown.lua
File metadata and controls
251 lines (222 loc) · 8.24 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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
local MAJOR_VERSION = "LibGlobalCooldown"
local MINOR_VERSION = 1
local lib = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION)
if not lib then
return
end
-- Cross-client API wrappers (see compat.lua).
local _, st = ...
local api = st.api
-- See compat.lua: a restricted (secret) stat/cooldown reading must degrade to "skip this
-- update", never to an error. Exact no-op where the probe is absent (classic clients).
local is_secret = issecretvalue or function()
return false
end
local CreateFrame = CreateFrame
local C_Timer = C_Timer
local GetCombatRating = api.GetCombatRating
local GetCombatRatingBonus = api.GetCombatRatingBonus
-- Secret-safe for the reads below: compat's GetSpellCooldown reports a restricted
-- (secret) reading as 0/0/0/0, the same shape as a cleared cooldown, which the == 0
-- early-outs in poll_gcd / UNIT_SPELLCAST_INTERRUPTED already handle. No arithmetic
-- below ever sees a secret cooldown value.
local GetSpellCooldown = api.GetSpellCooldown
local GetSpellInfo = api.GetSpellInfo
local GetTime = GetTime
local GetShapeshiftForm = api.GetShapeshiftForm
-- Spell id whose cooldown reading mirrors the global cooldown (family-specific).
local gcd_probe_spell_id = st.gcd_probe_spell_id
local math = math
local select = select
local UnitClass = UnitClass
-- Callbacks and event names
lib.callbacks = lib.callbacks or LibStub("CallbackHandler-1.0"):New(lib)
lib.GCD_OVER = "GCD_OVER"
lib.GCD_STARTED = "GCD_STARTED"
lib.GCD_DURATIONS_UPDATED = "GCD_DURATIONS_UPDATED"
lib.GCD_PHYS_UPDATED = "GCD_PHYS_UPDATED"
lib.GCD_SPELL_UPDATED = "GCD_SPELL_UPDATED"
-- Containers for state tracking
lib.in_combat = false
lib.current_form = nil
-- Containers for active GCD info
lib.gcd_duration = nil
lib.gcd_started = nil
lib.gcd_expires = nil
-- Containers for predicted GCD info
lib.phys_gcd = 1.5 -- will be modified for feral cats
lib.base_spell_gcd = 1.5
lib.current_spell_gcd = nil
lib.has_bloodlust = false
lib.has_breath_haste = false
lib.current_spell_haste = nil
-- A cast time that will be guaranteed to not benefit from a class's
-- instant cast procs. Defaults to Flash of Light, but if the player is a
-- Paladin we'll change this to rank 1 Frostbolt to avoid AoW instant casts.
lib.cast_time_1500ms_spell_id = 19750
function lib:Fire(event, ...)
self.callbacks:Fire(event, ...)
end
-----------------------------------------------------------
-- Funcs to manipulate state and fire events
-----------------------------------------------------------
function lib:calculate_expected_spell_gcd()
-- First try to calc the expected GCD from first principles
local rating_percent_reduction = GetCombatRatingBonus(20)
if is_secret(rating_percent_reduction) then
-- Restricted stat read: keep the previous estimate instead of folding in a
-- value we cannot see (the wrapper would otherwise raise on the arithmetic).
return
end
local spell_gcd = self.base_spell_gcd
local reduction_from_haste_rating = 100 / (100 + rating_percent_reduction)
local current = spell_gcd * reduction_from_haste_rating
if lib.has_bloodlust then
current = current * (1 / 1.3)
end
if lib.has_breath_haste then
current = current * (1 / 1.25)
end
-- Flash of Light has a 1.5s cast time so can be used to check
-- for the spell GCD duration, in case we miss any multiplicative buffs.
-- If there are any debuffs decreasing cast speed, this will be wrong,
-- and we'll fallback on the first principles calc.
local cast_time_test = select(4, GetSpellInfo(self.cast_time_1500ms_spell_id))
cast_time_test = cast_time_test or 1500
cast_time_test = cast_time_test / 1000 -- change from ms to s
-- Get the minimum of the 2 methods, which should be the more accurate.
-- print(current, cast_time_test)
current = math.min(current, cast_time_test)
-- The minimum GCD for all classes is 1 second.
if current < 1 then
current = 1.0
end
-- print(string.format("current: %f, old: %f", current, self.current_spell_gcd))
-- print(current, self.current_spell_gcd)
-- If has changed, fire relevant triggers.
if current ~= self.current_spell_gcd then
self.current_spell_gcd = current
self:Fire(self.GCD_SPELL_UPDATED, self.current_spell_gcd)
self:Fire(self.GCD_DURATIONS_UPDATED, self.phys_gcd, self.current_spell_gcd)
end
-- print(string.format('spell GCD: %f, phys GCD: %f', current, self.phys_gcd))
end
function lib:release_gcd_lock()
self.gcd_duration = nil
self.gcd_started = nil
self:Fire(self.GCD_OVER)
end
function lib:poll_gcd()
-- Function to be called when we have reason to believe a new GCD may
-- have triggered. Locks out new updates when we're on a GCD.
if self.gcd_duration then
return
end
local time_started, duration_reported = GetSpellCooldown(gcd_probe_spell_id)
if duration_reported == 0 then
return
end
local t = GetTime()
local duration_actual = duration_reported - (t - time_started)
local expires = t + duration_actual
self.gcd_duration = duration_reported
self.gcd_started = time_started
self.gcd_expires = expires
-- print(duration_reported)
self:Fire(self.GCD_STARTED, duration_actual, expires)
C_Timer.After(duration_actual, function() self:release_gcd_lock() end)
end
-----------------------------------------------------------
-- Event handlers
-----------------------------------------------------------
function lib:COMBAT_RATING_UPDATE()
local new_spell_haste = GetCombatRating(CR_HASTE_SPELL)
if is_secret(new_spell_haste) then
-- Restricted stat read: keep the previous estimate, never compare or store an
-- opaque value.
return
end
-- print(new_spell_haste)
if new_spell_haste ~= self.current_spell_haste then
-- print('spell haste changed')
self.current_spell_haste = new_spell_haste
self:calculate_expected_spell_gcd()
end
end
function lib:PLAYER_LOGIN()
-- Populate info on class here.
local class = select(2, UnitClass("player"))
-- print("class says: "..tostring(class))
self.class = class
if self.class == "PALADIN" then
self.cast_time_1500ms_spell_id = 116
end
lib.phys_gcd = 1.5
lib.base_spell_gcd = 1.5
-- Set the phys gcds for special cases i.e. rogue/cat.
if class == "ROGUE" then
lib.phys_gcd = 1
elseif self.class == "DRUID" then
local i = GetShapeshiftForm()
if i == 3 then
lib.phys_gcd = 1
end
self.current_form = i
end
self:calculate_expected_spell_gcd()
self:Fire("GCD_DURATIONS_UPDATED", lib.phys_gcd, lib.current_spell_gcd)
end
-- function lib:PLAYER_REGEN_DISABLED()
-- self.in_combat = true
-- end
-- function lib:PLAYER_REGEN_ENABLED()
-- self.in_combat = false
-- end
function lib:SPELL_UPDATE_COOLDOWN()
self:poll_gcd()
end
function lib:UNIT_AURA()
self:calculate_expected_spell_gcd()
end
function lib:UNIT_SPELLCAST_INTERRUPTED()
if self.gcd_duration then
local duration_reported = select(2, GetSpellCooldown(gcd_probe_spell_id))
if duration_reported == 0 then
self:release_gcd_lock()
end
end
end
function lib:UPDATE_SHAPESHIFT_FORM()
-- Only druids have their physical gcd change.
if self.class ~= "DRUID" then
return
end
local i = GetShapeshiftForm()
if i == 3 then
self.phys_gcd = 1.0
else
self.phys_gcd = 1.5
end
self.current_form = i
self:Fire(self.GCD_PHYS_UPDATED, lib.phys_gcd)
self:Fire(self.GCD_DURATIONS_UPDATED, lib.phys_gcd, lib.current_spell_gcd)
end
function lib:activate()
if not self.frame then
local frame = CreateFrame("Frame")
self.frame = frame
frame:RegisterEvent("PLAYER_LOGIN")
-- frame:RegisterEvent("PLAYER_REGEN_DISABLED")
-- frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:RegisterEvent("COMBAT_RATING_UPDATE")
frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
frame:RegisterUnitEvent("UNIT_AURA", "player")
frame:RegisterUnitEvent("UNIT_SPELLCAST_INTERRUPTED", "player")
end
self.frame:SetScript("OnEvent", function(_, event, ...)
lib[event](lib, event, ...)
end
)
end
lib:activate()