Skip to main content

wowlab_engine_content/hooks/fury_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::{
7    HookCtx, ImpactEvent, MasteryCtx, ResourceGainSource, SwingEvent, SwingHand,
8};
9use wowlab_engine_rng::proc_chance;
10use wowlab_types::constants::HUNDRED;
11
12use super::config::{fury_config, FuryConfig, FuryRuntime, RAGE_DISPLAY_DIV};
13use crate::{
14    generated::specs::fury_warrior::{
15        AURA_BLADESTORM, AURA_BLOODBATH_DOT, AURA_BLOODBORNE, AURA_DEEP_WOUNDS, AURA_ENRAGE,
16        AURA_EXECUTIONER, AURA_EXECUTIONERS_WRATH, AURA_FRENZY, AURA_GUSHING_WOUND,
17        AURA_IMMINENT_DEMISE, AURA_MID2_RECKLESSNESS_CRIT, AURA_OVERWHELMED, AURA_RECKLESSNESS,
18        AURA_SUDDEN_DEATH, AURA_SURGE_OF_ADRENALINE, AURA_WHIRLWIND, AURA_WILD_STRIKES, EFFECT,
19        SPELL, SPELL_AVATAR, SPELL_BLADESTORM, SPELL_EXECUTE, SPELL_RAGING_BLOW, SPELL_RAMPAGE,
20        SPELL_RECKLESSNESS, SPELL_REND, SPELL_SLAM, SPELL_THUNDER_CLAP,
21    },
22    hooks::shared::warrior::{
23        consume_bladestorm_extension, on_special_attack as on_warrior_special_attack,
24        RageSpendConfig, SpecialAttackConfig,
25    },
26};
27
28const UNHINGED_TICK_INTERVAL: u32 = 2;
29
30/// Unshackled Fury mastery.
31#[must_use]
32pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
33    if ctx.is_aura_active(AURA_ENRAGE) {
34        ctx.bonus(1)
35    } else {
36        1.0
37    }
38}
39
40fn on_special_attack(ctx: &mut HookCtx<'_>, cfg: &FuryConfig, rage_spent: f64) {
41    on_warrior_special_attack(
42        ctx,
43        SpecialAttackConfig {
44            rage: RageSpendConfig {
45                anger_management_div: cfg.anger_management_div,
46                frothing_chance: cfg.frothing_chance,
47                frothing_refund_pct: cfg.frothing_refund_pct,
48                frothing_refund_source: EFFECT::FROTHING_REFUND,
49                avatar: SPELL_AVATAR,
50                paired_cooldown: SPELL_RECKLESSNESS,
51                tactician: None,
52            },
53            sudden_death_driver:
54                wowlab_engine_combat::generated_identities::system_rppm::SUDDEN_DEATH,
55            sudden_death_aura: AURA_SUDDEN_DEATH,
56            execute: SPELL_EXECUTE,
57            slayers_dominance: cfg.slayers_dominance,
58            slayers_dominance_driver:
59                wowlab_engine_combat::generated_identities::proc_counter::SLAYERS_DOMINANCE_ATTEMPT,
60            prd_c: cfg.prd_c,
61            slayers_strike: crate::generated::specs::fury_warrior::DAMAGE_SPELL::SLAYERS_STRIKE,
62            slayers_strike_coef: cfg.slayers_strike_coef,
63            executioner: AURA_EXECUTIONER,
64            imminent_demise_driver:
65                wowlab_engine_combat::generated_identities::proc_counter::IMMINENT_DEMISE,
66            imminent_demise_every: cfg.imminent_demise_every,
67        },
68        rage_spent,
69    );
70}
71
72pub(crate) fn bloodthirst_hook(ctx: &mut HookCtx<'_>) {
73    enrage_special_attack(
74        ctx,
75        crate::generated::specs::fury_warrior::DAMAGE_EFFECT::BLOODTHIRST_ENRAGE_CHANCE,
76    );
77}
78
79/// Handles Bloodbath's Enrage roll; its on-impact bleeds use shared impact procs.
80pub(crate) fn bloodbath_hook(ctx: &mut HookCtx<'_>) {
81    enrage_special_attack(
82        ctx,
83        crate::generated::specs::fury_warrior::DAMAGE_EFFECT::BLOODBATH_ENRAGE_CHANCE,
84    );
85}
86
87fn enrage_special_attack(
88    ctx: &mut HookCtx<'_>,
89    enrage_effect: wowlab_engine_combat::DamageEffectId,
90) {
91    let chance = ctx.effect_base_points(enrage_effect) / HUNDRED;
92
93    if proc_chance(ctx.rng(), chance) {
94        ctx.apply_aura(AURA_ENRAGE);
95    }
96
97    let cfg = fury_config(ctx);
98
99    if cfg.bloodborne {
100        ctx.apply_aura(AURA_BLOODBORNE);
101    }
102
103    if cfg.mid2_4pc && ctx.is_aura_active(AURA_RECKLESSNESS) {
104        ctx.apply_aura(AURA_MID2_RECKLESSNESS_CRIT);
105    }
106
107    on_special_attack(ctx, &cfg, 0.0);
108}
109
110pub(super) const fn bloodbath_impact_filter(spell_id: u32) -> bool {
111    spell_id == SPELL::BLOODBATH
112}
113
114pub(super) fn bloodbath_dot_impact(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
115    ctx.apply_aura(AURA_BLOODBATH_DOT);
116}
117
118pub(super) const fn bloodthirst_family_impact_filter(spell_id: u32) -> bool {
119    matches!(spell_id, SPELL::BLOODTHIRST | SPELL::BLOODBATH)
120}
121
122pub(super) fn cold_steel_hot_blood_impact(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
123    ctx.apply_aura(AURA_GUSHING_WOUND);
124    let amount = ctx.effect_base_points(
125        crate::generated::specs::fury_warrior::DAMAGE_EFFECT::COLD_STEEL_HOT_BLOOD_RAGE,
126    ) / RAGE_DISPLAY_DIV;
127
128    ctx.gain_resource_from(
129        amount,
130        ResourceGainSource::SpellEffect {
131            spell_id: EFFECT::COLD_STEEL_HOT_BLOOD_RAGE.0,
132            effect_index: EFFECT::COLD_STEEL_HOT_BLOOD_RAGE.1,
133        },
134    );
135}
136
137fn raging_blow_reset_riders(ctx: &mut HookCtx<'_>, cfg: &FuryConfig) {
138    if cfg.rb_reset_chance <= 0.0 {
139        return;
140    }
141
142    let mut chance = cfg.rb_reset_chance;
143
144    if ctx.is_aura_active(AURA_ENRAGE) {
145        chance += cfg.wnf_reset_chance;
146    }
147
148    if !proc_chance(ctx.rng(), chance) {
149        return;
150    }
151
152    let recharge_ms = ctx.program().spell(SPELL_RAGING_BLOW).cooldown.recharge_ms;
153
154    ctx.reduce_cooldown(SPELL_RAGING_BLOW, recharge_ms);
155
156    if cfg.surge_of_adrenaline {
157        ctx.apply_aura(AURA_SURGE_OF_ADRENALINE);
158    }
159}
160
161pub(crate) fn raging_blow_hook(ctx: &mut HookCtx<'_>) {
162    let cfg = fury_config(ctx);
163
164    raging_blow_reset_riders(ctx, &cfg);
165    extend_mid2_recklessness(ctx, &cfg);
166    on_special_attack(ctx, &cfg, 0.0);
167}
168
169pub(crate) fn crushing_blow_hook(ctx: &mut HookCtx<'_>) {
170    let cfg = fury_config(ctx);
171
172    raging_blow_reset_riders(ctx, &cfg);
173    extend_mid2_recklessness(ctx, &cfg);
174    on_special_attack(ctx, &cfg, 0.0);
175}
176
177fn extend_mid2_recklessness(ctx: &mut HookCtx<'_>, cfg: &FuryConfig) {
178    if cfg.mid2_2pc_extension_ms == 0 || !ctx.is_aura_active(AURA_RECKLESSNESS) {
179        return;
180    }
181
182    let extensions = ctx
183        .spec_runtime::<FuryRuntime>()
184        .map_or(0, |runtime| runtime.mid2_recklessness_extensions);
185
186    if extensions >= cfg.mid2_2pc_extension_cap {
187        return;
188    }
189
190    ctx.extend_aura(AURA_RECKLESSNESS, cfg.mid2_2pc_extension_ms);
191
192    if let Some(runtime) = ctx.spec_runtime_mut::<FuryRuntime>() {
193        runtime.mid2_recklessness_extensions = extensions.saturating_add(1);
194    }
195}
196
197pub(crate) fn rampage_hook(ctx: &mut HookCtx<'_>) {
198    let cfg = fury_config(ctx);
199
200    if cfg.frenzy {
201        ctx.apply_aura(AURA_FRENZY);
202    }
203
204    let cost = ctx.spell_resource_cost(SPELL_RAMPAGE);
205
206    on_special_attack(ctx, &cfg, cost);
207}
208
209fn rampaging_ruin_active(ctx: &HookCtx<'_>) -> bool {
210    ctx.talent_selected(crate::generated::specs::fury_warrior::DAMAGE_TALENT::RAMPAGING_RUIN)
211        && ctx.is_aura_active(AURA_WHIRLWIND)
212}
213
214pub(crate) fn rampage_single_target_trigger(ctx: &HookCtx<'_>) -> bool {
215    !rampaging_ruin_active(ctx)
216}
217
218pub(crate) fn rampaging_ruin_trigger(ctx: &HookCtx<'_>) -> bool {
219    rampaging_ruin_active(ctx)
220}
221
222pub(crate) fn focus_in_chaos_miss_chance(ctx: &HookCtx<'_>, _hand: SwingHand) -> Option<f64> {
223    ctx.is_aura_active(AURA_ENRAGE).then_some(0.0)
224}
225
226pub(crate) fn recklessness_hook(ctx: &mut HookCtx<'_>) {
227    ctx.expire_aura(AURA_MID2_RECKLESSNESS_CRIT);
228
229    if let Some(runtime) = ctx.spec_runtime_mut::<FuryRuntime>() {
230        runtime.mid2_recklessness_extensions = 0;
231    }
232}
233
234pub(crate) fn recklessness_expire_hook(ctx: &mut HookCtx<'_>) {
235    ctx.expire_aura(AURA_MID2_RECKLESSNESS_CRIT);
236}
237
238pub(crate) fn execute_hook(ctx: &mut HookCtx<'_>) {
239    let cfg = fury_config(ctx);
240
241    if cfg.deep_wounds {
242        ctx.apply_aura(AURA_DEEP_WOUNDS);
243    }
244
245    if ctx.is_aura_active(AURA_SUDDEN_DEATH) {
246        ctx.consume_aura(AURA_SUDDEN_DEATH);
247
248        if cfg.imminent_demise_every > 0 {
249            ctx.apply_aura(AURA_IMMINENT_DEMISE);
250        }
251
252        if cfg.reap_chance > 0.0
253            && ctx.proc_category_ready(
254                wowlab_engine_combat::generated_identities::proc_category::REAP_THE_STORM,
255            )
256            && proc_chance(ctx.rng(), cfg.reap_chance)
257        {
258            ctx.start_proc_category_cooldown(
259                wowlab_engine_combat::generated_identities::proc_category::REAP_THE_STORM,
260            );
261            ctx.deal_physical_damage_ap_if_positive(
262                crate::generated::specs::fury_warrior::DAMAGE_SPELL::REAP_THE_STORM,
263                cfg.reap_coef,
264            );
265        }
266
267        let executioner = ctx.aura_stacks(AURA_EXECUTIONER);
268
269        if cfg.uo_cdr_ms_per_stack > 0 && executioner > 0 {
270            ctx.reduce_cooldown(
271                SPELL_BLADESTORM,
272                cfg.uo_cdr_ms_per_stack
273                    * wowlab_types::numeric::i32_to_u32_nonnegative(executioner),
274            );
275            ctx.apply_aura_n(AURA_OVERWHELMED, cfg.uo_overwhelmed_per_stack * executioner);
276        }
277    }
278
279    if cfg.executioners_wrath {
280        ctx.apply_aura(AURA_EXECUTIONERS_WRATH);
281    }
282
283    let cost = ctx.spell_resource_cost(SPELL_EXECUTE);
284
285    on_special_attack(ctx, &cfg, cost);
286}
287
288/// Handles Whirlwind's proc riders; its repeated child hits execute from DBC.
289pub(crate) fn whirlwind_hook(ctx: &mut HookCtx<'_>) {
290    let cfg = fury_config(ctx);
291
292    on_special_attack(ctx, &cfg, 0.0);
293}
294
295pub(crate) fn bladestorm_hook(ctx: &mut HookCtx<'_>) {
296    let cfg = fury_config(ctx);
297    let _ = consume_bladestorm_extension(
298        ctx,
299        AURA_IMMINENT_DEMISE,
300        AURA_BLADESTORM,
301        cfg.bladestorm_duration_ms,
302        cfg.bladestorm_period_ms,
303    );
304
305    ctx.reset_proc_counter(wowlab_engine_combat::generated_identities::proc_counter::UNHINGED);
306
307    on_special_attack(ctx, &cfg, 0.0);
308}
309
310pub(crate) fn bladestorm_tick_hook(ctx: &mut HookCtx<'_>) {
311    let cfg = fury_config(ctx);
312
313    if !cfg.unhinged
314        || ctx
315            .add_proc_counter(
316                wowlab_engine_combat::generated_identities::proc_counter::UNHINGED,
317                1,
318            )
319            .is_multiple_of(UNHINGED_TICK_INTERVAL)
320    {
321        return;
322    }
323
324    let bloodbath = ctx
325        .talent_selected(crate::generated::specs::fury_warrior::DAMAGE_TALENT::RECKLESS_ABANDON)
326        && ctx.is_aura_active(AURA_RECKLESSNESS);
327    let (spell_id, coefficient, rage_effect, rage, enrage_effect) = if bloodbath {
328        (
329            crate::generated::specs::fury_warrior::DAMAGE_SPELL::BLOODBATH,
330            cfg.unhinged_bloodbath_coef,
331            EFFECT::BLOODBATH_RAGE,
332            cfg.unhinged_bloodbath_rage,
333            crate::generated::specs::fury_warrior::DAMAGE_EFFECT::BLOODBATH_ENRAGE_CHANCE,
334        )
335    } else {
336        (
337            crate::generated::specs::fury_warrior::DAMAGE_SPELL::BLOODTHIRST,
338            cfg.unhinged_bloodthirst_coef,
339            EFFECT::BLOODTHIRST_RAGE,
340            cfg.unhinged_bloodthirst_rage,
341            crate::generated::specs::fury_warrior::DAMAGE_EFFECT::BLOODTHIRST_ENRAGE_CHANCE,
342        )
343    };
344
345    ctx.deal_physical_damage_ap_if_positive(spell_id, coefficient);
346
347    if cfg.bloodborne {
348        ctx.apply_aura(AURA_BLOODBORNE);
349    }
350
351    ctx.gain_resource_from(
352        rage,
353        ResourceGainSource::SpellEffect {
354            spell_id: rage_effect.0,
355            effect_index: rage_effect.1,
356        },
357    );
358
359    let enrage_chance = ctx.effect_percent(enrage_effect);
360
361    if proc_chance(ctx.rng(), enrage_chance) {
362        ctx.apply_aura(AURA_ENRAGE);
363    }
364}
365
366/// Handles Odyn's Fury's proc riders; its child hits and bleed execute from DBC.
367pub(crate) fn odyns_fury_hook(ctx: &mut HookCtx<'_>) {
368    let cfg = fury_config(ctx);
369
370    on_special_attack(ctx, &cfg, 0.0);
371}
372
373/// Rend: costed special (proc web; direct hit and `DoT` are manifest-driven).
374pub(crate) fn rend_hook(ctx: &mut HookCtx<'_>) {
375    let cfg = fury_config(ctx);
376    let cost = ctx.spell_resource_cost(SPELL_REND);
377
378    on_special_attack(ctx, &cfg, cost);
379}
380
381/// Thunder Clap: costed special (proc web only).
382pub(crate) fn thunder_clap_hook(ctx: &mut HookCtx<'_>) {
383    let cfg = fury_config(ctx);
384    let cost = ctx.spell_resource_cost(SPELL_THUNDER_CLAP);
385
386    on_special_attack(ctx, &cfg, cost);
387}
388
389/// Slam: costed special (proc web only).
390pub(crate) fn slam_hook(ctx: &mut HookCtx<'_>) {
391    let cfg = fury_config(ctx);
392    let cost = ctx.spell_resource_cost(SPELL_SLAM);
393
394    on_special_attack(ctx, &cfg, cost);
395}
396
397pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
398    let is_crit = event.is_crit;
399
400    if !is_crit {
401        return;
402    }
403
404    let cfg = fury_config(ctx);
405
406    if cfg.wild_strikes
407        && ctx.proc_category_ready(
408            wowlab_engine_combat::generated_identities::proc_category::WILD_STRIKES,
409        )
410    {
411        ctx.apply_aura(AURA_WILD_STRIKES);
412        ctx.start_proc_category_cooldown(
413            wowlab_engine_combat::generated_identities::proc_category::WILD_STRIKES,
414        );
415    }
416}