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