wowlab_engine_content/hooks/outlaw_rogue/
hooks.rs1use wowlab_engine_combat::AuraOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::DamageOps as _;
4use wowlab_engine_combat::ResourceOps as _;
5use wowlab_engine_combat::{DamageFlags, HookCtx, SwingEvent, SwingHand};
6use wowlab_engine_rng::proc_chance;
7
8use crate::generated::specs::outlaw_rogue::{
9 AURA_AUDACITY, AURA_BETWEEN_THE_EYES, AURA_BLADE_FLURRY, AURA_DEADLY_PURSUIT_CDR,
10 AURA_FANG_STRIKE, AURA_GRAVEDIGGER, AURA_LOADED_DICE, AURA_OPPORTUNITY, AURA_PALMED_BULLETS,
11 AURA_SLICE_AND_DICE, SPELL_ADRENALINE_RUSH, SPELL_BETWEEN_THE_EYES, SPELL_BLADE_FLURRY,
12 SPELL_BLADE_RUSH, SPELL_KILLING_SPREE,
13};
14use crate::hooks::shared::rogue::{bound_edge_case, bound_hand_of_fate, cut_to_the_chase};
15
16use super::{
17 config::outlaw_config,
18 fatebound::BINDING as FATEBOUND,
19 procs::{
20 apply_opportunity, opportunity_chance, proc_instant_poison, proc_main_gauche,
21 proc_melee_hit,
22 },
23 restless_blades::{on_finisher, on_finisher_with_cp},
24 roll_the_bones::rtb_tier,
25};
26
27pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
28 let hand = event.hand;
29
30 proc_instant_poison(ctx);
31
32 match hand {
33 SwingHand::MainHand => proc_main_gauche(ctx),
34 SwingHand::OffHand => {}
35 }
36}
37
38const SINISTER_STRIKE_EXTRA_CP: f64 = 1.0;
39const RTB_BUILDER_CP_BONUS: f64 = 1.0;
40const RTB_TIER_BONUS_CP: usize = 2;
41
42const KILLING_SPREE_CP_PER_TICK: f64 = 1.0;
43
44pub(crate) fn sinister_strike_hook(ctx: &mut HookCtx<'_>) {
45 proc_melee_hit(ctx);
46
47 if rtb_tier(ctx) >= RTB_TIER_BONUS_CP {
48 ctx.gain_secondary_resource(RTB_BUILDER_CP_BONUS);
49 }
50
51 let chance = opportunity_chance(ctx);
52
53 if proc_chance(ctx.rng(), chance) {
54 apply_opportunity(ctx);
55 ctx.gain_secondary_resource(SINISTER_STRIKE_EXTRA_CP);
56
57 if rtb_tier(ctx) >= RTB_TIER_BONUS_CP {
58 ctx.gain_secondary_resource(RTB_BUILDER_CP_BONUS);
59 }
60
61 let coef = ctx.effect_ap_coefficient(crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::SINISTER_STRIKE_EXTRA_ATTACK_DAMAGE);
62
63 ctx.deal_damage_ap(
64 crate::generated::specs::outlaw_rogue::DAMAGE_REPORTED::SINISTER_STRIKE_EXTRA_ATTACK,
65 coef,
66 DamageFlags::PHYSICAL,
67 );
68 proc_melee_hit(ctx);
69 }
70
71 trigger_fang_strike(ctx);
72}
73
74pub(crate) fn ambush_hook(ctx: &mut HookCtx<'_>) {
75 proc_melee_hit(ctx);
76
77 if rtb_tier(ctx) >= RTB_TIER_BONUS_CP {
78 ctx.gain_secondary_resource(RTB_BUILDER_CP_BONUS);
79 }
80
81 let modifier = outlaw_config(ctx).hidden_opportunity_mod;
82
83 let chance = opportunity_chance(ctx) * modifier;
84
85 if modifier > 0.0 && proc_chance(ctx.rng(), chance) {
86 apply_opportunity(ctx);
87 }
88
89 trigger_fang_strike(ctx);
90}
91
92const PALMED_BULLETS_GRAVEDIGGER_STACKS: i32 = 6;
94const BETWEEN_THE_EYES_MS_PER_POINT: u32 = 2000;
95
96pub(crate) fn dispatch_hook(ctx: &mut HookCtx<'_>) {
97 proc_melee_hit(ctx);
98 let fang_strike = ctx.is_aura_active(AURA_FANG_STRIKE);
99 let cp = if fang_strike {
100 ctx.program().base_stats().secondary_resource_max
101 } else {
102 ctx.last_secondary_spent().max(1.0)
103 };
104
105 if fang_strike {
106 let spent = ctx.last_secondary_spent().max(0.0);
107
108 if spent > 0.0 {
109 ctx.gain_secondary_resource(spent);
110 }
111
112 ctx.consume_aura(AURA_FANG_STRIKE);
113 }
114
115 let dispatch = ctx.effect_ap_coefficient(
116 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::DISPATCH_DAMAGE,
117 );
118
119 ctx.deal_damage_ap(
120 crate::generated::specs::outlaw_rogue::DAMAGE_SPELL::DISPATCH,
121 dispatch * cp,
122 DamageFlags::PHYSICAL,
123 );
124
125 on_finisher_with_cp(ctx, cp);
126 cut_to_the_chase(ctx, AURA_SLICE_AND_DICE, cp);
127 bound_hand_of_fate(ctx, true, FATEBOUND);
128
129 let cfg = outlaw_config(ctx);
130
131 if cfg.scoundrel_cp_threshold > 0.0 && cp >= cfg.scoundrel_cp_threshold {
132 let coef = ctx.effect_ap_coefficient(
133 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::SCOUNDREL_STRIKE_DAMAGE,
134 );
135
136 ctx.deal_damage_ap(
137 crate::generated::specs::outlaw_rogue::DAMAGE_REPORTED::SCOUNDREL_STRIKE,
138 coef,
139 DamageFlags::PHYSICAL,
140 );
141 proc_instant_poison(ctx);
142 }
143
144 if cfg.palmed_bullets_chance_per_cp > 0.0
145 && proc_chance(ctx.rng(), cfg.palmed_bullets_chance_per_cp * cp)
146 {
147 ctx.apply_aura(AURA_PALMED_BULLETS);
148
149 if ctx.aura_stacks(AURA_PALMED_BULLETS) >= PALMED_BULLETS_GRAVEDIGGER_STACKS
150 && !ctx.is_aura_active(AURA_GRAVEDIGGER)
151 {
152 ctx.apply_aura(AURA_GRAVEDIGGER);
153 }
154 }
155}
156
157fn trigger_fang_strike(ctx: &mut HookCtx<'_>) {
158 let chance = outlaw_config(ctx).mid2_fang_chance;
159
160 if chance > 0.0 && proc_chance(ctx.rng(), chance) {
161 ctx.apply_aura(AURA_FANG_STRIKE);
162 }
163}
164
165pub(crate) fn between_the_eyes_hook(ctx: &mut HookCtx<'_>) {
166 proc_melee_hit(ctx);
167 let cp_spent = ctx.last_secondary_spent().max(0.0);
168 let duration_ms = (wowlab_types::numeric::f64_to_u32_saturating_trunc(cp_spent) + 1)
169 * BETWEEN_THE_EYES_MS_PER_POINT;
170
171 ctx.apply_aura_with_duration(AURA_BETWEEN_THE_EYES, duration_ms);
172 on_finisher(ctx);
173 bound_hand_of_fate(ctx, false, FATEBOUND);
174
175 let cfg = outlaw_config(ctx);
176
177 if cfg.ace_up_chance_per_cp > 0.0 && proc_chance(ctx.rng(), cfg.ace_up_chance_per_cp * cp_spent)
178 {
179 ctx.gain_secondary_resource(cfg.ace_up_cp_gain);
180 ctx.reset_cooldown(SPELL_BETWEEN_THE_EYES);
181 }
182
183 if ctx.is_aura_active(AURA_GRAVEDIGGER) {
184 ctx.gain_secondary_resource(cfg.gravedigger_cp);
185 ctx.reset_cooldown(SPELL_BETWEEN_THE_EYES);
186
187 for _ in 0..PALMED_BULLETS_GRAVEDIGGER_STACKS {
188 ctx.consume_aura_stack(AURA_PALMED_BULLETS);
189 }
190
191 ctx.consume_aura(AURA_GRAVEDIGGER);
192 }
193}
194
195pub(crate) fn slice_and_dice_hook(ctx: &mut HookCtx<'_>) {
196 on_finisher(ctx);
197 bound_hand_of_fate(ctx, false, FATEBOUND);
198}
199
200pub(crate) fn blade_flurry_hook(ctx: &mut HookCtx<'_>) {
201 let cfg = outlaw_config(ctx);
202
203 if cfg.deft_maneuvers_cp > 0.0 {
204 let target_count = ctx.with_resolved_spell_effect_targets(
205 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::BLADE_FLURRY_INSTANT_ATTACK_DAMAGE,
206 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::BLADE_FLURRY_INSTANT_ATTACK_DAMAGE.action(),
207 Some(cfg.blade_flurry_targets),
208 |_ctx, targets| targets.len(),
209 );
210 let combo_points = (0..target_count).fold(0.0, |total, _| total + cfg.deft_maneuvers_cp);
211
212 ctx.gain_secondary_resource(combo_points);
213 }
214}
215
216pub(crate) fn killing_spree_hook(ctx: &mut HookCtx<'_>) {
217 let spent = ctx.last_secondary_spent().max(0.0);
218 let max_points = ctx.program().base_stats().secondary_resource_max;
219 let ticks = wowlab_types::numeric::f64_to_u32_saturating_trunc(spent.min(max_points));
220 let damage_mult =
221 1.0 + (spent - max_points).max(0.0) * outlaw_config(ctx).killing_spree_excess_mult;
222 let mh = ctx.effect_ap_coefficient(
223 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::KILLING_SPREE_MH_DAMAGE,
224 );
225 let oh = ctx.effect_ap_coefficient(
226 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::KILLING_SPREE_OH_DAMAGE,
227 );
228
229 for i in 0..ticks {
230 ctx.deal_damage_ap(
231 crate::generated::specs::outlaw_rogue::DAMAGE_REPORTED::KILLING_SPREE_MH,
232 mh * damage_mult,
233 DamageFlags::PHYSICAL,
234 );
235 ctx.deal_damage_ap(
236 crate::generated::specs::outlaw_rogue::DAMAGE_REPORTED::KILLING_SPREE_OH,
237 oh * damage_mult,
238 DamageFlags::PHYSICAL,
239 );
240 proc_main_gauche(ctx);
241
242 if i > 0 {
243 ctx.gain_secondary_resource(KILLING_SPREE_CP_PER_TICK);
244 }
245 }
246
247 on_finisher(ctx);
248 bound_hand_of_fate(ctx, false, FATEBOUND);
249}
250
251pub(crate) fn pistol_shot_hook(ctx: &mut HookCtx<'_>) {
252 let cfg = outlaw_config(ctx);
253 let opportunity_stacks = ctx.aura_stacks(AURA_OPPORTUNITY).max(0);
254
255 proc_instant_poison(ctx);
256
257 if opportunity_stacks == 0 {
258 return;
259 }
260
261 if cfg.quick_draw_cp > 0.0 {
262 ctx.gain_secondary_resource(cfg.quick_draw_cp);
263 }
264
265 let chance = opportunity_chance(ctx);
266
267 if cfg.has_audacity && proc_chance(ctx.rng(), chance) {
268 ctx.apply_aura(AURA_AUDACITY);
269 }
270
271 ctx.consume_aura_stack(AURA_OPPORTUNITY);
272 let remaining = u32::try_from(opportunity_stacks.saturating_sub(1)).unwrap_or_default();
273 let extra_shots = remaining.min(cfg.fan_the_hammer_extra);
274 let coef = ctx.effect_ap_coefficient(
275 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::PISTOL_SHOT_DAMAGE,
276 ) * cfg.fan_the_hammer_mult;
277
278 for _ in 0..extra_shots {
279 ctx.deal_damage_ap(
281 crate::generated::specs::outlaw_rogue::DAMAGE_SPELL::PISTOL_SHOT,
282 coef,
283 DamageFlags::PHYSICAL,
284 );
285 proc_instant_poison(ctx);
286
287 if cfg.quick_draw_cp > 0.0 {
288 ctx.gain_secondary_resource(cfg.quick_draw_cp);
289 }
290
291 ctx.consume_aura_stack(AURA_OPPORTUNITY);
292 }
293}
294
295pub(crate) fn blade_rush_hook(ctx: &mut HookCtx<'_>) {
296 let coef = ctx.effect_ap_coefficient(
297 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::BLADE_RUSH_ATTACK_DAMAGE,
298 );
299 let primary_multiplier = ctx.effect_percent(
300 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::BLADE_RUSH_PRIMARY_MULT,
301 );
302 let secondary_multiplier = if ctx.is_aura_active(AURA_BLADE_FLURRY) {
303 1.0 + ctx.effect_percent(
304 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::BLADE_RUSH_SECONDARY_MULT,
305 )
306 } else {
307 1.0
308 };
309 let primary = ctx.target();
310
311 ctx.with_resolved_spell_effect_targets(
312 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::BLADE_RUSH_ATTACK_DAMAGE,
313 crate::generated::specs::outlaw_rogue::DAMAGE_REPORTED::BLADE_RUSH_ATTACK,
314 None,
315 |ctx, targets| {
316 for target in targets.iter().copied() {
317 let multiplier = if Some(target) == primary {
318 primary_multiplier
319 } else {
320 secondary_multiplier
321 };
322
323 ctx.deal_damage_ap_to_target(
324 target,
325 crate::generated::specs::outlaw_rogue::DAMAGE_REPORTED::BLADE_RUSH_ATTACK,
326 coef * multiplier,
327 DamageFlags::PHYSICAL,
328 );
329 }
330 },
331 );
332
333 proc_main_gauche(ctx);
334}
335
336pub(crate) fn preparation_hook(ctx: &mut HookCtx<'_>) {
337 ctx.reset_cooldown(SPELL_ADRENALINE_RUSH);
338 ctx.reset_cooldown(SPELL_BETWEEN_THE_EYES);
339 ctx.reset_cooldown(SPELL_BLADE_FLURRY);
340 ctx.reset_cooldown(SPELL_BLADE_RUSH);
341 ctx.reset_cooldown(SPELL_KILLING_SPREE);
342}
343
344pub(crate) fn adrenaline_rush_hook(ctx: &mut HookCtx<'_>) {
345 let cfg = outlaw_config(ctx);
346
347 if cfg.has_loaded_dice {
348 ctx.apply_aura(AURA_LOADED_DICE);
349 }
350
351 if cfg.has_improved_adrenaline_rush {
352 let max_cp = ctx.program().base_stats().secondary_resource_max;
353
354 ctx.gain_secondary_resource(max_cp);
355 }
356
357 bound_edge_case(ctx, FATEBOUND);
358}
359
360pub(crate) fn deadly_pursuit_wait_expire_hook(ctx: &mut HookCtx<'_>) {
361 ctx.apply_aura(AURA_DEADLY_PURSUIT_CDR);
362}
363
364#[cfg(test)]
365mod tests {
366 use googletest::prelude::*;
367
368 use super::*;
369 use crate::generated::specs::outlaw_rogue::{EFFECT, SPELL};
370 use crate::hooks::outlaw_rogue::config::OutlawConfig;
371 use wowlab_engine_gamedata::SpellProps;
372 use wowlab_engine_ports::CombatStats;
373 use wowlab_engine_telemetry::TelemetrySink;
374 use wowlab_types::sim::{EnemyIdx, SimTime, SpellIdx};
375
376 #[gtest]
377 fn deft_maneuvers_uses_filtered_ordered_and_capped_hit_identities() -> Result<()> {
378 let encounter = crate::hooks::shared::test_support::spatial_hook_encounter()?;
379 let mut data = wowlab_engine_gamedata::ResolvedGameData::builder();
380
381 data.set_creature_armor(0.0);
382 data.set_armor_constant(1.0);
383 data.set_level(80);
384 data.insert_spell_props(
385 SpellIdx::from_raw(SPELL::BLADE_FLURRY_INSTANT_ATTACK),
386 SpellProps {
387 hostile_max_range: 20.0,
388 radius: 8.0,
389 ..SpellProps::default()
390 },
391 );
392 data.insert_effect_type(
393 wowlab_types::sim::EffectRef::new(
394 SpellIdx::from_raw(SPELL::BLADE_FLURRY_INSTANT_ATTACK),
395 EFFECT::BLADE_FLURRY_INSTANT_ATTACK_DAMAGE.1,
396 ),
397 2,
398 );
399 data.insert_implicit_targets(
400 wowlab_types::sim::EffectRef::new(
401 SpellIdx::from_raw(SPELL::BLADE_FLURRY_INSTANT_ATTACK),
402 EFFECT::BLADE_FLURRY_INSTANT_ATTACK_DAMAGE.1,
403 ),
404 18,
405 16,
406 );
407 let mut draft = crate::test_combat_builder_for(
408 wowlab_engine_combat::generated_identities::specs::outlaw_rogue::PROOF,
409 CombatStats::default(),
410 )
411 .secondary_resource("combo_points", 10.0)
412 .secondary_resource_type_id(4)
413 .game_data(data.build())
414 .encounter(encounter)
415 .spell(
416 "blade_flurry_instant_attack",
417 SPELL::BLADE_FLURRY_INSTANT_ATTACK,
418 Ok,
419 )
420 .into_draft()
421 .or_fail()?;
422
423 draft.set_spec_config(OutlawConfig {
424 deft_maneuvers_cp: 1.0,
425 blade_flurry_targets: 4,
426 ..OutlawConfig::default()
427 });
428 let mut built = draft
429 .finalize(&crate::test_support::wait_rotation())
430 .or_fail()?;
431
432 built
433 .instance
434 .apply_enemy_damage(
435 EnemyIdx(7),
436 crate::hooks::shared::test_support::max_health(),
437 SimTime::ZERO,
438 )
439 .or_fail()?;
440 let mut sink = TelemetrySink::default();
441 let mut rng = || 0.5;
442 let mut ctx = HookCtx::new(
443 wowlab_engine_combat::HookCtxServices {
444 program: &built.program,
445 instance: &mut built.instance,
446 sink: &mut sink,
447 rng: &mut rng,
448 },
449 wowlab_engine_combat::HookCtxRequest::for_target(SimTime::ZERO, EnemyIdx::PRIMARY),
450 );
451
452 verify_that!(
453 ctx.with_resolved_spell_effect_targets(
454 crate::generated::specs::outlaw_rogue::DAMAGE_EFFECT::BLADE_FLURRY_INSTANT_ATTACK_DAMAGE,
455 crate::generated::specs::outlaw_rogue::DAMAGE_SPELL::BLADE_FLURRY_INSTANT_ATTACK,
456 Some(4),
457 |_ctx, targets| targets.to_vec(),
458 ),
459 container_eq([EnemyIdx(0), EnemyIdx(1), EnemyIdx(3), EnemyIdx(4)])
460 )?;
461 blade_flurry_hook(&mut ctx);
462
463 verify_that!(ctx.secondary_resource(), eq(4.0))
464 }
465}