1use wowlab_engine_combat::{
2 AuraOps as _, CooldownOps as _, DamageFlags, DamageOps as _, HookCtx, ImpactEvent, MasteryCtx,
3 ResourceOps as _, SwingEvent, SwingHand,
4};
5use wowlab_engine_rng::proc_chance;
6use wowlab_types::constants::HUNDRED;
7
8use super::config::BeastMasteryConfig;
9use crate::{
10 generated::specs::beast_mastery_hunter::{
11 AURA, AURA_BARBED_SHOT_REGEN, AURA_BEAR_ACTIVE, AURA_BEAR_READY, AURA_BEAST_CLEAVE,
12 AURA_BLOODSHED_DOT, AURA_BLOODY_FRENZY, AURA_BOAR_READY, AURA_COBRA_FANG,
13 AURA_DIRE_BEAST_ACTIVE, AURA_HOWL_COOLDOWN, AURA_HUNTMASTERS_CALL, AURA_NATURES_ALLY,
14 AURA_NATURES_ALLY_KILL_COMMAND, AURA_REND_FLESH, AURA_WYVERN_READY, AURA_WYVERNS_CRY,
15 DAMAGE_EFFECT as MID2_DAMAGE_EFFECT, DAMAGE_REPORTED as MID2_DAMAGE_REPORTED, HERO,
16 SPELL_BARBED_SHOT, SPELL_KILL_COMMAND,
17 },
18 hooks::shared::hunter::{PackLeaderHowl, apply_next_howl_ready, consume_howl},
19};
20
21const STABLE_PET_AP: f64 = 0.6;
23
24const DIRE_BEAST_AP: f64 = 1.0;
26
27const CALL_PET_AP: f64 = 2.0;
29const PET_SWING_MS: u32 = 2_000;
30const BEAR_SWING_MS: u32 = 1_500;
31
32const PERMANENT_PETS: usize = 2;
34
35const WAR_ORDERS_KC_CDR_MS: u32 = 3_000;
37
38const CALL_PET_SWINGS: f64 = 5.0;
40
41const RAVENOUS_LEAP_TICKS: f64 = 8.0;
43
44const HUNTMASTERS_CALL_CAP: i32 = 3;
46
47const FENRYR_CHANCE: f64 = 0.5;
49
50const STAMPEDE_HITS: f64 = 24.0;
52
53const BEAR_SWINGS: f64 = 6.7;
55const BEAR_AUTO_MULT: f64 = 7.0;
56
57const URSINE_FURY_DIRE_BEASTS: i64 = 2;
59
60const MASTERY_SHOTS_DIRECT: u8 = 5;
62const MASTERY_SHOTS_PERIODIC: u8 = 6;
63
64#[must_use]
66pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
67 if ctx.is_pet
68 || ctx.spell_id == HERO::BM_PETS::SPELL::BLOODSHED
69 || ctx.spell_id == AURA::REND_FLESH
70 {
71 return ctx.bonus(1);
72 }
73
74 if ctx.is_periodic {
79 ctx.affected_spell_bonus(MASTERY_SHOTS_PERIODIC)
80 } else {
81 ctx.affected_spell_bonus(MASTERY_SHOTS_DIRECT)
82 }
83}
84
85fn pet_mult(ctx: &HookCtx<'_>) -> f64 {
86 let cry = f64::from(ctx.aura_stacks(AURA_WYVERNS_CRY));
87
88 1.0 + cry
89 * ctx.effect_percent(
90 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::WYVERNS_CRY_PCT,
91 )
92}
93
94fn stable_pets(ctx: &HookCtx<'_>) -> usize {
95 if ctx.is_aura_active(AURA_NATURES_ALLY) {
96 PERMANENT_PETS + 1
97 } else {
98 PERMANENT_PETS
99 }
100}
101
102fn advance_howl_cycle(ctx: &mut HookCtx<'_>) {
103 if ctx.is_aura_active(AURA_HOWL_COOLDOWN) {
104 return;
105 }
106
107 let ready_up = ctx.is_aura_active(AURA_WYVERN_READY)
108 || ctx.is_aura_active(AURA_BOAR_READY)
109 || ctx.is_aura_active(AURA_BEAR_READY);
110
111 if ready_up {
112 return;
113 }
114
115 apply_next_howl_ready(
116 ctx,
117 wowlab_engine_combat::generated_identities::proc_counter::BEAST_MASTERY_HOWL,
118 [AURA_WYVERN_READY, AURA_BOAR_READY, AURA_BEAR_READY],
119 );
120 ctx.apply_aura(AURA_HOWL_COOLDOWN);
121}
122
123fn temporary_pet_swings(ctx: &HookCtx<'_>, duration_ms: u32, swing_ms: u32) -> f64 {
128 let hasted_swing_ms = f64::from(swing_ms) / (1.0 + ctx.buf().player().haste / HUNDRED);
129
130 if hasted_swing_ms <= 0.0 {
131 return 1.0;
132 }
133
134 1.0 + (f64::from(duration_ms) / hasted_swing_ms).floor()
135}
136
137fn summon_dire_beast(ctx: &mut HookCtx<'_>) {
138 ctx.apply_aura(AURA_DIRE_BEAST_ACTIVE);
139
140 let duration_ms = ctx.action_aura_duration_ms(
141 crate::generated::specs::beast_mastery_hunter::DAMAGE_AURA::DIRE_BEAST_ACTIVE,
142 );
143 let melee = DIRE_BEAST_AP
145 * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS)
146 * temporary_pet_swings(ctx, duration_ms, PET_SWING_MS)
147 * pet_mult(ctx);
148
149 ctx.deal_physical_pet_damage_ap(
150 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::MELEE,
151 melee,
152 );
153 trigger_huntmasters_call(ctx);
154}
155
156pub(crate) fn dire_beast_proc(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
157 summon_dire_beast(ctx);
158}
159
160fn trigger_huntmasters_call(ctx: &mut HookCtx<'_>) {
162 ctx.apply_aura(AURA_HUNTMASTERS_CALL);
163
164 if ctx.aura_stacks(AURA_HUNTMASTERS_CALL) < HUNTMASTERS_CALL_CAP {
165 return;
166 }
167
168 ctx.expire_aura(AURA_HUNTMASTERS_CALL);
169 let mult = pet_mult(ctx);
170 let melee = CALL_PET_AP
171 * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS)
172 * CALL_PET_SWINGS
173 * mult;
174
175 ctx.deal_physical_pet_damage_ap(
176 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::MELEE,
177 melee,
178 );
179
180 if proc_chance(ctx.rng(), FENRYR_CHANCE) {
181 let leap = ctx.effect_ap_coefficient(
182 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::RAVENOUS_LEAP,
183 ) * CALL_PET_AP
184 * RAVENOUS_LEAP_TICKS
185 * mult;
186
187 ctx.deal_physical_pet_damage_ap(
188 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::RAVENOUS_LEAP,
189 leap,
190 );
191 }
192}
193
194fn howl_wyvern(ctx: &mut HookCtx<'_>) {
195 let stacks = ctx.effect_base_points(
196 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::HOWL_WYVERN_STACKS,
197 );
198
199 ctx.apply_aura_n(
200 AURA_WYVERNS_CRY,
201 wowlab_types::numeric::f64_to_i32_saturating_trunc(stacks),
202 );
203}
204
205fn howl_boar(ctx: &mut HookCtx<'_>) {
206 let mult = pet_mult(ctx);
207
208 ctx.deal_damage_ap(
209 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::BOAR_CHARGE,
210 ctx.effect_ap_coefficient(
211 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::BOAR_CHARGE,
212 ) * mult,
213 DamageFlags::PHYSICAL,
214 );
215 ctx.deal_damage_ap(
216 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::BOAR_CHARGE_CLEAVE,
217 ctx.effect_ap_coefficient(
218 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::BOAR_CHARGE_CLEAVE,
219 ) * mult,
220 DamageFlags::PHYSICAL,
221 );
222}
223
224fn howl_bear(ctx: &mut HookCtx<'_>) {
225 ctx.apply_aura(AURA_BEAR_ACTIVE);
226 ctx.apply_aura(AURA_REND_FLESH);
227 let bear_melee = STABLE_PET_AP
228 * wowlab_engine_domain::damage::pet_swing_ap(BEAR_SWING_MS)
229 * BEAR_AUTO_MULT
230 * BEAR_SWINGS
231 * pet_mult(ctx);
232
233 ctx.deal_physical_pet_damage_ap(
234 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::MELEE,
235 bear_melee,
236 );
237
238 for _ in 0..URSINE_FURY_DIRE_BEASTS {
239 summon_dire_beast(ctx);
240 }
241}
242
243fn howl_consumed(ctx: &mut HookCtx<'_>) {
244 let cdr_ms = ctx.effect_base_points(
245 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::PACK_MENTALITY_BARBED_CDR,
246 );
247
248 ctx.reduce_cooldown(
249 SPELL_BARBED_SHOT,
250 wowlab_types::numeric::f64_to_u32_saturating_trunc(cdr_ms),
251 );
252}
253
254pub(crate) fn kill_command_hook(ctx: &mut HookCtx<'_>) {
255 advance_howl_cycle(ctx);
256
257 let howl_ready = ctx.is_aura_active(AURA_WYVERN_READY)
258 || ctx.is_aura_active(AURA_BOAR_READY)
259 || ctx.is_aura_active(AURA_BEAR_READY);
260
261 let mut bonus = ctx.effect_percent(
262 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::ALPHA_PREDATOR_KC_PCT,
263 );
264
265 if howl_ready {
266 bonus += ctx.effect_percent(
267 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::PACK_MENTALITY_KC_PCT,
268 );
269 }
270
271 let kc_coef = ctx.effect_ap_coefficient(
272 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::KC_PET_DAMAGE,
273 ) * STABLE_PET_AP
274 * (1.0 + bonus)
275 * pet_mult(ctx);
276
277 for _ in 0..stable_pets(ctx) {
278 ctx.deal_physical_pet_damage_ap(
279 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::KILL_COMMAND_PET,
280 kc_coef,
281 );
282 }
283
284 let ws_coef = ctx.effect_ap_coefficient(
285 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::WILDSPEAKER_KC,
286 ) * DIRE_BEAST_AP
287 * pet_mult(ctx);
288
289 for _ in 0..ctx.aura_stacks(AURA_DIRE_BEAST_ACTIVE) {
290 ctx.deal_physical_pet_damage_ap(
291 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::WILDSPEAKER_KILL_COMMAND,
292 ws_coef,
293 );
294 }
295
296 consume_howl(
297 ctx,
298 PackLeaderHowl {
299 wyvern_ready: AURA_WYVERN_READY,
300 boar_ready: AURA_BOAR_READY,
301 bear_ready: AURA_BEAR_READY,
302 on_wyvern: howl_wyvern,
303 on_boar: howl_boar,
304 on_bear: howl_bear,
305 on_consumed: howl_consumed,
306 consume_all: false,
307 },
308 );
309
310 let dire_command_chance = ctx.effect_percent(
311 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::DIRE_COMMAND_PCT,
312 );
313
314 if proc_chance(ctx.rng(), dire_command_chance) {
315 summon_dire_beast(ctx);
316 }
317
318 ctx.expire_aura(AURA_NATURES_ALLY_KILL_COMMAND);
319}
320
321pub(crate) fn barbed_shot_hook(ctx: &mut HookCtx<'_>) {
322 ctx.apply_aura(AURA_BARBED_SHOT_REGEN);
323 ctx.apply_aura(AURA_NATURES_ALLY_KILL_COMMAND);
324 let pack_tactics_focus = ctx.effect_base_points(
325 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::PACK_TACTICS_FOCUS,
326 );
327
328 ctx.gain_resource(pack_tactics_focus);
329 let stomp = ctx.effect_ap_coefficient(
330 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::STOMP_DAMAGE,
331 ) * STABLE_PET_AP
332 * pet_mult(ctx);
333
334 let cfg = ctx
335 .spec_config::<BeastMasteryConfig>()
336 .copied()
337 .unwrap_or_default();
338
339 for _ in 0..stable_pets(ctx) {
340 trigger_stomp(ctx, stomp, cfg.mid2_cobra_fang);
341
342 if cfg.mid2_extra_stomp_mult > 0.0 {
343 trigger_stomp(ctx, stomp * cfg.mid2_extra_stomp_mult, cfg.mid2_cobra_fang);
344 }
345 }
346
347 ctx.reduce_cooldown(SPELL_KILL_COMMAND, WAR_ORDERS_KC_CDR_MS);
348}
349
350pub(crate) fn bestial_wrath_hook(ctx: &mut HookCtx<'_>) {
351 ctx.apply_aura(AURA_NATURES_ALLY);
352
353 if ctx.talent_selected(
354 crate::generated::specs::beast_mastery_hunter::DAMAGE_TALENT::BLOODY_FRENZY,
355 ) {
356 ctx.apply_aura(AURA_BLOODY_FRENZY);
357 }
358
359 let mult = pet_mult(ctx);
360 let bw_hit = ctx.effect_ap_coefficient(
361 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::PET_BW_DAMAGE,
362 ) * STABLE_PET_AP
363 * mult;
364 let hooves = ctx.effect_ap_coefficient(
365 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::STOMP_DAMAGE,
366 ) * (1.0
367 + ctx.effect_percent(
368 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::THUNDERING_HOOVES_PCT,
369 ))
370 * STABLE_PET_AP
371 * mult;
372
373 for _ in 0..stable_pets(ctx) {
374 ctx.deal_physical_pet_damage_ap(
375 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::PET_BESTIAL_WRATH,
376 bw_hit,
377 );
378
379 let cobra_fang = ctx
380 .spec_config::<BeastMasteryConfig>()
381 .is_some_and(|cfg| cfg.mid2_cobra_fang);
382
383 trigger_stomp(ctx, hooves, cobra_fang);
384 }
385
386 ctx.apply_aura(AURA_BLOODSHED_DOT);
387 ctx.grant_charges(SPELL_BARBED_SHOT, 1);
388
389 ctx.deal_effect_damage_ap_with_geometry(
391 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::STAMPEDE_DAMAGE,
392 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::STAMPEDE_GEOMETRY,
393 STAMPEDE_HITS,
394 DamageFlags::PHYSICAL,
395 );
396}
397
398pub(crate) fn wild_thrash_hook(ctx: &mut HookCtx<'_>) {
400 let target_count = ctx.with_resolved_spell_effect_targets(
401 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::WILD_THRASH_DAMAGE,
402 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::WILD_THRASH_DAMAGE.action(),
403 None,
404 |_ctx, targets| targets.len(),
405 );
406 let damage_mult = wild_thrash_target_multiplier(
407 target_count,
408 ctx.effect_base_points(
409 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::WILD_THRASH_THRESHOLD,
410 ),
411 ctx.effect_percent(
412 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::WILD_THRASH_BONUS,
413 ),
414 );
415
416 for _ in 0..stable_pets(ctx) {
417 ctx.deal_effect_damage_ap(
418 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::WILD_THRASH_DAMAGE,
419 STABLE_PET_AP * pet_mult(ctx) * damage_mult,
420 DamageFlags::PHYSICAL | DamageFlags::PET,
421 );
422 }
423
424 let duration_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(ctx.effect_base_points(
425 crate::generated::specs::beast_mastery_hunter::DAMAGE_EFFECT::BEAST_CLEAVE_DURATION,
426 ));
427
428 ctx.apply_aura_with_duration(AURA_BEAST_CLEAVE, duration_ms);
429}
430
431fn wild_thrash_target_multiplier(target_count: usize, threshold: f64, bonus: f64) -> f64 {
432 let target_count = f64::from(u32::try_from(target_count).unwrap_or(u32::MAX));
433
434 if target_count >= threshold {
435 1.0 + bonus
436 } else {
437 1.0
438 }
439}
440
441pub(crate) fn cobra_shot_hook(ctx: &mut HookCtx<'_>) {
442 const BARBED_SCALES_CDR_MS: u32 = 2_000;
443 ctx.reduce_cooldown(SPELL_BARBED_SHOT, BARBED_SCALES_CDR_MS);
444 ctx.apply_aura(AURA_NATURES_ALLY_KILL_COMMAND);
445
446 let cfg = ctx
447 .spec_config::<BeastMasteryConfig>()
448 .copied()
449 .unwrap_or_default();
450 let coefficient = ctx.effect_ap_coefficient(MID2_DAMAGE_EFFECT::COBRA_SHOT_DAMAGE);
451 let stacks = ctx.aura_stacks(AURA_COBRA_FANG);
452
453 ctx.deal_damage_ap(
454 MID2_DAMAGE_EFFECT::COBRA_SHOT_DAMAGE.action(),
455 if stacks > 0 && !ctx.is_aura_active(AURA_BEAST_CLEAVE) {
456 coefficient * (1.0 + f64::from(stacks) * cfg.cobra_fang_primary_per_stack)
457 } else {
458 coefficient
459 },
460 DamageFlags::empty(),
461 );
462
463 if stacks <= 0 {
464 return;
465 }
466
467 let stacks = f64::from(stacks);
468
469 if ctx.is_aura_active(AURA_BEAST_CLEAVE) {
470 let primary = ctx.target();
471
472 ctx.with_resolved_spell_effect_targets(
473 MID2_DAMAGE_EFFECT::COBRA_CLEAVE_GEOMETRY,
474 MID2_DAMAGE_REPORTED::COBRA_CLEAVE,
475 None,
476 |ctx, targets| {
477 for target in targets
478 .iter()
479 .copied()
480 .filter(|target| Some(*target) != primary)
481 {
482 ctx.deal_damage_ap_to_target(
483 target,
484 MID2_DAMAGE_REPORTED::COBRA_CLEAVE,
485 coefficient * stacks * cfg.cobra_fang_cleave_per_stack,
486 DamageFlags::empty(),
487 );
488 }
489 },
490 );
491 }
492
493 ctx.expire_aura(AURA_COBRA_FANG);
494}
495
496fn trigger_stomp(ctx: &mut HookCtx<'_>, coefficient: f64, cobra_fang: bool) {
497 ctx.deal_physical_pet_damage_ap(
498 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::STOMP,
499 coefficient,
500 );
501
502 if cobra_fang {
503 ctx.apply_aura(AURA_COBRA_FANG);
504 }
505}
506
507const CLAW_COEF: f64 = 0.75;
509const CLAWS_PER_SWING: f64 = 0.88;
510
511pub(crate) fn pet_swing(ctx: &mut HookCtx<'_>, event: SwingEvent) {
512 let hand = event.hand;
513
514 if hand == SwingHand::OffHand {
515 return;
516 }
517
518 let mult = pet_mult(ctx);
519 let extra_pets = stable_pets(ctx) - 1;
520 let swing = STABLE_PET_AP
521 * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS)
522 * mult
523 * wowlab_types::numeric::usize_to_f64(extra_pets);
524
525 ctx.deal_physical_pet_damage_ap(
526 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::MELEE,
527 swing,
528 );
529
530 let claw = STABLE_PET_AP
531 * CLAW_COEF
532 * CLAWS_PER_SWING
533 * mult
534 * wowlab_types::numeric::usize_to_f64(stable_pets(ctx));
535
536 ctx.deal_physical_pet_damage_ap(
537 crate::generated::specs::beast_mastery_hunter::DAMAGE_SPELL::CLAW,
538 claw,
539 );
540}
541
542#[cfg(test)]
543mod tests {
544 use googletest::prelude::*;
545
546 use super::wild_thrash_target_multiplier;
547
548 #[gtest]
549 #[rstest::rstest]
550 #[case::below_threshold(1, 2.0, 2.0, 1.0)]
551 #[case::at_threshold(2, 2.0, 2.0, 3.0)]
552 #[case::above_threshold(5, 2.0, 2.0, 3.0)]
553 fn wild_thrash_bonus_uses_resolved_target_count(
554 #[case] target_count: usize,
555 #[case] threshold: f64,
556 #[case] bonus: f64,
557 #[case] expected: f64,
558 ) -> Result<()> {
559 verify_that!(
560 wild_thrash_target_multiplier(target_count, threshold, bonus),
561 eq(expected)
562 )
563 }
564}