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::{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
18const PET_AP_FROM_OWNER: f64 = 0.6;
20const 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;
28const CLAW_COEF: f64 = 0.75;
30const STAMPEDE_HITS: f64 = 9.0;
32const SHRAPNEL_BOMB_FOCUS: f64 = 15.0;
33const WILDFIRE_BOMB_PRIMARY_TARGET_MULT: f64 = 1.8;
35const TWIN_FANGS_TIP_STACKS: i32 = 3;
36const TAKEDOWN_PET_DAMAGE_MULT: f64 = 1.2;
38
39const SPIRIT_BOND_BUFF_BONUS: f64 = 1.0;
41
42#[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 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;