Skip to main content

wowlab_engine_content/hooks/beast_mastery_hunter/
hooks.rs

1use 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
19/// Stable pets inherit 0.6 owner AP.
20const STABLE_PET_AP: f64 = 0.6;
21
22/// Dire beasts inherit 1.0 owner AP.
23const DIRE_BEAST_AP: f64 = 1.0;
24
25/// Fenryr and Hati inherit 2.0 owner AP.
26const CALL_PET_AP: f64 = 2.0;
27const PET_SWING_MS: u32 = 2_000;
28const BEAR_SWING_MS: u32 = 1_500;
29
30/// Spell 267116 supplies two permanent pets without a damage penalty.
31const PERMANENT_PETS: usize = 2;
32
33/// War Orders 393933 e3 reduces Kill Command cooldown by three seconds.
34const WAR_ORDERS_KC_CDR_MS: u32 = 3_000;
35
36/// Fenryr/Hati melee swings over their 10s summon.
37const CALL_PET_SWINGS: f64 = 5.0;
38
39/// Fenryr's Ravenous Leap: 8 x 1s ticks (459753).
40const RAVENOUS_LEAP_TICKS: f64 = 8.0;
41
42/// Huntmaster's Call 459731 requires three stacks.
43const HUNTMASTERS_CALL_CAP: i32 = 3;
44
45/// Huntmaster's Call selects Fenryr or Hati with equal probability.
46const FENRYR_CHANCE: f64 = 0.5;
47
48/// Stampede 201594 deals eight ticks of three hits.
49const STAMPEDE_HITS: f64 = 24.0;
50
51/// Bear summons deal ten seconds of 1.5-second swings at a 7x multiplier.
52const BEAR_SWINGS: f64 = 6.7;
53const BEAR_AUTO_MULT: f64 = 7.0;
54
55/// Ursine Fury: bear summon spawns 2 dire beasts (472476 e1).
56const URSINE_FURY_DIRE_BEASTS: i64 = 2;
57
58/// Master of Beasts 76657: the "shots and traps" direct and periodic effects.
59const MASTERY_SHOTS_DIRECT: u8 = 5;
60const MASTERY_SHOTS_PERIODIC: u8 = 6;
61
62/// Returns Master of Beasts mastery damage: 76657 e1 for pets, e5/e6 for the hunter's own shots.
63#[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    // "and the damage of your shots and traps by $s5.1%" — e5/e6 carry their own
73    // affect list at half the pet coefficient, and base points of 0, so the static
74    // fold resolves them to nothing and only the live mastery value supplies them.
75
76    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
118/// Whole melee swings a temporary beast lands over `duration_ms`, counting the one it opens with.
119///
120/// A summon lands one opening swing plus every whole hasted swing period that fits in its window.
121///   `SimC` fires `main_hand_attack->execute()` inside `dire_critter_t::summon` and the pet
122///   despawns when that window ends (`sc_hunter.cpp:1874-1893`).
123///   Guardian autos inherit the owner's haste (`hunter_t::composite_melee_auto_attack_speed`).
124fn 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    // Temporary-pet melee is collapsed to summon time.
142    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
155// Temporary-pet melee and Ravenous Leap are collapsed to summon time.
156fn 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    // Stampede is collapsed to the Bestial Wrath cast.
327    ctx.deal_effect_damage_ap_with_geometry(
328        EFFECT::STAMPEDE_DAMAGE,
329        EFFECT::STAMPEDE_GEOMETRY,
330        STAMPEDE_HITS,
331        DamageFlags::PHYSICAL,
332    );
333}
334
335/// Executes each stable pet's Wild Thrash and enables Beast Cleave.
336pub(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
381/// Claw 16827 uses 0.75 pet AP at 0.88 casts per two-second swing window.
382const 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}