wowlab_engine_content/hooks/shadow_priest/
hooks.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::ResourceOps as _;
3use wowlab_engine_combat::ProcOps as _;
4use wowlab_engine_combat::AuraOps as _;
5use wowlab_engine_combat::{DamageFlags, HookCtx, ImpactEvent, MasteryCtx};
6use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
7use wowlab_engine_rng::proc_chance;
8use wowlab_types::{
9 combat::DamageSchool,
10 constants::{HUNDRED, MS_PER_SECOND},
11 sim::SpellIdx,
12};
13
14use super::config::shadow_config;
15use crate::generated::specs::shadow_priest::{
16 AURA_ENTROPIC_RIFT, AURA_INSIDIOUS_IRE, AURA_SHADOW_WORD_MADNESS, AURA_SHADOW_WORD_PAIN,
17 AURA_SHADOWY_INSIGHT, AURA_SHATTERED_PSYCHE, AURA_VAMPIRIC_TOUCH, AURA_VOID_BLAST,
18 AURA_VOIDFORM, AURA_VOIDHEART, EFFECT, SPELL, TALENT,
19};
20
21const SHADOWY_INSIGHT_INCREMENT_SCALE: f64 = 2.0;
23
24const MIND_BLAST_INSANITY: f64 = 6.0;
26const VOID_BLAST_INSANITY: f64 = 8.0;
27const TENTACLE_SLAM_EXTRA_INSANITY: f64 = 6.0;
29const TENTACLE_SLAM_HITS: u8 = 2;
30const VOIDFORM_WEAVING_DOTS: f64 = 3.0;
32const COLLAPSING_VOID_MAX_STACKS: u32 = 5;
33
34#[must_use]
36pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
37 if ctx.school != DamageSchool::Shadow {
38 return 1.0;
39 }
40
41 let dots = if ctx.is_aura_active(AURA_VOIDFORM) {
42 VOIDFORM_WEAVING_DOTS
43 } else {
44 f64::from(ctx.is_aura_active(AURA_SHADOW_WORD_PAIN))
45 + f64::from(ctx.is_aura_active(AURA_VAMPIRIC_TOUCH))
46 + f64::from(ctx.is_aura_active(AURA_SHADOW_WORD_MADNESS))
47 };
48
49 1.0 + dots * ctx.points(1) / HUNDRED
50}
51
52fn deal_effect_sp(ctx: &mut HookCtx<'_>, effect: (u32, u8)) {
53 let coef = ctx
54 .game_data()
55 .sp_coef(SpellIdx::from_raw(effect.0), effect.1);
56
57 ctx.deal_damage_sp(effect.0, coef, DamageFlags::empty());
58}
59
60fn refresh_insidious_ire(ctx: &mut HookCtx<'_>) {
61 if !shadow_config(ctx).insidious_ire {
62 return;
63 }
64
65 let Some(pain) = ctx.aura_remaining_ms(AURA_SHADOW_WORD_PAIN.raw()) else {
66 return;
67 };
68 let Some(touch) = ctx.aura_remaining_ms(AURA_VAMPIRIC_TOUCH.raw()) else {
69 return;
70 };
71 let Some(madness) = ctx.aura_remaining_ms(AURA_SHADOW_WORD_MADNESS.raw()) else {
72 return;
73 };
74 let remaining = pain.min(touch).min(madness);
75
76 if remaining >= ctx.aura_remaining_ms(AURA_INSIDIOUS_IRE.raw()).unwrap_or(0) {
77 ctx.apply_aura_with_duration(AURA_INSIDIOUS_IRE.raw(), remaining);
78 }
79}
80
81fn trigger_apparitions(ctx: &mut HookCtx<'_>) {
82 let cfg = shadow_config(ctx);
83
84 if !cfg.shadowy_apparitions || !ctx.is_aura_active(AURA_VAMPIRIC_TOUCH.raw()) {
85 return;
86 }
87
88 if cfg.void_apparition_chance > 0.0 && proc_chance(ctx.rng(), cfg.void_apparition_chance) {
89 deal_effect_sp(ctx, EFFECT::VOID_APPARITION_DAMAGE);
90 deal_effect_sp(ctx, EFFECT::VOID_BOLT_DAMAGE);
91 } else {
92 deal_effect_sp(ctx, EFFECT::SHADOWY_APPARITION_DAMAGE);
93 }
94
95 if cfg.auspicious_insanity > 0.0 {
96 ctx.gain_resource(cfg.auspicious_insanity);
97 }
98}
99
100pub(crate) fn mind_blast_hook(ctx: &mut HookCtx<'_>) {
102 let cfg = shadow_config(ctx);
103 ctx.consume_aura(AURA_SHADOWY_INSIGHT.raw());
106
107 if cfg.void_blast && ctx.is_aura_active(AURA_VOID_BLAST.raw()) {
108 deal_effect_sp(ctx, EFFECT::VOID_BLAST_DAMAGE);
109 ctx.gain_resource(VOID_BLAST_INSANITY);
110
111 if cfg.darkening_horizon && ctx.is_aura_active(AURA_ENTROPIC_RIFT.raw()) {
112 let max = wowlab_types::numeric::f64_to_u32_saturating_trunc(
113 ctx.game_data()
114 .effect_base_points(EFFECT::DARKENING_HORIZON_MAX),
115 );
116
117 if ctx.proc_counter(TALENT::DARKENING_HORIZON) < max {
118 ctx.add_proc_counter(TALENT::DARKENING_HORIZON, 1);
119 let extension_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(
120 ctx.game_data()
121 .effect_base_points(EFFECT::DARKENING_HORIZON_EXT)
122 * MS_PER_SECOND,
123 );
124
125 ctx.extend_aura(AURA_ENTROPIC_RIFT.raw(), extension_ms);
126 ctx.extend_aura(AURA_VOID_BLAST.raw(), extension_ms);
127 ctx.extend_aura(AURA_VOIDHEART.raw(), extension_ms);
128 }
129 }
130 } else {
131 let coef = ctx
132 .game_data()
133 .effect_sp_coefficient(EFFECT::MIND_BLAST_DAMAGE);
134
135 ctx.deal_damage_sp(SPELL::MIND_BLAST, coef, DamageFlags::empty());
136 ctx.gain_resource(MIND_BLAST_INSANITY);
137 }
138
139 if cfg.shattered_psyche {
140 ctx.consume_aura(AURA_SHATTERED_PSYCHE.raw());
142 }
143}
144
145pub(crate) fn shadow_word_pain_hook(ctx: &mut HookCtx<'_>) {
146 refresh_insidious_ire(ctx);
147}
148
149pub(crate) fn vampiric_touch_hook(ctx: &mut HookCtx<'_>) {
150 refresh_insidious_ire(ctx);
151}
152
153pub(crate) fn shadow_word_madness_hook(ctx: &mut HookCtx<'_>) {
154 trigger_apparitions(ctx);
155 let cfg = shadow_config(ctx);
156
157 if cfg.collapsing_void_pct > 0.0
158 && ctx.is_aura_active(AURA_ENTROPIC_RIFT.raw())
159 && ctx.proc_counter(TALENT::COLLAPSING_VOID) < COLLAPSING_VOID_MAX_STACKS
160 {
161 ctx.add_proc_counter(TALENT::COLLAPSING_VOID, 1);
162 }
163
164 refresh_insidious_ire(ctx);
165}
166
167pub(crate) fn void_torrent_hook(ctx: &mut HookCtx<'_>) {
169 let cfg = shadow_config(ctx);
170
171 ctx.reset_proc_counter(TALENT::DARKENING_HORIZON);
172 ctx.reset_proc_counter(TALENT::COLLAPSING_VOID);
173 ctx.apply_aura(AURA_ENTROPIC_RIFT.raw());
174
175 if cfg.void_blast {
176 ctx.apply_aura(AURA_VOID_BLAST.raw());
177 }
178
179 if cfg.voidheart {
180 ctx.apply_aura(AURA_VOIDHEART.raw());
181 }
182}
183
184pub(crate) fn entropic_rift_expire_hook(ctx: &mut HookCtx<'_>) {
186 let cfg = shadow_config(ctx);
187
188 if cfg.collapsing_void_pct > 0.0 {
189 let stacks = f64::from(ctx.proc_counter(TALENT::COLLAPSING_VOID));
190 let coef = ctx
191 .game_data()
192 .effect_sp_coefficient(EFFECT::COLLAPSING_VOID_DAMAGE)
193 * (1.0 + stacks * cfg.collapsing_void_pct);
194
195 ctx.deal_damage_sp(EFFECT::COLLAPSING_VOID_DAMAGE.0, coef, DamageFlags::empty());
196 }
197
198 ctx.expire_aura(AURA_VOID_BLAST.raw());
199 ctx.expire_aura(AURA_VOIDHEART.raw());
200 ctx.reset_proc_counter(TALENT::DARKENING_HORIZON);
201 ctx.reset_proc_counter(TALENT::COLLAPSING_VOID);
202}
203
204pub(crate) fn shadow_word_death_hook(ctx: &mut HookCtx<'_>) {
206 let data = ctx.game_data();
207 let mut coef = data.effect_sp_coefficient(EFFECT::SHADOW_WORD_DEATH_DAMAGE);
208 let threshold = data.effect_base_points(EFFECT::SHADOW_WORD_DEATH_THRESHOLD) / HUNDRED;
209 let execute_bonus = data.effect_base_points(EFFECT::SHADOW_WORD_DEATH_EXECUTE) / HUNDRED;
210 let health_pct = ctx.target_health_fraction();
211
212 if health_pct < threshold {
213 coef *= 1.0 + execute_bonus;
214 }
215
216 ctx.deal_damage_sp(SPELL::SHADOW_WORD_DEATH, coef, DamageFlags::empty());
217}
218
219pub(crate) fn mind_flay_tick_hook(ctx: &mut HookCtx<'_>) {
221 let cfg = shadow_config(ctx);
222
223 if cfg.mind_flay_tick_insanity > 0.0 {
224 ctx.gain_resource(cfg.mind_flay_tick_insanity);
225 }
226
227 if cfg.shattered_psyche {
228 ctx.apply_aura(AURA_SHATTERED_PSYCHE.raw());
229 }
230}
231
232pub(crate) fn tentacle_slam_hook(ctx: &mut HookCtx<'_>) {
234 for _ in 0..TENTACLE_SLAM_HITS {
235 deal_effect_sp(ctx, EFFECT::TENTACLE_SLAM_DAMAGE);
236 }
237
238 ctx.gain_resource(TENTACLE_SLAM_EXTRA_INSANITY);
239 refresh_insidious_ire(ctx);
240}
241
242pub(super) const fn swp_filter(spell_id: u32) -> bool {
243 spell_id == SPELL::SHADOW_WORD_PAIN
244}
245
246pub(super) fn tormented_spirits_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
248 trigger_apparitions(ctx);
249}
250
251pub(super) fn shadowy_insight_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
253 let Some(idx) = shadow_config(ctx).shadowy_insight_threshold else {
254 return;
255 };
256
257 if ctx.roll_threshold(idx, SHADOWY_INSIGHT_INCREMENT_SCALE) {
258 ctx.apply_aura(AURA_SHADOWY_INSIGHT.raw());
259 }
260}