Skip to main content

wowlab_engine_content/hooks/assassination_rogue/
hooks.rs

1use wowlab_engine_combat::AuraOps as _;
2use wowlab_engine_combat::DamageOps as _;
3use wowlab_engine_combat::ResourceOps as _;
4use wowlab_engine_combat::SchedulingOps as _;
5use wowlab_engine_combat::{DamageFlags, HookCtx, SwingEvent};
6use wowlab_engine_domain::constants::OFF_HAND_DAMAGE_MULT;
7use wowlab_engine_rng::proc_chance;
8
9use crate::generated::specs::assassination_rogue::{
10    AURA_ALACRITY, AURA_AMPLIFYING_POISON_DEBUFF, AURA_BLINDSIDE, AURA_ENVENOM,
11    AURA_FINISH_THE_JOB, AURA_GARROTE, AURA_IMPLACABLE_BONUS, AURA_IMPLACABLE_REGEN,
12    AURA_IMPLACABLE_TRACKER, AURA_IMPROVED_GARROTE_WINDOW, AURA_INSPIRING_STRIKE,
13    AURA_INTERNAL_BLEEDING, AURA_KINGSBANE_RAMP, AURA_REGICIDES_REWARD, AURA_RUPTURE,
14    AURA_SLICE_AND_DICE,
15};
16#[cfg(test)]
17use crate::generated::specs::assassination_rogue::{EFFECT, SPELL};
18use crate::hooks::shared::rogue::{bound_edge_case, bound_hand_of_fate, cut_to_the_chase};
19
20use super::{
21    config::sin_config,
22    fatebound::{overflowing_purse, BINDING as FATEBOUND},
23    poisons::{proc_poisons, seal_fate},
24};
25
26const MS_PER_CP_ENVENOM: u32 = 1000;
27const MS_PER_CP_RUPTURE: u32 = 4000;
28/// Implacable Strikes 1265787: five 200-millisecond ticks over one second.
29const IMPLACABLE_STRIKE_TICKS: u32 = 5;
30const IMPLACABLE_MAX_BONUS_STACKS: i32 = 4;
31
32/// Disables DBC damage children that an affected spell's cast hook owns.
33pub(crate) const fn hook_owned_damage_trigger_disabled(_ctx: &HookCtx<'_>) -> bool {
34    false
35}
36
37pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, _event: SwingEvent) {
38    proc_poisons(ctx);
39}
40
41pub(crate) fn mutilate_hook(ctx: &mut HookCtx<'_>) {
42    let cfg = sin_config(ctx);
43    let mh = ctx.effect_ap_coefficient(
44        crate::generated::specs::assassination_rogue::DAMAGE_EFFECT::MUTILATE_MH_DMG,
45    );
46    let oh = ctx.effect_ap_coefficient(
47        crate::generated::specs::assassination_rogue::DAMAGE_EFFECT::MUTILATE_OH_DMG,
48    ) * OFF_HAND_DAMAGE_MULT;
49
50    for (action, coef) in [
51        (
52            crate::generated::specs::assassination_rogue::DAMAGE_SPELL::MUTILATE_MH,
53            mh,
54        ),
55        (
56            crate::generated::specs::assassination_rogue::DAMAGE_SPELL::MUTILATE_OH,
57            oh,
58        ),
59    ] {
60        ctx.deal_damage_ap(action, coef, DamageFlags::PHYSICAL);
61
62        if cfg.doomblade_pct > 0.0 {
63            // The engine lacks residual DoTs, so Mutilated Flesh 381673 e1 is dealt directly.
64            ctx.deal_damage_ap(
65                crate::generated::specs::assassination_rogue::DAMAGE_SPELL::MUTILATED_FLESH,
66                coef * cfg.doomblade_pct,
67                DamageFlags::PHYSICAL,
68            );
69        }
70
71        seal_fate(ctx);
72        proc_poisons(ctx);
73    }
74
75    trigger_blindside(ctx);
76}
77
78pub(crate) fn ambush_hook(ctx: &mut HookCtx<'_>) {
79    let cfg = sin_config(ctx);
80
81    ctx.consume_aura(AURA_BLINDSIDE);
82
83    let mut coef = ctx.effect_ap_coefficient(
84        crate::generated::specs::assassination_rogue::DAMAGE_EFFECT::AMBUSH_DMG,
85    );
86
87    if ctx.is_aura_active(AURA_GARROTE) {
88        coef *= cfg.systemic_failure_mult;
89    }
90
91    ctx.deal_damage_ap(
92        crate::generated::specs::assassination_rogue::DAMAGE_SPELL::AMBUSH,
93        coef,
94        DamageFlags::PHYSICAL,
95    );
96
97    seal_fate(ctx);
98    proc_poisons(ctx);
99    trigger_blindside(ctx);
100}
101
102pub(crate) fn garrote_hook(ctx: &mut HookCtx<'_>) {
103    if ctx.is_aura_active(AURA_IMPROVED_GARROTE_WINDOW) {
104        ctx.refresh_aura_snapshot_multiplier(
105            AURA_GARROTE,
106            1.0 + ctx.effect_percent(crate::generated::specs::assassination_rogue::DAMAGE_EFFECT::IMPROVED_GARROTE_DAMAGE),
107        );
108    }
109}
110
111pub(crate) fn rupture_hook(ctx: &mut HookCtx<'_>) {
112    let cfg = sin_config(ctx);
113    let cp =
114        wowlab_types::numeric::f64_to_u32_saturating_trunc(ctx.last_secondary_spent().max(0.0));
115
116    if cp > 0 {
117        ctx.extend_aura(AURA_RUPTURE, cp * MS_PER_CP_RUPTURE);
118    }
119
120    if cfg.has_internal_bleeding && cp > 0 {
121        ctx.apply_aura(AURA_INTERNAL_BLEEDING);
122        ctx.refresh_aura_snapshot_multiplier(AURA_INTERNAL_BLEEDING, f64::from(cp));
123    }
124
125    proc_poisons(ctx);
126    alacrity(ctx);
127    bound_hand_of_fate(ctx, false, FATEBOUND);
128}
129
130pub(crate) fn envenom_hook(ctx: &mut HookCtx<'_>) {
131    let cfg = sin_config(ctx);
132    let cp = ctx.last_secondary_spent().max(1.0);
133    let envenom_was_up = ctx.is_aura_active(AURA_ENVENOM);
134
135    if envenom_was_up && cfg.has_implacable_regen {
136        ctx.apply_aura(AURA_IMPLACABLE_TRACKER);
137    }
138
139    let mut coef = ctx.effect_ap_coefficient(
140        crate::generated::specs::assassination_rogue::DAMAGE_EFFECT::ENVENOM_DMG,
141    ) * cp;
142
143    if cp >= cfg.delivered_doom_cp {
144        coef *= cfg.delivered_doom_mult;
145    }
146
147    if envenom_was_up {
148        coef *= cfg.rapid_injection_mult;
149    }
150
151    if cfg.amplifying_consume_stacks > 0
152        && ctx.aura_stacks(AURA_AMPLIFYING_POISON_DEBUFF) >= cfg.amplifying_consume_stacks
153    {
154        ctx.consume_aura_stacks(AURA_AMPLIFYING_POISON_DEBUFF, cfg.amplifying_consume_stacks);
155        coef *= cfg.amplifying_bonus_mult;
156    }
157
158    // Apply the buff before the hit so Envenom's poison procs benefit.
159
160    ctx.apply_aura(AURA_ENVENOM);
161
162    if cp > 1.0 {
163        ctx.extend_aura(
164            AURA_ENVENOM,
165            (wowlab_types::numeric::f64_to_u32_saturating_trunc(cp) - 1) * MS_PER_CP_ENVENOM,
166        );
167    }
168
169    if envenom_was_up && cfg.inspiring_strike_mult > 1.0 {
170        ctx.apply_aura_with_duration(
171            AURA_INSPIRING_STRIKE,
172            wowlab_types::numeric::f64_to_u32_saturating_trunc(cp) * MS_PER_CP_ENVENOM,
173        );
174    }
175
176    ctx.deal_damage_ap(
177        crate::generated::specs::assassination_rogue::DAMAGE_SPELL::ENVENOM,
178        coef,
179        DamageFlags::empty(),
180    );
181    cut_to_the_chase(ctx, AURA_SLICE_AND_DICE, cp);
182    proc_poisons(ctx);
183    alacrity(ctx);
184    bound_hand_of_fate(ctx, true, FATEBOUND);
185    overflowing_purse(ctx);
186}
187
188pub(crate) fn envenom_expire_hook(ctx: &mut HookCtx<'_>) {
189    let cfg = sin_config(ctx);
190
191    if !cfg.has_implacable_regen {
192        return;
193    }
194
195    let bonus_stacks = ctx
196        .aura_stacks(AURA_IMPLACABLE_TRACKER)
197        .clamp(0, IMPLACABLE_MAX_BONUS_STACKS);
198
199    ctx.expire_aura(AURA_IMPLACABLE_TRACKER);
200    ctx.apply_aura(AURA_IMPLACABLE_REGEN);
201    ctx.apply_aura_n(AURA_IMPLACABLE_BONUS, bonus_stacks);
202}
203
204pub(crate) fn kingsbane_hook(ctx: &mut HookCtx<'_>) {
205    let cfg = sin_config(ctx);
206
207    if !cfg.has_implacable_strikes {
208        return;
209    }
210
211    let nature = ctx.effect_ap_coefficient(
212        crate::generated::specs::assassination_rogue::DAMAGE_EFFECT::IMPLACABLE_STRIKE_NATURE_DMG,
213    );
214    let physical = ctx.effect_ap_coefficient(
215        crate::generated::specs::assassination_rogue::DAMAGE_EFFECT::IMPLACABLE_STRIKE_PHYSICAL_DMG,
216    );
217
218    for _ in 0..IMPLACABLE_STRIKE_TICKS {
219        ctx.deal_damage_ap(
220            crate::generated::specs::assassination_rogue::DAMAGE_SPELL::IMPLACABLE_STRIKES_NATURE,
221            nature,
222            DamageFlags::empty(),
223        );
224        ctx.deal_damage_ap(
225            crate::generated::specs::assassination_rogue::DAMAGE_SPELL::IMPLACABLE_STRIKES_PHYSICAL,
226            physical,
227            DamageFlags::PHYSICAL,
228        );
229        ctx.gain_secondary_resource(cfg.implacable_strike_cp);
230        proc_poisons(ctx);
231    }
232}
233
234pub(crate) fn kingsbane_expire_hook(ctx: &mut HookCtx<'_>) {
235    let cfg = sin_config(ctx);
236    let ramp_stacks = ctx.aura_stacks(AURA_KINGSBANE_RAMP);
237
238    if cfg.regicides_reward_divisor > 0 {
239        ctx.apply_aura_n(
240            AURA_REGICIDES_REWARD,
241            ramp_stacks / cfg.regicides_reward_divisor,
242        );
243    }
244
245    ctx.consume_aura(AURA_KINGSBANE_RAMP);
246}
247
248pub(crate) fn deathmark_hook(ctx: &mut HookCtx<'_>) {
249    let cfg = sin_config(ctx);
250
251    let _ = ctx.trigger_periodic_driver(
252        wowlab_engine_combat::generated_identities::periodic_driver::DEATHMARK,
253    );
254
255    if cfg.has_finish_the_job {
256        ctx.apply_aura(AURA_FINISH_THE_JOB);
257    }
258
259    bound_edge_case(ctx, FATEBOUND);
260}
261
262pub(super) fn deathmark_energize_tick(ctx: &mut HookCtx<'_>) {
263    ctx.gain_resource(sin_config(ctx).deathmark_energy_per_tick);
264}
265
266pub(crate) fn vanish_hook(ctx: &mut HookCtx<'_>) {
267    if ctx.is_aura_active(crate::generated::specs::assassination_rogue::AURA_IMPROVED_GARROTE) {
268        ctx.apply_aura(AURA_IMPROVED_GARROTE_WINDOW);
269    }
270}
271
272fn alacrity(ctx: &mut HookCtx<'_>) {
273    let cfg = sin_config(ctx);
274
275    if cfg.alacrity_chance_per_cp <= 0.0 {
276        return;
277    }
278
279    let cp = ctx.last_secondary_spent().max(0.0);
280
281    if proc_chance(ctx.rng(), cfg.alacrity_chance_per_cp * cp) {
282        ctx.apply_aura(AURA_ALACRITY);
283    }
284}
285
286fn trigger_blindside(ctx: &mut HookCtx<'_>) {
287    let cfg = sin_config(ctx);
288    let chance = if ctx.target_health_fraction() < cfg.blindside_execute_threshold {
289        cfg.blindside_execute_chance
290    } else {
291        cfg.blindside_chance
292    };
293
294    if chance > 0.0 && proc_chance(ctx.rng(), chance) {
295        ctx.apply_aura(AURA_BLINDSIDE);
296    }
297}
298
299#[cfg(test)]
300#[path = "hooks/tests.rs"]
301mod tests;