wowlab_engine_content/hooks/fury_warrior/
hooks.rs1use 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#[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
79pub(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
288pub(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
366pub(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
373pub(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
381pub(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
389pub(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}