Skip to content

Commit 25e0e2e

Browse files
committed
Updated Sudden Doom proc efficiency
1 parent 3f8e9af commit 25e0e2e

File tree

1 file changed

+102
-21
lines changed

1 file changed

+102
-21
lines changed

src/analysis/retail/deathknight/unholy/modules/talents/SuddenDoom.tsx

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,128 @@
11
import SPELLS from 'common/SPELLS';
2+
import TALENTS from 'common/TALENTS/deathknight';
23
import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer';
3-
import Events, { ApplyBuffEvent, RemoveBuffEvent } from 'parser/core/Events';
4+
import Events, {
5+
ApplyBuffEvent,
6+
CastEvent,
7+
RefreshBuffEvent,
8+
RemoveBuffEvent,
9+
} from 'parser/core/Events';
410
import { ThresholdStyle } from 'parser/core/ParseResults';
11+
import { formatPercentage } from 'common/format';
512
import BoringSpellValueText from 'parser/ui/BoringSpellValueText';
613
import Statistic from 'parser/ui/Statistic';
714
import { STATISTIC_ORDER } from 'parser/ui/StatisticBox';
815

16+
/* The Sudden Doom buff lasts 10 seconds. We use this to distinguish natural expiration
17+
(removal near the 10s mark) from consumption (removal mid-duration via DC/Epidemic).
18+
This is necessary because WCL event ordering can place removebuff before the cast event
19+
at the same timestamp, making flag-based consumption detection unreliable. */
920
const BUFF_DURATION_MS = 10000;
21+
const EXPIRE_BUFFER_MS = 100;
1022

1123
class SuddenDoom extends Analyzer {
12-
wastedProcs = 0;
13-
lastProcTime = 0;
24+
totalProcs = 0;
25+
consumedProcs = 0;
26+
wastedRefreshes = 0;
27+
wastedExpires = 0;
28+
29+
private buffActive = false;
30+
private buffAppliedAt: number | null = null;
1431

1532
constructor(options: Options) {
1633
super(options);
34+
this.active = this.selectedCombatant.hasTalent(TALENTS.SUDDEN_DOOM_TALENT);
35+
if (!this.active) {
36+
return;
37+
}
1738

1839
this.addEventListener(
19-
Events.refreshbuff.by(SELECTED_PLAYER).spell(SPELLS.SUDDEN_DOOM_BUFF),
20-
this.onRefreshBuff,
40+
Events.applybuff.by(SELECTED_PLAYER).spell(SPELLS.SUDDEN_DOOM_BUFF),
41+
this.onApplyBuff,
2142
);
2243
this.addEventListener(
23-
Events.applybuff.by(SELECTED_PLAYER).spell(SPELLS.SUDDEN_DOOM_BUFF),
24-
this.onBuff,
44+
Events.refreshbuff.by(SELECTED_PLAYER).spell(SPELLS.SUDDEN_DOOM_BUFF),
45+
this.onRefreshBuff,
2546
);
2647
this.addEventListener(
2748
Events.removebuff.by(SELECTED_PLAYER).spell(SPELLS.SUDDEN_DOOM_BUFF),
2849
this.onRemoveBuff,
2950
);
51+
this.addEventListener(
52+
Events.cast.by(SELECTED_PLAYER).spell([SPELLS.DEATH_COIL, SPELLS.EPIDEMIC]),
53+
this.onCast,
54+
);
3055
}
3156

32-
onBuff(event: ApplyBuffEvent) {
33-
this.lastProcTime = event.timestamp;
57+
onApplyBuff(event: ApplyBuffEvent) {
58+
this.totalProcs += 1;
59+
this.buffActive = true;
60+
this.buffAppliedAt = event.timestamp;
61+
}
62+
63+
onRefreshBuff(event: RefreshBuffEvent) {
64+
/* Only count as wasted if the previous proc was still unconsumed.
65+
A refreshbuff can fire on the same tick as a consumption cast, in which case
66+
buffActive is already false and the old proc was used — not wasted. */
67+
if (this.buffActive) {
68+
this.wastedRefreshes += 1;
69+
}
70+
71+
this.totalProcs += 1;
72+
this.buffActive = true;
73+
this.buffAppliedAt = event.timestamp;
3474
}
3575

3676
onRemoveBuff(event: RemoveBuffEvent) {
37-
const durationHeld = event.timestamp - this.lastProcTime;
38-
if (durationHeld > BUFF_DURATION_MS) {
39-
this.wastedProcs += 1;
77+
if (!this.buffActive) {
78+
return;
4079
}
80+
81+
/* Only count as expired if the removal happened near the expected buff expiry.
82+
Consumption removals happen mid-duration and won't match this window. */
83+
const expectedExpireAt = (this.buffAppliedAt ?? event.timestamp) + BUFF_DURATION_MS;
84+
if (
85+
event.timestamp >= expectedExpireAt - EXPIRE_BUFFER_MS &&
86+
event.timestamp <= expectedExpireAt + EXPIRE_BUFFER_MS
87+
) {
88+
this.wastedExpires += 1;
89+
}
90+
91+
this.buffActive = false;
92+
this.buffAppliedAt = null;
93+
}
94+
95+
onCast(_event: CastEvent) {
96+
if (!this.buffActive) {
97+
return;
98+
}
99+
100+
this.consumedProcs += 1;
101+
this.buffActive = false;
102+
this.buffAppliedAt = null;
41103
}
42104

43-
onRefreshBuff() {
44-
this.wastedProcs += 1;
105+
get wastedProcs() {
106+
return this.wastedRefreshes + this.wastedExpires;
107+
}
108+
109+
get wasteRate() {
110+
return this.totalProcs > 0 ? this.wastedProcs / this.totalProcs : 0;
111+
}
112+
113+
get efficiency() {
114+
return this.totalProcs > 0 ? 1 - this.wasteRate : 1;
45115
}
46116

47117
get suggestionThresholds() {
48118
return {
49-
actual: this.wastedProcs,
119+
actual: this.wasteRate,
50120
isGreaterThan: {
51-
minor: 0,
52-
average: 2,
53-
major: 4,
121+
minor: 0.1,
122+
average: 0.2,
123+
major: 0.3,
54124
},
55-
style: ThresholdStyle.NUMBER,
125+
style: ThresholdStyle.PERCENTAGE,
56126
};
57127
}
58128

@@ -61,11 +131,22 @@ class SuddenDoom extends Analyzer {
61131
<Statistic
62132
position={STATISTIC_ORDER.CORE(12)}
63133
size="flexible"
64-
tooltip="A proc counts as wasted if it fades without being used or if it refreshes"
134+
tooltip={
135+
<>
136+
<div>
137+
You wasted {this.wastedProcs} out of {this.totalProcs} Sudden Doom procs (
138+
{formatPercentage(this.wasteRate)}%).
139+
</div>
140+
<div>
141+
{this.wastedExpires} procs expired without being used and {this.wastedRefreshes} procs
142+
were overwritten by new procs.
143+
</div>
144+
</>
145+
}
65146
>
66147
<BoringSpellValueText spell={SPELLS.SUDDEN_DOOM_BUFF}>
67148
<>
68-
{this.wastedProcs} <small>wasted procs</small>
149+
{formatPercentage(this.efficiency)} % <small>efficiency</small>
69150
</>
70151
</BoringSpellValueText>
71152
</Statistic>

0 commit comments

Comments
 (0)