wowlab_engine_content/hooks/shadow_priest/
hooks.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::ResourceOps as _;
4use wowlab_engine_combat::ProcOps as _;
5use wowlab_engine_combat::AuraOps as _;
6use wowlab_engine_combat::{DamageFlags, HookCtx, ImpactEvent, MasteryCtx};
7use wowlab_engine_rng::proc_chance;
8use wowlab_types::{
9 combat::DamageSchool,
10 constants::{HUNDRED, MS_PER_SECOND},
11};
12
13use super::config::shadow_config;
14use crate::generated::specs::shadow_priest::{
15 AURA_ANCIENT_MADNESS, AURA_ANCIENT_MADNESS_EXTENSION, AURA_ENTROPIC_RIFT,
16 AURA_INSIDIOUS_IRE, AURA_SHADOW_WORD_MADNESS, AURA_SHADOW_WORD_PAIN, AURA_SHADOWY_INSIGHT,
17 AURA_SHATTERED_PSYCHE, AURA_VAMPIRIC_TOUCH, AURA_VOID_BLAST,
18 AURA_VOIDFORM, AURA_VOIDHEART, AURA_VOID_VOLLEY_CHARGES, AURA_VOID_VOLLEY_SET_BONUS,
19 SPELL, SPELL_TENTACLE_SLAM,
20};
21
22const SHADOWY_INSIGHT_INCREMENT_SCALE: f64 = 2.0;
24
25const MIND_BLAST_INSANITY: f64 = 6.0;
27const VOID_BLAST_INSANITY: f64 = 8.0;
28const TENTACLE_SLAM_EXTRA_INSANITY: f64 = 6.0;
30const TENTACLE_SLAM_HITS: u8 = 2;
31const VOIDFORM_WEAVING_DOTS: f64 = 3.0;
33const COLLAPSING_VOID_MAX_STACKS: u32 = 5;
34
35#[must_use]
37pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
38 if ctx.school != DamageSchool::Shadow {
39 return 1.0;
40 }
41
42 let dots = if ctx.is_aura_active(AURA_VOIDFORM) {
43 VOIDFORM_WEAVING_DOTS
44 } else {
45 f64::from(ctx.is_aura_active(AURA_SHADOW_WORD_PAIN))
46 + f64::from(ctx.is_aura_active(AURA_VAMPIRIC_TOUCH))
47 + f64::from(ctx.is_aura_active(AURA_SHADOW_WORD_MADNESS))
48 };
49
50 1.0 + dots * ctx.points(1) / HUNDRED
51}
52
53fn deal_effect_sp(ctx: &mut HookCtx<'_>, effect: wowlab_engine_combat::DamageEffectId) {
54 let coef = ctx.effect_sp_coefficient(effect);
55
56 ctx.deal_damage_sp(effect.action(), coef, DamageFlags::empty());
57}
58
59fn refresh_insidious_ire(ctx: &mut HookCtx<'_>) {
60 if !shadow_config(ctx).insidious_ire {
61 return;
62 }
63
64 let Some(pain) = ctx.aura_remaining_ms(AURA_SHADOW_WORD_PAIN) else {
65 return;
66 };
67 let Some(touch) = ctx.aura_remaining_ms(AURA_VAMPIRIC_TOUCH) else {
68 return;
69 };
70 let Some(madness) = ctx.aura_remaining_ms(AURA_SHADOW_WORD_MADNESS) else {
71 return;
72 };
73 let remaining = pain.min(touch).min(madness);
74
75 if remaining >= ctx.aura_remaining_ms(AURA_INSIDIOUS_IRE).unwrap_or(0) {
76 ctx.apply_aura_with_duration(AURA_INSIDIOUS_IRE, remaining);
77 }
78}
79
80fn trigger_apparitions(ctx: &mut HookCtx<'_>) {
81 let cfg = shadow_config(ctx);
82
83 if !cfg.shadowy_apparitions || !ctx.is_aura_active(AURA_VAMPIRIC_TOUCH) {
84 return;
85 }
86
87 if cfg.void_apparition_chance > 0.0 && proc_chance(ctx.rng(), cfg.void_apparition_chance) {
88 deal_effect_sp(ctx, crate::generated::specs::shadow_priest::DAMAGE_EFFECT::VOID_APPARITION_DAMAGE);
89 deal_effect_sp(ctx, crate::generated::specs::shadow_priest::DAMAGE_EFFECT::VOID_BOLT_DAMAGE);
90 } else {
91 deal_effect_sp(ctx, crate::generated::specs::shadow_priest::DAMAGE_EFFECT::SHADOWY_APPARITION_DAMAGE);
92 }
93
94 if cfg.shadeburst {
95 let effect = crate::generated::specs::shadow_priest::DAMAGE_EFFECT::SHADEBURST_DAMAGE;
96
97 ctx.deal_effect_damage_sp_with_geometry(effect, effect, 1.0, DamageFlags::empty());
98 }
99
100 if cfg.auspicious_insanity > 0.0 {
101 ctx.gain_resource(cfg.auspicious_insanity);
102 }
103}
104
105pub(crate) fn mind_blast_hook(ctx: &mut HookCtx<'_>) {
107 let cfg = shadow_config(ctx);
108 ctx.consume_aura(AURA_SHADOWY_INSIGHT);
111
112 if cfg.void_blast && ctx.is_aura_active(AURA_VOID_BLAST) {
113 deal_effect_sp(ctx, crate::generated::specs::shadow_priest::DAMAGE_EFFECT::VOID_BLAST_DAMAGE);
114 ctx.gain_resource(VOID_BLAST_INSANITY);
115
116 if cfg.darkening_horizon && ctx.is_aura_active(AURA_ENTROPIC_RIFT) {
117 let max = wowlab_types::numeric::f64_to_u32_saturating_trunc(
118 ctx.effect_base_points(crate::generated::specs::shadow_priest::DAMAGE_EFFECT::DARKENING_HORIZON_MAX),
119 );
120
121 if ctx.proc_counter(wowlab_engine_combat::generated_identities::proc_counter::DARKENING_HORIZON) < max {
122 ctx.add_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::DARKENING_HORIZON, 1);
123 let extension_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(
124 ctx.effect_base_points(crate::generated::specs::shadow_priest::DAMAGE_EFFECT::DARKENING_HORIZON_EXT)
125 * MS_PER_SECOND,
126 );
127
128 ctx.extend_aura(AURA_ENTROPIC_RIFT, extension_ms);
129 ctx.extend_aura(AURA_VOID_BLAST, extension_ms);
130 ctx.extend_aura(AURA_VOIDHEART, extension_ms);
131 }
132 }
133 } else {
134 let coef = ctx.effect_sp_coefficient(crate::generated::specs::shadow_priest::DAMAGE_EFFECT::MIND_BLAST_DAMAGE);
135
136 ctx.deal_damage_sp(crate::generated::specs::shadow_priest::DAMAGE_SPELL::MIND_BLAST, coef, DamageFlags::empty());
137 ctx.gain_resource(MIND_BLAST_INSANITY);
138 }
139
140 if cfg.shattered_psyche {
141 ctx.consume_aura(AURA_SHATTERED_PSYCHE);
143 }
144}
145
146pub(crate) fn shadow_word_pain_hook(ctx: &mut HookCtx<'_>) {
147 refresh_insidious_ire(ctx);
148}
149
150pub(crate) fn vampiric_touch_hook(ctx: &mut HookCtx<'_>) {
151 refresh_insidious_ire(ctx);
152}
153
154pub(crate) fn shadow_word_madness_hook(ctx: &mut HookCtx<'_>) {
155 trigger_apparitions(ctx);
156 let cfg = shadow_config(ctx);
157
158 if cfg.ancient_madness
159 && ctx.is_aura_active(AURA_VOIDFORM)
160 && u32::try_from(ctx.aura_stacks(AURA_ANCIENT_MADNESS_EXTENSION).max(0))
161 .unwrap_or(u32::MAX)
162 < cfg.ancient_madness_extension_max
163 {
164 ctx.extend_aura(AURA_VOIDFORM, cfg.ancient_madness_extension_ms);
165 ctx.apply_aura(AURA_ANCIENT_MADNESS_EXTENSION);
166 }
167
168 if cfg.collapsing_void_pct > 0.0
169 && ctx.is_aura_active(AURA_ENTROPIC_RIFT)
170 && ctx.proc_counter(wowlab_engine_combat::generated_identities::proc_counter::COLLAPSING_VOID) < COLLAPSING_VOID_MAX_STACKS
171 {
172 ctx.add_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::COLLAPSING_VOID, 1);
173 }
174
175 refresh_insidious_ire(ctx);
176}
177
178pub(crate) fn void_torrent_hook(ctx: &mut HookCtx<'_>) {
180 let cfg = shadow_config(ctx);
181
182 ctx.reset_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::DARKENING_HORIZON);
183 ctx.reset_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::COLLAPSING_VOID);
184 ctx.apply_aura(AURA_ENTROPIC_RIFT);
185
186 if cfg.void_blast {
187 ctx.apply_aura(AURA_VOID_BLAST);
188 }
189
190 if cfg.voidheart {
191 ctx.apply_aura(AURA_VOIDHEART);
192 }
193}
194
195pub(crate) fn entropic_rift_expire_hook(ctx: &mut HookCtx<'_>) {
197 let cfg = shadow_config(ctx);
198
199 if cfg.collapsing_void_pct > 0.0 {
200 let stacks = f64::from(ctx.proc_counter(wowlab_engine_combat::generated_identities::proc_counter::COLLAPSING_VOID));
201 let coef = ctx.effect_sp_coefficient(crate::generated::specs::shadow_priest::DAMAGE_EFFECT::COLLAPSING_VOID_DAMAGE)
202 * (1.0 + stacks * cfg.collapsing_void_pct);
203
204 ctx.deal_damage_sp(crate::generated::specs::shadow_priest::DAMAGE_EFFECT::COLLAPSING_VOID_DAMAGE.action(), coef, DamageFlags::empty());
205 }
206
207 ctx.expire_aura(AURA_VOID_BLAST);
208 ctx.expire_aura(AURA_VOIDHEART);
209 ctx.reset_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::DARKENING_HORIZON);
210 ctx.reset_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::COLLAPSING_VOID);
211}
212
213pub(crate) fn shadow_word_death_hook(ctx: &mut HookCtx<'_>) {
215 let mut coef = ctx.effect_sp_coefficient(
216 crate::generated::specs::shadow_priest::DAMAGE_EFFECT::SHADOW_WORD_DEATH_DAMAGE,
217 );
218 let threshold = ctx.effect_percent(
219 crate::generated::specs::shadow_priest::DAMAGE_EFFECT::SHADOW_WORD_DEATH_THRESHOLD,
220 );
221 let execute_bonus = ctx.effect_percent(
222 crate::generated::specs::shadow_priest::DAMAGE_EFFECT::SHADOW_WORD_DEATH_EXECUTE,
223 );
224 let health_pct = ctx.target_health_fraction();
225
226 if health_pct < threshold {
227 coef *= 1.0 + execute_bonus;
228 }
229
230 ctx.deal_damage_sp(crate::generated::specs::shadow_priest::DAMAGE_SPELL::SHADOW_WORD_DEATH, coef, DamageFlags::empty());
231}
232
233pub(crate) fn mind_flay_tick_hook(ctx: &mut HookCtx<'_>) {
235 let cfg = shadow_config(ctx);
236
237 if cfg.mind_flay_tick_insanity > 0.0 {
238 ctx.gain_resource(cfg.mind_flay_tick_insanity);
239 }
240
241 if cfg.shattered_psyche {
242 ctx.apply_aura(AURA_SHATTERED_PSYCHE);
243 }
244}
245
246pub(crate) fn tentacle_slam_hook(ctx: &mut HookCtx<'_>) {
248 let cfg = shadow_config(ctx);
249
250 for _ in 0..TENTACLE_SLAM_HITS {
251 let effect = crate::generated::specs::shadow_priest::DAMAGE_EFFECT::TENTACLE_SLAM_DAMAGE;
252 let coef = ctx.effect_sp_coefficient(effect) * cfg.mid2_tentacle_damage_mult;
253
254 ctx.deal_damage_sp(effect.action(), coef, DamageFlags::empty());
255 }
256
257 if cfg.mid2_tentacle_cooldown_reduction_ms > 0 {
258 ctx.reduce_cooldown(SPELL_TENTACLE_SLAM, cfg.mid2_tentacle_cooldown_reduction_ms);
259 }
260
261 if cfg.mid2_volley_chance > 0.0 && proc_chance(ctx.rng(), cfg.mid2_volley_chance) {
262 let charges_before = ctx.aura_stacks(AURA_VOID_VOLLEY_CHARGES);
263
264 ctx.apply_aura(AURA_VOID_VOLLEY_CHARGES);
265
266 if ctx.aura_stacks(AURA_VOID_VOLLEY_CHARGES) > charges_before {
267 ctx.apply_aura(AURA_VOID_VOLLEY_SET_BONUS);
268 }
269 }
270
271 ctx.gain_resource(TENTACLE_SLAM_EXTRA_INSANITY);
272 refresh_insidious_ire(ctx);
273}
274
275fn deal_void_volley(ctx: &mut HookCtx<'_>, effectiveness: f64) {
276 let cfg = shadow_config(ctx);
277 let effect = crate::generated::specs::shadow_priest::DAMAGE_EFFECT::VOID_VOLLEY_BOLT_DAMAGE;
278 let coef = ctx.effect_sp_coefficient(effect) * effectiveness;
279
280 for _ in 0..cfg.void_volley_bolts {
281 ctx.deal_damage_sp(effect.action(), coef, DamageFlags::empty());
282 }
283
284 let primary = ctx.target();
285
286 ctx.with_resolved_spell_effect_targets(
287 crate::generated::specs::shadow_priest::DAMAGE_EFFECT::VOID_VOLLEY_TARGETS,
288 effect.action(),
289 None,
290 |ctx, targets| {
291 for target in targets
292 .iter()
293 .copied()
294 .filter(|target| Some(*target) != primary)
295 {
296 for _ in 0..cfg.void_volley_secondary_bolts {
297 ctx.deal_damage_sp_to_target(
298 target,
299 effect.action(),
300 coef,
301 DamageFlags::empty(),
302 );
303 }
304 }
305 },
306 );
307 ctx.gain_resource(cfg.void_volley_insanity);
308}
309
310pub(crate) fn voidform_hook(ctx: &mut HookCtx<'_>) {
312 deal_void_volley(ctx, 1.0);
313
314 for _ in 0..shadow_config(ctx).voidform_volley_charges {
315 ctx.apply_aura(AURA_VOID_VOLLEY_CHARGES);
316 }
317}
318
319pub(crate) fn void_volley_hook(ctx: &mut HookCtx<'_>) {
321 let set_bonus_cast = ctx.consume_aura_stack(AURA_VOID_VOLLEY_SET_BONUS);
322 let effectiveness = if set_bonus_cast {
323 shadow_config(ctx).mid2_volley_effectiveness
324 } else {
325 1.0
326 };
327
328 ctx.consume_aura_stack(AURA_VOID_VOLLEY_CHARGES);
329 deal_void_volley(ctx, effectiveness);
330}
331
332pub(crate) fn voidform_expire_hook(ctx: &mut HookCtx<'_>) {
334 let cfg = shadow_config(ctx);
335 let extensions = u32::try_from(ctx.aura_stacks(AURA_ANCIENT_MADNESS_EXTENSION).max(0))
336 .unwrap_or(u32::MAX);
337
338 ctx.expire_aura(AURA_ANCIENT_MADNESS_EXTENSION);
339
340 for _ in 0..extensions.saturating_mul(cfg.ancient_madness_haste_stacks_per_extension) {
341 ctx.apply_aura(AURA_ANCIENT_MADNESS);
342 }
343
344 if cfg.crushing_void {
345 ctx.apply_aura(AURA_VOID_VOLLEY_CHARGES);
346 }
347}
348
349pub(super) const fn swp_filter(spell_id: u32) -> bool {
350 spell_id == SPELL::SHADOW_WORD_PAIN
351}
352
353pub(super) fn tormented_spirits_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
355 trigger_apparitions(ctx);
356}
357
358pub(super) fn shadowy_insight_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
360 let Some(idx) = shadow_config(ctx).shadowy_insight_threshold else {
361 return;
362 };
363
364 if ctx.roll_threshold(idx, SHADOWY_INSIGHT_INCREMENT_SCALE) {
365 ctx.apply_aura(AURA_SHADOWY_INSIGHT);
366 }
367}