Skip to main content

wowlab_engine_content/hooks/outlaw_rogue/
hooks.rs

1use 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
92/// Palmed Bullets threshold (spell 1265931 effect 1, six stacks).
93const 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        // Each secondary shot snapshots Opportunity before consuming its charge.
280        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}