Skip to main content

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