wowlab_engine_content/hooks/fury_warrior/
hooks.rs1use 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#[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
77pub(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
248pub(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
321pub(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
328pub(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
336pub(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
344pub(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}