Skip to main content

wowlab_engine_content/hooks/survival_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::ProcOps as _;
5use wowlab_engine_combat::AuraOps as _;
6use wowlab_engine_combat::SchedulingOps as _;
7use wowlab_engine_combat::{DamageFlags, HookCtx, MasteryCtx, SwingEvent, SwingHand};
8use wowlab_engine_rng::proc_chance;
9use wowlab_types::{constants::HUNDRED};
10
11use crate::generated::specs::survival_hunter::{
12    AURA_BEAR_READY, AURA_BOAR_READY, AURA_HOWL_COOLDOWN, AURA_MONGOOSE_FURY,
13    AURA_RAPTOR_SWIPE, AURA_REND_FLESH, AURA_SIC_EM,
14    AURA_TIP_OF_THE_SPEAR, AURA_WYVERN_READY, AURA_WYVERNS_CRY, EFFECT, SPELL_BOOMSTICK,
15    SPELL_CLAW, SPELL_WILDFIRE_BOMB,
16};
17#[cfg(test)]
18use crate::generated::specs::survival_hunter::{AURA, SPELL};
19use crate::hooks::shared::hunter::{PackLeaderHowl, apply_next_howl_ready, consume_howl};
20
21use super::config::SurvivalConfig;
22
23/// Main-pet attack power is 60% of owner attack power.
24const PET_AP_FROM_OWNER: f64 = 0.6;
25/// Boomstick (1261193): 3s channel, 1s ticks, Extra Initial Period (tick zero) = 4 ticks.
26const BOOMSTICK_TICKS: f64 = 4.0;
27const BOOMSTICK_TICK_INTERVAL_MS: u32 = 1_000;
28const FLAMEFANG_AOE_TICKS: f64 = 4.0;
29const WILDFIRE_BOMB_DOT_TICKS: f64 = 6.0;
30const WILDFIRE_INFUSION_CDR_MS: u32 = 1_000;
31const WILDFIRE_SHELLS_CDR_MS: u32 = 2_000;
32const LETHAL_CALIBRATION_CDR_MS: u32 = 2_000;
33const PACK_MENTALITY_WILDFIRE_CDR_MS: u32 = 6_000;
34/// Claw (16827) effect#1 AP coefficient 0.75 (pet AP).
35const CLAW_COEF: f64 = 0.75;
36/// Stampede spell 1258344 produces nine hits.
37const STAMPEDE_HITS: f64 = 9.0;
38const SHRAPNEL_BOMB_FOCUS: f64 = 15.0;
39/// Wildfire Bomb (259495) effect#3 = 80: +80% on the primary target, explosion and `DoT`.
40const WILDFIRE_BOMB_PRIMARY_TARGET_MULT: f64 = 1.8;
41const TWIN_FANGS_TIP_STACKS: i32 = 3;
42/// Takedown buff (1250646) e3: +20% pet damage stacking with the aura's generic e1 +20%.
43const TAKEDOWN_PET_DAMAGE_MULT: f64 = 1.2;
44
45/// `spirit_bond_buff` (459722) effectN(1) = 100%.
46const SPIRIT_BOND_BUFF_BONUS: f64 = 1.0;
47
48/// Spirit Bond (263135).
49#[must_use]
50pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
51    1.0 + ctx.points(EFFECT::SPIRIT_BOND_DIRECT.1) / HUNDRED * (1.0 + SPIRIT_BOND_BUFF_BONUS)
52}
53
54fn consume_tip(ctx: &mut HookCtx<'_>) -> f64 {
55    if !ctx.consume_aura_stack(AURA_TIP_OF_THE_SPEAR) {
56        return 1.0;
57    }
58
59    trigger_strike_as_one_with_cooldown(ctx, 1.0);
60
61    1.0 + ctx.effect_percent(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::TIP_OF_THE_SPEAR_BONUS)
62}
63
64fn trigger_strike_as_one(ctx: &mut HookCtx<'_>, mult: f64) {
65    ctx.deal_physical_pet_damage_ap(
66        crate::generated::specs::survival_hunter::DAMAGE_SPELL::STRIKE_AS_ONE,
67        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::STRIKE_AS_ONE_DAMAGE) * mult * PET_AP_FROM_OWNER,
68    );
69}
70
71fn trigger_strike_as_one_with_cooldown(ctx: &mut HookCtx<'_>, mult: f64) {
72    if !ctx.proc_category_ready(wowlab_engine_combat::generated_identities::proc_category::STRIKE_AS_ONE) {
73        return;
74    }
75
76    ctx.start_proc_category_cooldown(wowlab_engine_combat::generated_identities::proc_category::STRIKE_AS_ONE);
77    trigger_strike_as_one(ctx, mult);
78}
79
80fn trigger_howl(ctx: &mut HookCtx<'_>) {
81    apply_next_howl_ready(
82        ctx,
83        wowlab_engine_combat::generated_identities::proc_counter::SURVIVAL_HOWL,
84        [AURA_WYVERN_READY, AURA_BOAR_READY, AURA_BEAR_READY],
85    );
86}
87
88pub(crate) fn howl_cooldown_expire_hook(ctx: &mut HookCtx<'_>) {
89    trigger_howl(ctx);
90}
91
92fn start_howl_cooldown(ctx: &mut HookCtx<'_>) {
93    if !ctx.is_aura_active(AURA_HOWL_COOLDOWN) {
94        ctx.apply_aura(AURA_HOWL_COOLDOWN);
95    }
96}
97
98fn howl_wyvern(ctx: &mut HookCtx<'_>) {
99    let stacks = wowlab_types::numeric::f64_to_i32_saturating_trunc(
100        ctx.effect_base_points(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::HOWL_WYVERN_STACKS),
101    );
102
103    ctx.apply_aura_n(AURA_WYVERNS_CRY, stacks);
104}
105
106fn howl_boar(ctx: &mut HookCtx<'_>) {
107    ctx.deal_damage_ap(
108        crate::generated::specs::survival_hunter::DAMAGE_SPELL::BOAR_CHARGE,
109        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::BOAR_CHARGE),
110        DamageFlags::PHYSICAL,
111    );
112    ctx.deal_damage_ap(
113        crate::generated::specs::survival_hunter::DAMAGE_SPELL::BOAR_CHARGE_CLEAVE,
114        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::BOAR_CHARGE_CLEAVE),
115        DamageFlags::PHYSICAL,
116    );
117}
118
119fn howl_bear(ctx: &mut HookCtx<'_>) {
120    ctx.apply_aura(AURA_REND_FLESH);
121    ctx.deal_damage_ap(
122        crate::generated::specs::survival_hunter::DAMAGE_SPELL::STAMPEDE_TICK,
123        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::STAMPEDE_DAMAGE) * STAMPEDE_HITS,
124        DamageFlags::PHYSICAL,
125    );
126}
127
128fn howl_consumed(ctx: &mut HookCtx<'_>) {
129    ctx.reduce_cooldown(SPELL_WILDFIRE_BOMB, PACK_MENTALITY_WILDFIRE_CDR_MS);
130    start_howl_cooldown(ctx);
131}
132
133pub(crate) fn kill_command_hook(ctx: &mut HookCtx<'_>) {
134    consume_howl(
135        ctx,
136        PackLeaderHowl {
137            wyvern_ready: AURA_WYVERN_READY,
138            boar_ready: AURA_BOAR_READY,
139            bear_ready: AURA_BEAR_READY,
140            on_wyvern: howl_wyvern,
141            on_boar: howl_boar,
142            on_bear: howl_bear,
143            on_consumed: howl_consumed,
144            consume_all: true,
145        },
146    );
147    ctx.deal_physical_pet_damage_ap(
148        crate::generated::specs::survival_hunter::DAMAGE_SPELL::KILL_COMMAND,
149        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::KILL_COMMAND_PET_DAMAGE) * PET_AP_FROM_OWNER,
150    );
151    ctx.apply_aura(AURA_TIP_OF_THE_SPEAR);
152    ctx.reduce_cooldown(SPELL_WILDFIRE_BOMB, WILDFIRE_INFUSION_CDR_MS);
153}
154
155pub(crate) fn wildfire_bomb_hook(ctx: &mut HookCtx<'_>) {
156    let tip_mult = consume_tip(ctx);
157
158    ctx.deal_damage_ap(
159        crate::generated::specs::survival_hunter::DAMAGE_SPELL::WILDFIRE_BOMB_DAMAGE,
160        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::WILDFIRE_BOMB_DAMAGE)
161            * tip_mult
162            * WILDFIRE_BOMB_PRIMARY_TARGET_MULT,
163        DamageFlags::empty(),
164    );
165    ctx.gain_resource(SHRAPNEL_BOMB_FOCUS);
166    ctx.reduce_cooldown(SPELL_BOOMSTICK, LETHAL_CALIBRATION_CDR_MS);
167    // Bleed (1253171): bleeds bypass armor, so fold the DoT as non-physical to skip the armor step.
168    let hasted_ticks =
169        WILDFIRE_BOMB_DOT_TICKS
170            * wowlab_engine_domain::stats::haste_multiplier(&ctx.program().base_stats().stats);
171
172    ctx.deal_damage_ap(
173        crate::generated::specs::survival_hunter::DAMAGE_SPELL::WILDFIRE_BOMB_BLEED,
174        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::WILDFIRE_BOMB_DOT)
175            * hasted_ticks
176            * WILDFIRE_BOMB_PRIMARY_TARGET_MULT,
177        DamageFlags::empty(),
178    );
179}
180
181pub(crate) fn raptor_strike_hook(ctx: &mut HookCtx<'_>) {
182    if ctx.consume_aura(AURA_RAPTOR_SWIPE) {
183        if ctx.is_aura_active(AURA_TIP_OF_THE_SPEAR) {
184            trigger_strike_as_one(ctx, ctx.effect_percent(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::RAPTOR_SWIPE_STRIKE_AS_ONE));
185        }
186
187        let tip_mult = consume_tip(ctx);
188        let primary_mult = 1.0
189            + ctx.effect_percent(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::RAPTOR_SWIPE_TWO_PRIMARY)
190                * f64::from(ctx.talent_rank(
191                    crate::generated::specs::survival_hunter::DAMAGE_TALENT::RAPTOR_SWIPE_2,
192                ));
193
194        ctx.deal_damage_ap(
195            crate::generated::specs::survival_hunter::DAMAGE_SPELL::RAPTOR_SWIPE,
196            ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::RAPTOR_SWIPE_DAMAGE) * primary_mult * tip_mult,
197            DamageFlags::PHYSICAL,
198        );
199
200        return;
201    }
202
203    let tip_mult = consume_tip(ctx);
204
205    ctx.deal_damage_ap(
206        crate::generated::specs::survival_hunter::DAMAGE_SPELL::RAPTOR_STRIKE,
207        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::RAPTOR_STRIKE_DAMAGE) * tip_mult,
208        DamageFlags::PHYSICAL,
209    );
210
211    let chance = ctx.effect_percent(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::RAPTOR_SWIPE_CHANCE);
212
213    if proc_chance(ctx.rng(), chance) {
214        ctx.apply_aura(AURA_RAPTOR_SWIPE);
215    }
216}
217
218pub(crate) fn takedown_hook(ctx: &mut HookCtx<'_>) {
219    ctx.apply_aura_n(AURA_TIP_OF_THE_SPEAR, TWIN_FANGS_TIP_STACKS);
220    trigger_howl(ctx);
221    let tip_mult = consume_tip(ctx);
222
223    ctx.gain_resource(ctx.effect_base_points(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::TAKEDOWN_FOCUS));
224    ctx.deal_damage_ap(
225        crate::generated::specs::survival_hunter::DAMAGE_SPELL::TAKEDOWN_DAMAGE,
226        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::TAKEDOWN_DAMAGE) * tip_mult,
227        DamageFlags::PHYSICAL,
228    );
229    ctx.deal_physical_pet_damage_ap(
230        crate::generated::specs::survival_hunter::DAMAGE_SPELL::TAKEDOWN_PET,
231        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::TAKEDOWN_PET_DAMAGE)
232            * tip_mult
233            * TAKEDOWN_PET_DAMAGE_MULT
234            * PET_AP_FROM_OWNER,
235    );
236}
237
238pub(crate) fn boomstick_hook(ctx: &mut HookCtx<'_>) {
239    let tip_mult = consume_tip(ctx);
240
241    ctx.deal_damage_ap(
242        crate::generated::specs::survival_hunter::DAMAGE_SPELL::BOOMSTICK_DAMAGE,
243        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::BOOMSTICK_DAMAGE) * BOOMSTICK_TICKS * tip_mult,
244        DamageFlags::PHYSICAL,
245    );
246    ctx.reduce_cooldown(
247        SPELL_WILDFIRE_BOMB,
248        WILDFIRE_SHELLS_CDR_MS
249            * wowlab_types::numeric::f64_to_u32_saturating_trunc(BOOMSTICK_TICKS),
250    );
251
252    if ctx
253        .spec_config::<SurvivalConfig>()
254        .is_some_and(|cfg| cfg.mid2_4pc)
255    {
256        for tick in 0..wowlab_types::numeric::f64_to_u32_saturating_trunc(BOOMSTICK_TICKS) {
257            ctx.schedule_hook(
258                tick.saturating_mul(BOOMSTICK_TICK_INTERVAL_MS),
259                mid2_extend_mongoose_fury,
260            );
261        }
262    }
263}
264
265fn mid2_extend_mongoose_fury(ctx: &mut HookCtx<'_>) {
266    if ctx.is_aura_active(AURA_MONGOOSE_FURY) {
267        let extension_ms = ctx.effect_base_points(
268            crate::generated::specs::survival_hunter::DAMAGE_EFFECT::MID2_SURVIVAL_4PC_EXTEND_MS,
269        );
270
271        ctx.extend_aura(
272            AURA_MONGOOSE_FURY,
273            wowlab_types::numeric::f64_to_u32_saturating_trunc(extension_ms),
274        );
275    }
276}
277
278pub(crate) fn flamefang_pitch_hook(ctx: &mut HookCtx<'_>) {
279    ctx.deal_damage_ap(
280        crate::generated::specs::survival_hunter::DAMAGE_SPELL::FLAMEFANG_PITCH_DAMAGE,
281        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::FLAMEFANG_PITCH_DAMAGE),
282        DamageFlags::PHYSICAL,
283    );
284    ctx.deal_damage_ap(
285        crate::generated::specs::survival_hunter::DAMAGE_SPELL::FLAMEFANG_PITCH_AOE,
286        ctx.effect_ap_coefficient(crate::generated::specs::survival_hunter::DAMAGE_EFFECT::FLAMEFANG_PITCH_AOE) * FLAMEFANG_AOE_TICKS,
287        DamageFlags::PHYSICAL,
288    );
289    ctx.reset_cooldown(SPELL_WILDFIRE_BOMB);
290}
291
292pub(crate) fn pet_swing(ctx: &mut HookCtx<'_>, event: SwingEvent) {
293    let hand = event.hand;
294    let is_crit = event.is_crit;
295
296    if hand == SwingHand::OffHand {
297        return;
298    }
299
300    if ctx.spell_ready(SPELL_CLAW) {
301        ctx.start_cooldown(SPELL_CLAW);
302        ctx.deal_physical_pet_damage_ap(crate::generated::specs::survival_hunter::DAMAGE_SPELL::CLAW, CLAW_COEF * PET_AP_FROM_OWNER);
303    }
304
305    if is_crit {
306        ctx.apply_aura(AURA_SIC_EM);
307    }
308}
309
310#[cfg(test)]
311#[path = "hooks/tests.rs"]
312mod tests;