wowlab_engine_content/hooks/assassination_rogue/
hooks.rs1use 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;
26const IMPLACABLE_STRIKE_TICKS: u32 = 5;
28const IMPLACABLE_MAX_BONUS_STACKS: i32 = 4;
29
30pub(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 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 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;