Skip to main content

wowlab_engine_content/hooks/assassination_rogue/
hooks.rs

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