-
Notifications
You must be signed in to change notification settings - Fork 36
/
dot.go
385 lines (312 loc) · 11.6 KB
/
dot.go
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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
package core
import (
"strconv"
"time"
)
type OnSnapshot func(sim *Simulation, target *Unit, dot *Dot, isRollover bool)
type OnTick func(sim *Simulation, target *Unit, dot *Dot)
type DotConfig struct {
IsAOE bool // Set to true for AOE dots (Blizzard, Hurricane, Consecrate, etc)
SelfOnly bool // Set to true to only create the self-hot.
// Optional, will default to the corresponding spell.
Spell *Spell
Aura Aura
NumberOfTicks int32 // number of ticks over the whole duration
TickLength time.Duration // time between each tick
// If true, tick length will be shortened based on casting speed.
AffectedByCastSpeed bool
HasteAffectsDuration bool
OnSnapshot OnSnapshot
OnTick OnTick
BonusCoefficient float64 // EffectBonusCoefficient in SpellEffect client DB table, "SP mod" on Wowhead (not necessarily shown there even if > 0)
}
type Dot struct {
Spell *Spell
// Embed Aura, so we can use IsActive/Refresh/etc directly.
*Aura
NumberOfTicks int32 // number of ticks over the whole duration
BaseTickCount int32 // base tick count without haste applied
TickLength time.Duration // time between each tick
// If true, tick length will be shortened based on casting speed.
AffectedByCastSpeed bool
HasteAffectsDuration bool
OnSnapshot OnSnapshot
OnTick OnTick
SnapshotBaseDamage float64
SnapshotCritChance float64
SnapshotAttackerMultiplier float64
tickAction *PendingAction
tickPeriod time.Duration
// Number of ticks since last call to Apply().
TickCount int32
lastTickTime time.Duration
isChanneled bool
BonusCoefficient float64 // EffectBonusCoefficient in SpellEffect client DB table, "SP mod" on Wowhead (not necessarily shown there even if > 0)
}
// TickPeriod is how fast the snapshot dot ticks.
func (dot *Dot) TickPeriod() time.Duration {
return dot.tickPeriod
}
func (dot *Dot) NextTickAt() time.Duration {
return dot.lastTickTime + dot.tickPeriod
}
func (dot *Dot) TimeUntilNextTick(sim *Simulation) time.Duration {
return dot.NextTickAt() - sim.CurrentTime
}
func (dot *Dot) MaxTicksRemaining() int32 {
return dot.NumberOfTicks - dot.TickCount
}
func (dot *Dot) NumTicksRemaining(sim *Simulation) int32 {
maxTicksRemaining := dot.MaxTicksRemaining()
finalTickAt := dot.lastTickTime + dot.tickPeriod*time.Duration(maxTicksRemaining)
return max(0, int32((finalTickAt-sim.CurrentTime)/dot.tickPeriod)+1)
}
// Roll over = gets carried over with everlasting refresh and doesn't get applied if triggered when the spell is already up.
// - Example: critical strike rating, internal % damage modifiers: buffs or debuffs on player
// Nevermelting Ice, Shadow Mastery (ISB), Trick of the Trades, Deaths Embrace, Thaddius Polarity, Hera Spores, Crit on weapons from swapping
// Snapshot = calculation happens at refresh and application (stays up even if buff falls of, until new refresh or application)
// - Example: Spell power, Haste rating
// Blood Fury, Lightweave Embroid, Eradication, Bloodlust
// Dynamic = realtime update
// - Example: external % damage modifier debuffs on target
// Haunt, Curse of Shadow, Shadow Embrace
// Rollover is used to reset the duration of a dot from an external spell (not casting the dot itself)
// This keeps the snapshot crit and %dmg modifiers.
// However, sp and haste are recalculated.
func (dot *Dot) Rollover(sim *Simulation) {
dot.TakeSnapshot(sim, true)
dot.RecomputeAuraDuration() // recalculate haste
dot.Aura.Refresh(sim) // update aura's duration
oldNextTick := dot.tickAction.NextActionAt
dot.tickAction.Cancel(sim) // remove old PA ticker
// recreate with new period, resetting the next tick.
periodicOptions := dot.basePeriodicOptions()
periodicOptions.Period = dot.tickPeriod
dot.tickAction = NewPeriodicAction(sim, periodicOptions)
dot.tickAction.NextActionAt = oldNextTick
sim.AddPendingAction(dot.tickAction)
}
func (dot *Dot) RescheduleNextTick(sim *Simulation) {
dot.RecomputeAuraDuration()
dot.tickAction.Cancel(sim) // remove old PA ticker
// recreate with new period, resetting the next tick.
periodicOptions := dot.basePeriodicOptions()
periodicOptions.Period = dot.tickPeriod
dot.tickAction = NewPeriodicAction(sim, periodicOptions)
dot.tickAction.NextActionAt = dot.lastTickTime + dot.tickPeriod
sim.AddPendingAction(dot.tickAction)
}
// Snapshots and activates the Dot
// If the Dot is running it's duration will be refreshed and
// if there was a next Dot happening this will carry over to the new Dot
func (dot *Dot) Apply(sim *Simulation) {
dot.TakeSnapshot(sim, false)
dot.TickCount = 0
// we a have running dot tick
// the next tick never get's clipped and is added onto the dot's time for hasted dots
// see: https://github.com/wowsims/cata/issues/50git
if dot.tickAction != nil && !dot.tickAction.cancelled {
// save next tick timer as timer is computed based on tick time
// which we update in RecomputeAuraDuration
nextTick := dot.TimeUntilNextTick(sim)
dot.RecomputeAuraDuration()
dot.Aura.Duration += nextTick
// add extra tick
dot.TickCount--
// update tick action to work with new tick rate, but set next tick to still occur
oldNextAction := dot.tickAction.NextActionAt
dot.tickAction.Cancel(sim)
periodicOptions := dot.basePeriodicOptions()
periodicOptions.Period = dot.tickPeriod
dot.tickAction = NewPeriodicAction(sim, periodicOptions)
dot.tickAction.NextActionAt = oldNextAction
sim.AddPendingAction(dot.tickAction)
} else {
dot.RecomputeAuraDuration()
}
dot.Aura.Activate(sim)
}
// ApplyOrReset is used for rolling dots that reset the tick timer on reapplication.
// This is more efficient than Apply(), and works around tickAction.CleanUp() wrongly generating
// an extra ticks if (re-)application and tick happen at the same time.
func (dot *Dot) ApplyOrReset(sim *Simulation) {
if !dot.IsActive() {
dot.Apply(sim)
return
}
dot.TakeSnapshot(sim, true)
dot.RecomputeAuraDuration() // recalculate haste
dot.Aura.Refresh(sim) // update aura's duration
dot.TickCount = 0
oldTickAction := dot.tickAction
dot.tickAction = nil // prevent tickAction.CleanUp() from adding an extra tick
oldTickAction.Cancel(sim) // remove old PA ticker
// recreate with new period, resetting the next tick.
periodicOptions := dot.basePeriodicOptions()
periodicOptions.Period = dot.tickPeriod
dot.tickAction = NewPeriodicAction(sim, periodicOptions)
sim.AddPendingAction(dot.tickAction)
}
func (dot *Dot) Cancel(sim *Simulation) {
if dot.Aura.IsActive() {
dot.Aura.Deactivate(sim)
}
}
// Call this after manually changing NumberOfTicks or TickLength.
func (dot *Dot) RecomputeAuraDuration() {
if dot.AffectedByCastSpeed {
dot.tickPeriod = dot.Spell.Unit.ApplyCastSpeedForSpell(dot.TickLength, dot.Spell)
// cata haste logic here for dots
// channels seem not to be affected by the same logic
// see: https://youtu.be/Rr4YyKaU7Ik?si=Isuce7Z1bQWMWpMi&t=53
if !dot.isChanneled && !dot.HasteAffectsDuration {
dot.NumberOfTicks = int32(round(float64(dot.GetBaseDuration()) / float64(dot.tickPeriod)))
}
dot.Aura.Duration = dot.tickPeriod * time.Duration(dot.NumberOfTicks)
} else {
dot.tickPeriod = dot.TickLength
dot.Aura.Duration = dot.tickPeriod * time.Duration(dot.NumberOfTicks)
}
}
func (dot *Dot) AddTicks(num int32) {
dot.BaseTickCount += num
dot.NumberOfTicks += num
}
func (dot *Dot) GetBaseDuration() time.Duration {
return time.Duration(dot.BaseTickCount) * dot.TickLength
}
// Takes a new snapshot of this Dot's effects.
//
// In most cases this will be called automatically, and should only be called
// to force a new snapshot to be taken.
//
// doRollover will apply previously snapshotted crit/%dmg instead of recalculating.
func (dot *Dot) TakeSnapshot(sim *Simulation, doRollover bool) {
if dot.OnSnapshot != nil {
dot.OnSnapshot(sim, dot.Unit, dot, doRollover)
}
}
// Forces an instant tick. Does not reset the tick timer or aura duration,
// the tick is simply an extra tick.
func (dot *Dot) TickOnce(sim *Simulation) {
dot.lastTickTime = sim.CurrentTime
dot.OnTick(sim, dot.Unit, dot)
if dot.isChanneled {
// Note: even if the clip delay is 0ms, need a WaitUntil so that APL is called after the channel aura fully fades.
if dot.MaxTicksRemaining() == 0 {
if dot.Spell.Unit.GCD.IsReady(sim) {
dot.Spell.Unit.WaitUntil(sim, sim.CurrentTime+dot.Spell.Unit.ChannelClipDelay)
}
} else if dot.Spell.Unit.Rotation.shouldInterruptChannel(sim) {
dot.Cancel(sim)
if dot.Spell.Unit.GCD.IsReady(sim) {
dot.Spell.Unit.WaitUntil(sim, sim.CurrentTime+dot.Spell.Unit.ChannelClipDelay)
}
}
}
}
// ManualTick forces the dot forward one tick
// Will cancel the dot if it is out of ticks.
func (dot *Dot) ManualTick(sim *Simulation) {
if dot.lastTickTime != sim.CurrentTime {
dot.TickCount++
if dot.NumTicksRemaining(sim) <= 0 {
dot.Cancel(sim)
} else {
dot.TickOnce(sim)
}
}
}
func (dot *Dot) basePeriodicOptions() PeriodicActionOptions {
return PeriodicActionOptions{
//Priority: ActionPriorityDOT,
OnAction: func(sim *Simulation) {
if dot.lastTickTime != sim.CurrentTime {
dot.TickCount++
dot.TickOnce(sim)
}
},
CleanUp: func(sim *Simulation) {
// In certain cases, the last tick and the dot aura expiration can happen in
// different orders, so we might need to apply the last tick.
if dot.tickAction != nil && dot.tickAction.NextActionAt == sim.CurrentTime {
if dot.lastTickTime != sim.CurrentTime {
dot.TickCount++
dot.TickOnce(sim)
}
}
},
}
}
func newDot(config Dot) *Dot {
dot := &Dot{}
*dot = config
dot.tickPeriod = dot.TickLength
dot.Aura.Duration = dot.TickLength * time.Duration(dot.NumberOfTicks)
dot.Aura.ApplyOnGain(func(aura *Aura, sim *Simulation) {
dot.lastTickTime = sim.CurrentTime
periodicOptions := dot.basePeriodicOptions()
periodicOptions.Period = dot.tickPeriod
dot.tickAction = NewPeriodicAction(sim, periodicOptions)
sim.AddPendingAction(dot.tickAction)
if dot.isChanneled {
dot.Spell.Unit.ChanneledDot = dot
}
})
dot.Aura.ApplyOnExpire(func(aura *Aura, sim *Simulation) {
if dot.tickAction != nil {
dot.tickAction.Cancel(sim)
dot.tickAction = nil
}
if dot.isChanneled {
dot.Spell.Unit.ChanneledDot = nil
dot.Spell.Unit.Rotation.interruptChannelIf = nil
dot.Spell.Unit.Rotation.allowChannelRecastOnInterrupt = false
}
})
return dot
}
type DotArray []*Dot
func (dots DotArray) Get(target *Unit) *Dot {
return dots[target.UnitIndex]
}
func (spell *Spell) createDots(config DotConfig, isHot bool) {
if config.NumberOfTicks == 0 && config.TickLength == 0 {
return
}
if config.Spell == nil {
config.Spell = spell
}
dot := Dot{
Spell: config.Spell,
NumberOfTicks: config.NumberOfTicks,
BaseTickCount: config.NumberOfTicks,
TickLength: config.TickLength,
AffectedByCastSpeed: config.AffectedByCastSpeed,
HasteAffectsDuration: config.HasteAffectsDuration,
OnSnapshot: config.OnSnapshot,
OnTick: config.OnTick,
isChanneled: config.Spell.Flags.Matches(SpellFlagChanneled),
BonusCoefficient: config.BonusCoefficient,
}
auraConfig := config.Aura
if auraConfig.ActionID.IsEmptyAction() {
auraConfig.ActionID = dot.Spell.ActionID
}
caster := dot.Spell.Unit
if config.IsAOE || config.SelfOnly {
dot.Aura = caster.GetOrRegisterAura(auraConfig)
spell.aoeDot = newDot(dot)
} else {
auraConfig.Label += "-" + strconv.Itoa(int(caster.UnitIndex))
if spell.dots == nil {
spell.dots = make([]*Dot, len(caster.Env.AllUnits))
}
for _, target := range caster.Env.AllUnits {
if isHot != caster.IsOpponent(target) {
dot.Aura = target.GetOrRegisterAura(auraConfig)
spell.dots[target.UnitIndex] = newDot(dot)
}
}
}
}