Skip to main content

wowlab_engine_content/hooks/arms_warrior/
hooks.rs

1use wowlab_engine_combat::AuraOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::DamageOps as _;
4use wowlab_engine_combat::ProcOps as _;
5use wowlab_engine_combat::ResourceOps as _;
6use wowlab_engine_combat::SchedulingOps as _;
7use wowlab_types::constants::HUNDRED;
8
9use wowlab_engine_combat::{HookCtx, MasteryCtx, ResourceGainSource, SwingEvent};
10use wowlab_engine_rng::proc_chance;
11
12use crate::generated::specs::arms_warrior::{
13    AURA_BLADESTORM, AURA_DEEP_WOUNDS, AURA_EXECUTIONER, AURA_EXECUTIONERS_PRECISION,
14    AURA_FATAL_MARK, AURA_FIERCE_FOLLOWTHROUGH, AURA_IMMINENT_DEMISE, AURA_MARTIAL_PROWESS,
15    AURA_MASTER_OF_WARFARE, AURA_MASTER_OF_WARFARE_DAMAGE, AURA_OVERWHELMED, AURA_SUDDEN_DEATH,
16    AURA_WINDING_UP, EFFECT, SPELL_AVATAR, SPELL_BLADESTORM, SPELL_COLOSSUS_SMASH, SPELL_EXECUTE,
17    SPELL_HEROIC_STRIKE, SPELL_MORTAL_STRIKE, SPELL_OVERPOWER, SPELL_REND, SPELL_SLAM,
18    SPELL_THUNDER_CLAP,
19};
20use crate::hooks::shared::warrior::{
21    RageSpendConfig, SpecialAttackConfig, consume_bladestorm_extension,
22    on_special_attack as on_warrior_special_attack,
23};
24
25use super::{ArmsConfig, config::arms_config};
26
27/// Execute damage scale: `2 x min(40, rage spent) / 40`.
28const EXECUTE_DMG_SCALE: f64 = 2.0;
29
30/// Returns Master of Arms mastery (1258398 e1).
31#[must_use]
32pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
33    ctx.bonus(1)
34}
35
36// Buff critical strike chance is intentionally excluded.
37fn sheet_crit(ctx: &HookCtx<'_>) -> f64 {
38    ctx.program().base_stats().stats.crit_chance / HUNDRED
39}
40
41fn on_special_attack(ctx: &mut HookCtx<'_>, cfg: &ArmsConfig, rage_spent: f64, mow_eligible: bool) {
42    on_warrior_special_attack(
43        ctx,
44        SpecialAttackConfig {
45            rage: RageSpendConfig {
46                anger_management_div: cfg.anger_management_div,
47                frothing_chance: cfg.frothing_chance,
48                frothing_refund_pct: cfg.frothing_refund_pct,
49                frothing_refund_source: EFFECT::FROTHING_REFUND,
50                avatar: SPELL_AVATAR,
51                paired_cooldown: SPELL_COLOSSUS_SMASH,
52                tactician: Some((cfg.tactician_chance, SPELL_OVERPOWER)),
53            },
54            sudden_death_driver:
55                wowlab_engine_combat::generated_identities::system_rppm::SUDDEN_DEATH,
56            sudden_death_aura: AURA_SUDDEN_DEATH,
57            execute: SPELL_EXECUTE,
58            slayers_dominance: cfg.slayers_dominance,
59            slayers_dominance_driver:
60                wowlab_engine_combat::generated_identities::proc_counter::SLAYERS_DOMINANCE_ATTEMPT,
61            prd_c: cfg.prd_c,
62            slayers_strike: crate::generated::specs::arms_warrior::DAMAGE_SPELL::SLAYERS_STRIKE,
63            slayers_strike_coef: cfg.slayers_strike_coef,
64            executioner: AURA_EXECUTIONER,
65            imminent_demise_driver:
66                wowlab_engine_combat::generated_identities::proc_counter::IMMINENT_DEMISE,
67            imminent_demise_every: cfg.imminent_demise_every,
68        },
69        rage_spent,
70    );
71
72    if mow_eligible
73        && cfg.master_of_warfare
74        && !ctx.is_aura_active(AURA_MASTER_OF_WARFARE)
75        && ctx.roll_accumulating_proc(
76            wowlab_engine_combat::generated_identities::proc_counter::MASTER_OF_WARFARE_ATTEMPT,
77            cfg.prd_c,
78        )
79    {
80        ctx.apply_aura(AURA_MASTER_OF_WARFARE);
81    }
82}
83
84pub(crate) fn mortal_strike_hook(ctx: &mut HookCtx<'_>) {
85    let cfg = arms_config(ctx);
86
87    ctx.expire_aura(AURA_MARTIAL_PROWESS);
88    ctx.expire_aura(AURA_EXECUTIONERS_PRECISION);
89    ctx.expire_aura(AURA_FIERCE_FOLLOWTHROUGH);
90
91    if cfg.fatality_chance > 0.0 && proc_chance(ctx.rng(), cfg.fatality_chance) {
92        ctx.apply_aura(AURA_FATAL_MARK);
93    }
94
95    let crit_chance = sheet_crit(ctx);
96    let crit_roll = proc_chance(ctx.rng(), crit_chance);
97
98    if cfg.mortal_wounds && crit_roll {
99        ctx.apply_aura(AURA_DEEP_WOUNDS);
100    }
101
102    if cfg.fierce_followthrough && crit_roll {
103        ctx.apply_aura(AURA_FIERCE_FOLLOWTHROUGH);
104    }
105
106    if cfg.mid2_4pc {
107        ctx.apply_aura(AURA_WINDING_UP);
108    }
109
110    let cost = ctx.spell_resource_cost(SPELL_MORTAL_STRIKE);
111
112    on_special_attack(ctx, &cfg, cost, true);
113}
114
115pub(crate) fn overpower_hook(ctx: &mut HookCtx<'_>) {
116    let cfg = arms_config(ctx);
117
118    if cfg.martial_prowess {
119        ctx.apply_aura(AURA_MARTIAL_PROWESS);
120    }
121
122    if cfg.mid2_4pc {
123        ctx.apply_aura(AURA_WINDING_UP);
124    }
125
126    ctx.deal_physical_damage_ap_if_positive(
127        crate::generated::specs::arms_warrior::DAMAGE_SPELL::DREADNAUGHT,
128        cfg.dreadnaught_coef,
129    );
130
131    if cfg.op_finish_pct > 0.0 && ctx.target_health_fraction() < cfg.op_finish_below {
132        ctx.deal_physical_damage_ap_if_positive(
133            crate::generated::specs::arms_warrior::DAMAGE_SPELL::OVERPOWER,
134            cfg.overpower_coef * cfg.op_finish_pct,
135        );
136    }
137
138    on_special_attack(ctx, &cfg, 0.0, true);
139}
140
141pub(crate) fn execute_hook(ctx: &mut HookCtx<'_>) {
142    let cfg = arms_config(ctx);
143
144    let sudden_death = ctx.is_aura_active(AURA_SUDDEN_DEATH);
145    let mult;
146    let mut spent = 0.0;
147
148    if sudden_death {
149        ctx.consume_aura(AURA_SUDDEN_DEATH);
150        mult = EXECUTE_DMG_SCALE;
151        let executioner = ctx.aura_stacks(AURA_EXECUTIONER);
152
153        if cfg.uo_cdr_ms_per_stack > 0 && executioner > 0 {
154            ctx.reduce_cooldown(
155                SPELL_BLADESTORM,
156                cfg.uo_cdr_ms_per_stack
157                    * wowlab_types::numeric::i32_to_u32_nonnegative(executioner),
158            );
159            ctx.apply_aura_n(AURA_OVERWHELMED, cfg.uo_overwhelmed_per_stack * executioner);
160        }
161
162        if cfg.imminent_demise_every > 0 {
163            ctx.apply_aura(AURA_IMMINENT_DEMISE);
164        }
165
166        if cfg.reap_the_storm
167            && ctx.proc_category_ready(
168                wowlab_engine_combat::generated_identities::proc_category::REAP_THE_STORM,
169            )
170        {
171            ctx.start_proc_category_cooldown(
172                wowlab_engine_combat::generated_identities::proc_category::REAP_THE_STORM,
173            );
174            ctx.deal_physical_damage_ap_if_positive(
175                crate::generated::specs::arms_warrior::DAMAGE_SPELL::REAP_THE_STORM_EXECUTE,
176                cfg.reap_coef,
177            );
178        }
179    } else {
180        spent = ctx.last_primary_spent();
181        mult = EXECUTE_DMG_SCALE * spent.min(cfg.execute_max_rage) / cfg.execute_max_rage;
182    }
183
184    ctx.deal_physical_damage_ap_if_positive(
185        crate::generated::specs::arms_warrior::DAMAGE_SPELL::EXECUTE_DAMAGE,
186        cfg.execute_coef * mult,
187    );
188
189    if spent > 0.0 && cfg.execute_refund_pct > 0.0 {
190        ctx.gain_resource_from(
191            spent * cfg.execute_refund_pct,
192            ResourceGainSource::SpellEffect {
193                spell_id: EFFECT::EXECUTE_REFUND.0,
194                effect_index: EFFECT::EXECUTE_REFUND.1,
195            },
196        );
197    }
198
199    if cfg.deep_wounds {
200        ctx.apply_aura(AURA_DEEP_WOUNDS);
201    }
202
203    if cfg.executioners_precision {
204        ctx.apply_aura(AURA_EXECUTIONERS_PRECISION);
205    }
206
207    let marks = ctx.aura_stacks(AURA_FATAL_MARK);
208
209    if cfg.fatality_chance > 0.0 && marks > 0 {
210        ctx.deal_physical_damage_ap_if_positive(
211            crate::generated::specs::arms_warrior::DAMAGE_SPELL::FATALITY,
212            cfg.fatality_coef * f64::from(marks),
213        );
214        ctx.expire_aura(AURA_FATAL_MARK);
215    }
216
217    on_special_attack(ctx, &cfg, spent, true);
218}
219
220/// Applies Slam hook effects, shared with spell 1269391 e1's Heroic Strike replacement.
221pub(crate) fn slam_hook(ctx: &mut HookCtx<'_>) {
222    let cfg = arms_config(ctx);
223    // The pipeline spends the Master of Warfare charge after the cast hook, so a live aura here
224    // means this press resolved as Heroic Strike.
225    let heroic_strike = ctx.is_aura_active(AURA_MASTER_OF_WARFARE);
226
227    if heroic_strike {
228        ctx.apply_aura(AURA_MASTER_OF_WARFARE_DAMAGE);
229    }
230
231    if cfg.martial_prowess {
232        ctx.apply_aura(AURA_MARTIAL_PROWESS);
233    }
234
235    if cfg.mid2_2pc {
236        let primary_target = ctx.target();
237        let effect =
238            crate::generated::specs::arms_warrior::DAMAGE_EFFECT::MID2_CONCUSSIVE_SLAM_DAMAGE;
239
240        ctx.with_resolved_spell_effect_targets(
241            effect,
242            crate::generated::specs::arms_warrior::DAMAGE_REPORTED::CONCUSSIVE_SLAM,
243            None,
244            |ctx, targets| {
245                let secondary_count = targets
246                    .iter()
247                    .filter(|target| Some(**target) != primary_target)
248                    .count();
249                let reduced_aoe = if cfg.mid2_concussive_slam_reduced_targets > 0
250                    && secondary_count > cfg.mid2_concussive_slam_reduced_targets
251                {
252                    (wowlab_types::numeric::usize_to_f64(
253                        cfg.mid2_concussive_slam_reduced_targets,
254                    ) / wowlab_types::numeric::usize_to_f64(secondary_count))
255                    .sqrt()
256                } else {
257                    1.0
258                };
259
260                for target in targets
261                    .iter()
262                    .copied()
263                    .filter(|target| Some(*target) != primary_target)
264                {
265                    ctx.with_resolved_target(target, |target_ctx| {
266                        target_ctx.deal_damage_ap_to_target(
267                            target,
268                            crate::generated::specs::arms_warrior::DAMAGE_REPORTED::CONCUSSIVE_SLAM,
269                            cfg.mid2_concussive_slam_coef * reduced_aoe,
270                            wowlab_engine_combat::DamageFlags::AOE
271                                | wowlab_engine_combat::DamageFlags::PHYSICAL,
272                        );
273                    });
274                }
275            },
276        );
277    }
278
279    ctx.consume_aura(AURA_WINDING_UP);
280
281    let crit_chance = sheet_crit(ctx);
282
283    if cfg.mortal_wounds && proc_chance(ctx.rng(), crit_chance) {
284        ctx.apply_aura(AURA_DEEP_WOUNDS);
285    }
286
287    let resolved_spell = if heroic_strike {
288        SPELL_HEROIC_STRIKE
289    } else {
290        SPELL_SLAM
291    };
292    let resource_cost = ctx.spell_resource_cost(resolved_spell);
293
294    on_special_attack(ctx, &cfg, resource_cost, true);
295}
296
297pub(crate) fn colossus_smash_hook(ctx: &mut HookCtx<'_>) {
298    let cfg = arms_config(ctx);
299
300    on_special_attack(ctx, &cfg, 0.0, false);
301}
302
303pub(crate) fn bladestorm_hook(ctx: &mut HookCtx<'_>) {
304    let cfg = arms_config(ctx);
305    let duration = consume_bladestorm_extension(
306        ctx,
307        AURA_IMMINENT_DEMISE,
308        AURA_BLADESTORM,
309        cfg.bladestorm_duration_ms,
310        cfg.bladestorm_period_ms,
311    );
312
313    if cfg.overwhelming_blades
314        && let Some(duration) = duration
315    {
316        let _ = ctx.trigger_periodic_driver_for(
317            wowlab_engine_combat::generated_identities::periodic_driver::OVERWHELMING_BLADES,
318            duration.get(),
319        );
320    }
321
322    if cfg.unhinged
323        && let Some(duration) = duration
324    {
325        let _ = ctx.trigger_periodic_driver_for(
326            wowlab_engine_combat::generated_identities::periodic_driver::UNHINGED,
327            duration.get(),
328        );
329    }
330
331    on_special_attack(ctx, &cfg, 0.0, false);
332}
333
334pub(super) fn overwhelming_blades_tick(ctx: &mut HookCtx<'_>) {
335    ctx.apply_aura(AURA_OVERWHELMED);
336}
337
338pub(super) fn unhinged_tick(ctx: &mut HookCtx<'_>) {
339    let cfg = arms_config(ctx);
340
341    ctx.deal_physical_damage_ap_if_positive(
342        crate::generated::specs::arms_warrior::DAMAGE_TALENT::UNHINGED,
343        cfg.unhinged_coef,
344    );
345}
346
347pub(crate) fn rend_hook(ctx: &mut HookCtx<'_>) {
348    let cfg = arms_config(ctx);
349    let cost = ctx.spell_resource_cost(SPELL_REND);
350
351    on_special_attack(ctx, &cfg, cost, false);
352}
353
354pub(crate) fn thunder_clap_hook(ctx: &mut HookCtx<'_>) {
355    let cfg = arms_config(ctx);
356    let cost = ctx.spell_resource_cost(SPELL_THUNDER_CLAP);
357
358    on_special_attack(ctx, &cfg, cost, false);
359}
360
361pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
362    let is_crit = event.is_crit;
363
364    if !is_crit {
365        return;
366    }
367
368    let cfg = arms_config(ctx);
369
370    if cfg.crit_swing_extra_rage > 0.0 {
371        ctx.gain_resource_from(cfg.crit_swing_extra_rage, ResourceGainSource::AutoAttack);
372    }
373}