wowlab_engine_content/hooks/survival_hunter/
hooks.rs1use 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
23const PET_AP_FROM_OWNER: f64 = 0.6;
25const 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;
34const CLAW_COEF: f64 = 0.75;
36const STAMPEDE_HITS: f64 = 9.0;
38const SHRAPNEL_BOMB_FOCUS: f64 = 15.0;
39const WILDFIRE_BOMB_PRIMARY_TARGET_MULT: f64 = 1.8;
41const TWIN_FANGS_TIP_STACKS: i32 = 3;
42const TAKEDOWN_PET_DAMAGE_MULT: f64 = 1.2;
44
45const SPIRIT_BOND_BUFF_BONUS: f64 = 1.0;
47
48#[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 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;