wowlab_engine_content/hooks/beast_mastery_hunter/
hooks.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::ResourceOps as _;
4use wowlab_engine_combat::AuraOps as _;
5use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
6use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
7
8use crate::generated::specs::beast_mastery_hunter::{
9 AURA, AURA_BARBED_SHOT_REGEN, AURA_BEAR_ACTIVE, AURA_BEAR_READY, AURA_BEAST_CLEAVE,
10 AURA_BLOODSHED_DOT, AURA_BLOODY_FRENZY, AURA_BOAR_READY, AURA_DIRE_BEAST_ACTIVE,
11 AURA_HOWL_COOLDOWN, AURA_HUNTMASTERS_CALL, AURA_NATURES_ALLY, AURA_NATURES_ALLY_KILL_COMMAND,
12 AURA_REND_FLESH, AURA_WYVERN_READY, AURA_WYVERNS_CRY, EFFECT, HERO, SPELL_BARBED_SHOT,
13 SPELL_KILL_COMMAND, TALENT,
14};
15use crate::hooks::shared::hunter::{PackLeaderHowl, apply_next_howl_ready, consume_howl};
16use wowlab_engine_combat::{DamageFlags, HookCtx, ImpactEvent, MasteryCtx, SwingEvent, SwingHand};
17use wowlab_engine_rng::proc_chance;
18
19const STABLE_PET_AP: f64 = 0.6;
21
22const DIRE_BEAST_AP: f64 = 1.0;
24
25const CALL_PET_AP: f64 = 2.0;
27const PET_SWING_MS: u32 = 2_000;
28const BEAR_SWING_MS: u32 = 1_500;
29
30const PERMANENT_PETS: usize = 2;
32
33const WAR_ORDERS_KC_CDR_MS: u32 = 3_000;
35
36const CALL_PET_SWINGS: f64 = 5.0;
38
39const RAVENOUS_LEAP_TICKS: f64 = 8.0;
41
42const HUNTMASTERS_CALL_CAP: i32 = 3;
44
45const FENRYR_CHANCE: f64 = 0.5;
47
48const STAMPEDE_HITS: f64 = 24.0;
50
51const BEAR_SWINGS: f64 = 6.7;
53const BEAR_AUTO_MULT: f64 = 7.0;
54
55const URSINE_FURY_DIRE_BEASTS: i64 = 2;
57
58const MASTERY_SHOTS_DIRECT: u8 = 5;
60const MASTERY_SHOTS_PERIODIC: u8 = 6;
61
62#[must_use]
64pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
65 if ctx.is_pet
66 || ctx.spell_id == HERO::BM_PETS::SPELL::BLOODSHED
67 || ctx.spell_id == AURA::REND_FLESH
68 {
69 return ctx.bonus(1);
70 }
71
72 if ctx.is_periodic {
77 ctx.affected_spell_bonus(MASTERY_SHOTS_PERIODIC)
78 } else {
79 ctx.affected_spell_bonus(MASTERY_SHOTS_DIRECT)
80 }
81}
82
83fn pet_mult(ctx: &HookCtx<'_>) -> f64 {
84 let cry = f64::from(ctx.aura_stacks(AURA_WYVERNS_CRY.raw()));
85
86 1.0 + cry * ctx.effect_percent(EFFECT::WYVERNS_CRY_PCT)
87}
88
89fn stable_pets(ctx: &HookCtx<'_>) -> usize {
90 if ctx.is_aura_active(AURA_NATURES_ALLY.raw()) {
91 PERMANENT_PETS + 1
92 } else {
93 PERMANENT_PETS
94 }
95}
96
97fn advance_howl_cycle(ctx: &mut HookCtx<'_>) {
98 if ctx.is_aura_active(AURA_HOWL_COOLDOWN.raw()) {
99 return;
100 }
101
102 let ready_up = ctx.is_aura_active(AURA_WYVERN_READY.raw())
103 || ctx.is_aura_active(AURA_BOAR_READY.raw())
104 || ctx.is_aura_active(AURA_BEAR_READY.raw());
105
106 if ready_up {
107 return;
108 }
109
110 apply_next_howl_ready(
111 ctx,
112 AURA::HOWL_COOLDOWN,
113 [AURA_WYVERN_READY, AURA_BOAR_READY, AURA_BEAR_READY],
114 );
115 ctx.apply_aura(AURA_HOWL_COOLDOWN.raw());
116}
117
118fn temporary_pet_swings(ctx: &HookCtx<'_>, duration_ms: u32, swing_ms: u32) -> f64 {
125 let hasted_swing_ms = f64::from(swing_ms) / (1.0 + ctx.buf().player().haste / HUNDRED);
126
127 if hasted_swing_ms <= 0.0 {
128 return 1.0;
129 }
130
131 1.0 + (f64::from(duration_ms) / hasted_swing_ms).floor()
132}
133
134fn summon_dire_beast(ctx: &mut HookCtx<'_>) {
135 ctx.apply_aura(AURA_DIRE_BEAST_ACTIVE.raw());
136
137 let duration_ms = ctx
138 .game_data()
139 .aura_duration_ms(SpellIdx::from_raw(AURA::DIRE_BEAST_ACTIVE))
140 .unwrap_or_default();
141 let melee = DIRE_BEAST_AP
143 * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS)
144 * temporary_pet_swings(ctx, duration_ms, PET_SWING_MS)
145 * pet_mult(ctx);
146
147 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::MELEE, melee);
148 trigger_huntmasters_call(ctx);
149}
150
151pub(crate) fn dire_beast_proc(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
152 summon_dire_beast(ctx);
153}
154
155fn trigger_huntmasters_call(ctx: &mut HookCtx<'_>) {
157 ctx.apply_aura(AURA_HUNTMASTERS_CALL.raw());
158
159 if ctx.aura_stacks(AURA_HUNTMASTERS_CALL.raw()) < HUNTMASTERS_CALL_CAP {
160 return;
161 }
162
163 ctx.expire_aura(AURA_HUNTMASTERS_CALL.raw());
164 let mult = pet_mult(ctx);
165 let melee =
166 CALL_PET_AP
167 * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS)
168 * CALL_PET_SWINGS
169 * mult;
170
171 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::MELEE, melee);
172
173 if proc_chance(ctx.rng(), FENRYR_CHANCE) {
174 let leap = ctx.effect_ap_coefficient(EFFECT::RAVENOUS_LEAP)
175 * CALL_PET_AP
176 * RAVENOUS_LEAP_TICKS
177 * mult;
178
179 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::RAVENOUS_LEAP, leap);
180 }
181}
182
183fn howl_wyvern(ctx: &mut HookCtx<'_>) {
184 let stacks = ctx
185 .game_data()
186 .effect_base_points(EFFECT::HOWL_WYVERN_STACKS);
187
188 ctx.apply_aura_n(
189 AURA_WYVERNS_CRY.raw(),
190 wowlab_types::numeric::f64_to_i32_saturating_trunc(stacks),
191 );
192}
193
194fn howl_boar(ctx: &mut HookCtx<'_>) {
195 let mult = pet_mult(ctx);
196
197 ctx.deal_damage_ap(
198 HERO::PACK_LEADER::SPELL::BOAR_CHARGE,
199 ctx.effect_ap_coefficient(EFFECT::BOAR_CHARGE) * mult,
200 DamageFlags::PHYSICAL,
201 );
202 ctx.deal_damage_ap(
203 HERO::PACK_LEADER::SPELL::BOAR_CHARGE_CLEAVE,
204 ctx.effect_ap_coefficient(EFFECT::BOAR_CHARGE_CLEAVE) * mult,
205 DamageFlags::PHYSICAL,
206 );
207}
208
209fn howl_bear(ctx: &mut HookCtx<'_>) {
210 ctx.apply_aura(AURA_BEAR_ACTIVE.raw());
211 ctx.apply_aura(AURA_REND_FLESH.raw());
212 let bear_melee = STABLE_PET_AP
213 * wowlab_engine_domain::damage::pet_swing_ap(BEAR_SWING_MS)
214 * BEAR_AUTO_MULT
215 * BEAR_SWINGS
216 * pet_mult(ctx);
217
218 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::MELEE, bear_melee);
219
220 for _ in 0..URSINE_FURY_DIRE_BEASTS {
221 summon_dire_beast(ctx);
222 }
223}
224
225fn howl_consumed(ctx: &mut HookCtx<'_>) {
226 let cdr_ms = ctx
227 .game_data()
228 .effect_base_points(EFFECT::PACK_MENTALITY_BARBED_CDR);
229
230 ctx.reduce_cooldown(
231 SPELL_BARBED_SHOT.raw(),
232 wowlab_types::numeric::f64_to_u32_saturating_trunc(cdr_ms),
233 );
234}
235
236pub(crate) fn kill_command_hook(ctx: &mut HookCtx<'_>) {
237 advance_howl_cycle(ctx);
238
239 let howl_ready = ctx.is_aura_active(AURA_WYVERN_READY.raw())
240 || ctx.is_aura_active(AURA_BOAR_READY.raw())
241 || ctx.is_aura_active(AURA_BEAR_READY.raw());
242
243 let mut bonus = ctx.effect_percent(EFFECT::ALPHA_PREDATOR_KC_PCT);
244
245 if howl_ready {
246 bonus += ctx.effect_percent(EFFECT::PACK_MENTALITY_KC_PCT);
247 }
248
249 let kc_coef = ctx.effect_ap_coefficient(EFFECT::KC_PET_DAMAGE)
250 * STABLE_PET_AP
251 * (1.0 + bonus)
252 * pet_mult(ctx);
253
254 for _ in 0..stable_pets(ctx) {
255 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::KILL_COMMAND_PET, kc_coef);
256 }
257
258 let ws_coef = ctx.effect_ap_coefficient(EFFECT::WILDSPEAKER_KC) * DIRE_BEAST_AP * pet_mult(ctx);
259
260 for _ in 0..ctx.aura_stacks(AURA_DIRE_BEAST_ACTIVE.raw()) {
261 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::WILDSPEAKER_KILL_COMMAND, ws_coef);
262 }
263
264 consume_howl(
265 ctx,
266 PackLeaderHowl {
267 wyvern_ready: AURA_WYVERN_READY,
268 boar_ready: AURA_BOAR_READY,
269 bear_ready: AURA_BEAR_READY,
270 on_wyvern: howl_wyvern,
271 on_boar: howl_boar,
272 on_bear: howl_bear,
273 on_consumed: howl_consumed,
274 consume_all: false,
275 },
276 );
277
278 let dire_command_chance = ctx.effect_percent(EFFECT::DIRE_COMMAND_PCT);
279
280 if proc_chance(ctx.rng(), dire_command_chance) {
281 summon_dire_beast(ctx);
282 }
283
284 ctx.expire_aura(AURA_NATURES_ALLY_KILL_COMMAND.raw());
285}
286
287pub(crate) fn barbed_shot_hook(ctx: &mut HookCtx<'_>) {
288 ctx.apply_aura(AURA_BARBED_SHOT_REGEN.raw());
289 ctx.apply_aura(AURA_NATURES_ALLY_KILL_COMMAND.raw());
290 let pack_tactics_focus = ctx
291 .game_data()
292 .effect_base_points(EFFECT::PACK_TACTICS_FOCUS);
293
294 ctx.gain_resource(pack_tactics_focus);
295 let stomp = ctx.effect_ap_coefficient(EFFECT::STOMP_DAMAGE) * STABLE_PET_AP * pet_mult(ctx);
296
297 for _ in 0..stable_pets(ctx) {
298 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::STOMP, stomp);
299 }
300
301 ctx.reduce_cooldown(SPELL_KILL_COMMAND.raw(), WAR_ORDERS_KC_CDR_MS);
302}
303
304pub(crate) fn bestial_wrath_hook(ctx: &mut HookCtx<'_>) {
305 ctx.apply_aura(AURA_NATURES_ALLY.raw());
306
307 if ctx.talent_selected(TALENT::BLOODY_FRENZY) {
308 ctx.apply_aura(AURA_BLOODY_FRENZY.raw());
309 }
310
311 let mult = pet_mult(ctx);
312 let bw_hit = ctx.effect_ap_coefficient(EFFECT::PET_BW_DAMAGE) * STABLE_PET_AP * mult;
313 let hooves = ctx.effect_ap_coefficient(EFFECT::STOMP_DAMAGE)
314 * (1.0 + ctx.effect_percent(EFFECT::THUNDERING_HOOVES_PCT))
315 * STABLE_PET_AP
316 * mult;
317
318 for _ in 0..stable_pets(ctx) {
319 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::PET_BESTIAL_WRATH, bw_hit);
320 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::STOMP, hooves);
321 }
322
323 ctx.apply_aura(AURA_BLOODSHED_DOT.raw());
324 ctx.grant_charges(SPELL_BARBED_SHOT.raw(), 1);
325
326 ctx.deal_effect_damage_ap_with_geometry(
328 EFFECT::STAMPEDE_DAMAGE,
329 EFFECT::STAMPEDE_GEOMETRY,
330 STAMPEDE_HITS,
331 DamageFlags::PHYSICAL,
332 );
333}
334
335pub(crate) fn wild_thrash_hook(ctx: &mut HookCtx<'_>) {
337 let target_count = ctx
338 .resolved_spell_effect_targets(
339 EFFECT::WILD_THRASH_DAMAGE,
340 EFFECT::WILD_THRASH_DAMAGE.0,
341 None,
342 )
343 .len();
344 let damage_mult = wild_thrash_target_multiplier(
345 target_count,
346 ctx.effect_base_points(EFFECT::WILD_THRASH_THRESHOLD),
347 ctx.effect_percent(EFFECT::WILD_THRASH_BONUS),
348 );
349
350 for _ in 0..stable_pets(ctx) {
351 ctx.deal_effect_damage_ap(
352 EFFECT::WILD_THRASH_DAMAGE,
353 STABLE_PET_AP * pet_mult(ctx) * damage_mult,
354 DamageFlags::PHYSICAL | DamageFlags::PET,
355 );
356 }
357
358 let duration_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(
359 ctx.effect_base_points(EFFECT::BEAST_CLEAVE_DURATION),
360 );
361
362 ctx.apply_aura_with_duration(AURA_BEAST_CLEAVE.raw(), duration_ms);
363}
364
365fn wild_thrash_target_multiplier(target_count: usize, threshold: f64, bonus: f64) -> f64 {
366 let target_count = f64::from(u32::try_from(target_count).unwrap_or(u32::MAX));
367
368 if target_count >= threshold {
369 1.0 + bonus
370 } else {
371 1.0
372 }
373}
374
375pub(crate) fn cobra_shot_hook(ctx: &mut HookCtx<'_>) {
376 const BARBED_SCALES_CDR_MS: u32 = 2_000;
377 ctx.reduce_cooldown(SPELL_BARBED_SHOT.raw(), BARBED_SCALES_CDR_MS);
378 ctx.apply_aura(AURA_NATURES_ALLY_KILL_COMMAND.raw());
379}
380
381const CLAW_COEF: f64 = 0.75;
383const CLAWS_PER_SWING: f64 = 0.88;
384
385pub(crate) fn pet_swing(ctx: &mut HookCtx<'_>, event: SwingEvent) {
386 let hand = event.hand;
387
388 if hand == SwingHand::OffHand {
389 return;
390 }
391
392 let mult = pet_mult(ctx);
393 let extra_pets = stable_pets(ctx) - 1;
394 let swing = STABLE_PET_AP
395 * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS)
396 * mult
397 * wowlab_types::numeric::usize_to_f64(extra_pets);
398
399 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::MELEE, swing);
400
401 let claw = STABLE_PET_AP
402 * CLAW_COEF
403 * CLAWS_PER_SWING
404 * mult
405 * wowlab_types::numeric::usize_to_f64(stable_pets(ctx));
406
407 ctx.deal_physical_pet_damage_ap(HERO::BM_PETS::SPELL::CLAW, claw);
408}
409
410#[cfg(test)]
411mod tests {
412 use googletest::prelude::*;
413
414 use super::wild_thrash_target_multiplier;
415
416 #[gtest]
417 #[rstest::rstest]
418 #[case::below_threshold(1, 2.0, 2.0, 1.0)]
419 #[case::at_threshold(2, 2.0, 2.0, 3.0)]
420 #[case::above_threshold(5, 2.0, 2.0, 3.0)]
421 fn wild_thrash_bonus_uses_resolved_target_count(
422 #[case] target_count: usize,
423 #[case] threshold: f64,
424 #[case] bonus: f64,
425 #[case] expected: f64,
426 ) -> Result<()> {
427 verify_that!(
428 wild_thrash_target_multiplier(target_count, threshold, bonus),
429 eq(expected)
430 )
431 }
432}