Skip to main content

wowlab_engine_content/hooks/arms_warrior/
hooks.rs

1use 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::SchedulingOps as _;
6use wowlab_engine_combat::AuraOps 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_COLOSSUS_SMASH, AURA_DEEP_WOUNDS, AURA_EXECUTIONER,
14    AURA_EXECUTIONERS_PRECISION, AURA_FATAL_MARK, AURA_FIERCE_FOLLOWTHROUGH, AURA_IMMINENT_DEMISE,
15    AURA_MARTIAL_PROWESS, AURA_MASTER_OF_WARFARE, AURA_MASTER_OF_WARFARE_DAMAGE, AURA_OVERWHELMED,
16    AURA_SUDDEN_DEATH, EFFECT, HERO, SPELL, SPELL_AVATAR, SPELL_BLADESTORM, SPELL_COLOSSUS_SMASH,
17    SPELL_EXECUTE, SPELL_HEROIC_STRIKE, SPELL_MORTAL_STRIKE, SPELL_OVERPOWER, SPELL_REND,
18    SPELL_SLAM, SPELL_THUNDER_CLAP, TALENT,
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.state().config.base_stats.stats.crit_chance / HUNDRED
39}
40
41fn on_special_attack(
42    ctx: &mut HookCtx<'_>,
43    cfg: &ArmsConfig,
44    rage_spent: f64,
45    mow_eligible: bool,
46) {
47    on_warrior_special_attack(
48        ctx,
49        SpecialAttackConfig {
50            rage: RageSpendConfig {
51                anger_management_div: cfg.anger_management_div,
52                frothing_chance: cfg.frothing_chance,
53                frothing_refund_pct: cfg.frothing_refund_pct,
54                frothing_refund_source: EFFECT::FROTHING_REFUND,
55                avatar: SPELL_AVATAR,
56                paired_cooldown: SPELL_COLOSSUS_SMASH,
57                tactician: Some((cfg.tactician_chance, SPELL_OVERPOWER)),
58            },
59            sudden_death_driver: TALENT::SUDDEN_DEATH,
60            sudden_death_aura: AURA_SUDDEN_DEATH,
61            execute: SPELL_EXECUTE,
62            slayers_dominance: cfg.slayers_dominance,
63            slayers_dominance_driver: TALENT::SLAYERS_DOMINANCE,
64            prd_c: cfg.prd_c,
65            slayers_strike: HERO::SLAYER::SPELL::SLAYERS_STRIKE,
66            slayers_strike_coef: cfg.slayers_strike_coef,
67            executioner: AURA_EXECUTIONER,
68            imminent_demise_driver: TALENT::IMMINENT_DEMISE,
69            imminent_demise_every: cfg.imminent_demise_every,
70        },
71        rage_spent,
72    );
73
74    if mow_eligible
75        && cfg.master_of_warfare
76        && !ctx.is_aura_active(AURA_MASTER_OF_WARFARE.raw())
77        && ctx.roll_accumulating_proc(TALENT::MASTER_OF_WARFARE, cfg.prd_c)
78    {
79        ctx.apply_aura(AURA_MASTER_OF_WARFARE.raw());
80    }
81}
82
83pub(crate) fn mortal_strike_hook(ctx: &mut HookCtx<'_>) {
84    let cfg = arms_config(ctx);
85
86    ctx.expire_aura(AURA_MARTIAL_PROWESS.raw());
87    ctx.expire_aura(AURA_EXECUTIONERS_PRECISION.raw());
88    ctx.expire_aura(AURA_FIERCE_FOLLOWTHROUGH.raw());
89
90    if cfg.fatality_chance > 0.0 && proc_chance(ctx.rng(), cfg.fatality_chance) {
91        ctx.apply_aura(AURA_FATAL_MARK.raw());
92    }
93
94    let crit_chance = sheet_crit(ctx);
95    let crit_roll = proc_chance(ctx.rng(), crit_chance);
96
97    if cfg.mortal_wounds && crit_roll {
98        ctx.apply_aura(AURA_DEEP_WOUNDS.raw());
99    }
100
101    if cfg.fierce_followthrough && crit_roll {
102        ctx.apply_aura(AURA_FIERCE_FOLLOWTHROUGH.raw());
103    }
104
105    if cfg.set4pc_cs_extend_ms > 0 && ctx.is_aura_active(AURA_COLOSSUS_SMASH.raw()) {
106        ctx.extend_aura(AURA_COLOSSUS_SMASH.raw(), cfg.set4pc_cs_extend_ms);
107    }
108
109    let cost = ctx.spell_resource_cost(SPELL_MORTAL_STRIKE.raw());
110
111    on_special_attack(ctx, &cfg, cost, true);
112}
113
114pub(crate) fn overpower_hook(ctx: &mut HookCtx<'_>) {
115    let cfg = arms_config(ctx);
116
117    if cfg.martial_prowess {
118        ctx.apply_aura(AURA_MARTIAL_PROWESS.raw());
119    }
120
121    ctx.deal_physical_damage_ap_if_positive(SPELL::DREADNAUGHT, cfg.dreadnaught_coef);
122
123    if cfg.op_finish_pct > 0.0 && ctx.target_health_fraction() < cfg.op_finish_below {
124        ctx.deal_physical_damage_ap_if_positive(
125            SPELL::OVERPOWER,
126            cfg.overpower_coef * cfg.op_finish_pct,
127        );
128    }
129
130    on_special_attack(ctx, &cfg, 0.0, true);
131}
132
133pub(crate) fn execute_hook(ctx: &mut HookCtx<'_>) {
134    let cfg = arms_config(ctx);
135
136    let sudden_death = ctx.is_aura_active(AURA_SUDDEN_DEATH.raw());
137    let mult;
138    let mut spent = 0.0;
139
140    if sudden_death {
141        ctx.consume_aura(AURA_SUDDEN_DEATH.raw());
142        mult = EXECUTE_DMG_SCALE;
143        let executioner = ctx.aura_stacks(AURA_EXECUTIONER.raw());
144
145        if cfg.uo_cdr_ms_per_stack > 0 && executioner > 0 {
146            ctx.reduce_cooldown(
147                SPELL_BLADESTORM.raw(),
148                cfg.uo_cdr_ms_per_stack
149                    * wowlab_types::numeric::i32_to_u32_nonnegative(executioner),
150            );
151            ctx.apply_aura_n(
152                AURA_OVERWHELMED.raw(),
153                cfg.uo_overwhelmed_per_stack * executioner,
154            );
155        }
156
157        if cfg.imminent_demise_every > 0 {
158            ctx.apply_aura(AURA_IMMINENT_DEMISE.raw());
159        }
160
161        if cfg.reap_the_storm && ctx.proc_category_ready(TALENT::REAP_THE_STORM) {
162            ctx.start_proc_category_cooldown(TALENT::REAP_THE_STORM);
163            ctx.deal_physical_damage_ap_if_positive(
164                HERO::SLAYER::SPELL::REAP_THE_STORM_EXECUTE,
165                cfg.reap_coef,
166            );
167        }
168    } else {
169        spent = ctx.last_primary_spent();
170        mult = EXECUTE_DMG_SCALE * spent.min(cfg.execute_max_rage) / cfg.execute_max_rage;
171    }
172
173    ctx.deal_physical_damage_ap_if_positive(SPELL::EXECUTE_DAMAGE, cfg.execute_coef * mult);
174
175    if spent > 0.0 && cfg.execute_refund_pct > 0.0 {
176        ctx.gain_resource_from(
177            spent * cfg.execute_refund_pct,
178            ResourceGainSource::SpellEffect {
179                spell_id: EFFECT::EXECUTE_REFUND.0,
180                effect_index: EFFECT::EXECUTE_REFUND.1,
181            },
182        );
183    }
184
185    if cfg.deep_wounds {
186        ctx.apply_aura(AURA_DEEP_WOUNDS.raw());
187    }
188
189    if cfg.executioners_precision {
190        ctx.apply_aura(AURA_EXECUTIONERS_PRECISION.raw());
191    }
192
193    let marks = ctx.aura_stacks(AURA_FATAL_MARK.raw());
194
195    if cfg.fatality_chance > 0.0 && marks > 0 {
196        ctx.deal_physical_damage_ap_if_positive(
197            SPELL::FATALITY,
198            cfg.fatality_coef * f64::from(marks),
199        );
200        ctx.expire_aura(AURA_FATAL_MARK.raw());
201    }
202
203    on_special_attack(ctx, &cfg, spent, true);
204}
205
206/// Applies Slam hook effects, shared with spell 1269391 e1's Heroic Strike replacement.
207pub(crate) fn slam_hook(ctx: &mut HookCtx<'_>) {
208    let cfg = arms_config(ctx);
209    // The pipeline spends the Master of Warfare charge after the cast hook, so a live aura here
210    // means this press resolved as Heroic Strike (sc_warrior.cpp:3487, heroic_strike_t : slam_base_t).
211    let heroic_strike = ctx.is_aura_active(AURA_MASTER_OF_WARFARE.raw());
212
213    if heroic_strike {
214        ctx.apply_aura(AURA_MASTER_OF_WARFARE_DAMAGE.raw());
215    }
216
217    if cfg.martial_prowess {
218        ctx.apply_aura(AURA_MARTIAL_PROWESS.raw());
219    }
220
221    let crit_chance = sheet_crit(ctx);
222
223    if cfg.mortal_wounds && proc_chance(ctx.rng(), crit_chance) {
224        ctx.apply_aura(AURA_DEEP_WOUNDS.raw());
225    }
226
227    let resolved_spell = if heroic_strike {
228        SPELL_HEROIC_STRIKE
229    } else {
230        SPELL_SLAM
231    };
232    let resource_cost = ctx.spell_resource_cost(resolved_spell.raw());
233
234    on_special_attack(ctx, &cfg, resource_cost, true);
235}
236
237pub(crate) fn colossus_smash_hook(ctx: &mut HookCtx<'_>) {
238    let cfg = arms_config(ctx);
239
240    on_special_attack(ctx, &cfg, 0.0, false);
241}
242
243pub(crate) fn bladestorm_hook(ctx: &mut HookCtx<'_>) {
244    let cfg = arms_config(ctx);
245    let duration = consume_bladestorm_extension(
246        ctx,
247        AURA_IMMINENT_DEMISE,
248        AURA_BLADESTORM,
249        cfg.bladestorm_duration_ms,
250        cfg.bladestorm_period_ms,
251    );
252
253    if cfg.overwhelming_blades
254        && let Some(duration) = duration
255    {
256        let _ = ctx.trigger_periodic_driver_for(TALENT::OVERWHELMING_BLADES, duration.get());
257    }
258
259    if cfg.unhinged
260        && let Some(duration) = duration
261    {
262        let _ = ctx.trigger_periodic_driver_for(TALENT::UNHINGED, duration.get());
263    }
264
265    on_special_attack(ctx, &cfg, 0.0, false);
266}
267
268pub(super) fn overwhelming_blades_tick(ctx: &mut HookCtx<'_>) {
269    ctx.apply_aura(AURA_OVERWHELMED.raw());
270}
271
272pub(super) fn unhinged_tick(ctx: &mut HookCtx<'_>) {
273    let cfg = arms_config(ctx);
274
275    ctx.deal_physical_damage_ap_if_positive(TALENT::UNHINGED, cfg.unhinged_coef);
276}
277
278pub(crate) fn rend_hook(ctx: &mut HookCtx<'_>) {
279    let cfg = arms_config(ctx);
280    let cost = ctx.spell_resource_cost(SPELL_REND.raw());
281
282    on_special_attack(ctx, &cfg, cost, false);
283}
284
285pub(crate) fn thunder_clap_hook(ctx: &mut HookCtx<'_>) {
286    let cfg = arms_config(ctx);
287    let cost = ctx.spell_resource_cost(SPELL_THUNDER_CLAP.raw());
288
289    on_special_attack(ctx, &cfg, cost, false);
290}
291
292pub(crate) fn swing_hook(
293    ctx: &mut HookCtx<'_>,
294    event: SwingEvent,
295) {
296    let is_crit = event.is_crit;
297
298    if !is_crit {
299        return;
300    }
301
302    let cfg = arms_config(ctx);
303
304    if cfg.crit_swing_extra_rage > 0.0 {
305        ctx.gain_resource_from(
306            cfg.crit_swing_extra_rage,
307            ResourceGainSource::AutoAttack,
308        );
309    }
310}