wowlab_engine_content/hooks/vengeance_demon_hunter/
hooks.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::AuraOps as _;
4use wowlab_engine_combat::{DamageFlags, HookCtx, MasteryCtx};
5use wowlab_engine_rng::proc_chance;
6
7use super::config::{AnnihilatorConfig, AnnihilatorRuntime, annihilator_config};
8use crate::hooks::shared::demon_hunter::{
9 AnnihilatorCoefficient, AnnihilatorDamage, AnnihilatorDamageProfile, AnnihilatorRules,
10 AnnihilatorState, gain_voidfall_building, roll_voidfall, spend_voidfall,
11};
12
13use crate::generated::specs::vengeance_demon_hunter::{
14 AURA_DARK_MATTER, AURA_SEETHING_ANGER, AURA_SIGIL_OF_FLAME, AURA_SOUL_FRAGMENTS,
15 AURA_UNTETHERED_RAGE, AURA_VOIDFALL_BUILDING, AURA_VOIDFALL_FINAL_HOUR,
16 AURA_VOIDFALL_SPENDING,
17 SPELL_METAMORPHOSIS, SPELL_SPIRIT_BOMB, TALENT,
18};
19
20const SOUL_CLEAVE_FRAGMENTS: i32 = 2;
22
23#[rustfmt::skip]
24pub(super) const UNTETHERED_RAGE_TALENTS: [u32; 3] = [
25 TALENT::UNTETHERED_RAGE_1,
26 TALENT::UNTETHERED_RAGE_2,
27 TALENT::UNTETHERED_RAGE_3,
28];
29pub(super) const UNTETHERED_RAGE_CHANCE_PER_SOUL: f64 = 0.0075;
31const UNTETHERED_RAGE_GROWTH: f64 = 1.35;
32
33const fn annihilator_state() -> AnnihilatorState {
34 AnnihilatorState {
35 proc_driver: wowlab_engine_combat::generated_identities::proc_counter::VOIDFALL_ATTEMPT,
36 building: AURA_VOIDFALL_BUILDING,
37 spending: AURA_VOIDFALL_SPENDING,
38 final_hour: AURA_VOIDFALL_FINAL_HOUR,
39 }
40}
41
42const fn annihilator_rules(cfg: &AnnihilatorConfig) -> AnnihilatorRules {
43 AnnihilatorRules {
44 enabled: cfg.voidfall,
45 proc_chance: cfg.voidfall_chance,
46 building_stacks: cfg.voidfall_stacks,
47 maximum_stacks: cfg.voidfall_max_stacks,
48 meteoric_fall: cfg.meteoric_fall,
49 world_killer: cfg.world_killer,
50 }
51}
52
53const fn annihilator_damage(cfg: &AnnihilatorConfig) -> AnnihilatorDamage {
54 AnnihilatorDamage {
55 meteor: AnnihilatorDamageProfile {
56 action: crate::generated::specs::vengeance_demon_hunter::DAMAGE_SPELL::VOIDFALL_METEOR_DAMAGE,
57 coefficient: AnnihilatorCoefficient::AttackPower(cfg.meteor_coef),
58 },
59 world_killer: AnnihilatorDamageProfile {
60 action: crate::generated::specs::vengeance_demon_hunter::DAMAGE_SPELL::WORLD_KILLER_DAMAGE,
61 coefficient: AnnihilatorCoefficient::AttackPower(cfg.world_killer_coef),
62 },
63 }
64}
65
66fn spend_annihilator(ctx: &mut HookCtx<'_>, cfg: &AnnihilatorConfig) {
67 let spend = spend_voidfall(
68 ctx,
69 annihilator_state(),
70 annihilator_rules(cfg),
71 annihilator_damage(cfg),
72 );
73
74 if spend.world_killers > 0 {
75 ctx.reduce_cooldown(SPELL_METAMORPHOSIS, cfg.world_killer_meta_cdr_ms);
76 }
77}
78
79fn roll_untethered_rage(ctx: &mut HookCtx<'_>, cfg: &AnnihilatorConfig, souls: i32) {
80 if souls <= 0 || cfg.ur_chance_per_soul <= 0.0 {
81 return;
82 }
83
84 let seething = ctx.aura_stacks(AURA_SEETHING_ANGER);
85 let chance = f64::from(souls) * cfg.ur_chance_per_soul * UNTETHERED_RAGE_GROWTH.powi(seething);
86
87 if proc_chance(ctx.rng(), chance) {
88 ctx.expire_aura(AURA_SEETHING_ANGER);
89 ctx.apply_aura(AURA_UNTETHERED_RAGE);
90 ctx.reset_cooldown(SPELL_METAMORPHOSIS);
91 } else if cfg.ur_seething && !ctx.is_aura_active(AURA_UNTETHERED_RAGE) {
92 ctx.apply_aura(AURA_SEETHING_ANGER);
93 }
94}
95
96pub(crate) fn fracture_hook(ctx: &mut HookCtx<'_>) {
98 let fragments = ctx.effect_base_points(crate::generated::specs::vengeance_demon_hunter::DAMAGE_EFFECT::FRACTURE_FRAGMENTS);
99
100 #[expect(
101 clippy::cast_possible_truncation,
102 reason = "DBC base points are bounded to two fragments"
103 )]
104 ctx.apply_aura_n(AURA_SOUL_FRAGMENTS, fragments as i32);
105
106 let cfg = annihilator_config(ctx);
107
108 roll_voidfall(ctx, annihilator_state(), annihilator_rules(&cfg), 1);
109}
110
111pub(crate) fn soul_cleave_hook(ctx: &mut HookCtx<'_>) {
113 let mut consumed = 0i32;
114
115 for _ in 0..SOUL_CLEAVE_FRAGMENTS {
116 if !ctx.consume_aura_stack(AURA_SOUL_FRAGMENTS) {
117 break;
118 }
119
120 consumed += 1;
121 }
122
123 let cfg = annihilator_config(ctx);
124
125 if cfg.mid2_2pc_sigil_extension_ms > 0 && ctx.is_aura_active(AURA_SIGIL_OF_FLAME) {
126 ctx.extend_aura(AURA_SIGIL_OF_FLAME, cfg.mid2_2pc_sigil_extension_ms);
127 }
128
129 roll_untethered_rage(ctx, &cfg, consumed);
130 let was_spending = ctx.is_aura_active(AURA_VOIDFALL_SPENDING);
132
133 spend_annihilator(ctx, &cfg);
134
135 if !was_spending {
136 roll_voidfall(ctx, annihilator_state(), annihilator_rules(&cfg), consumed);
137 }
138}
139
140pub(crate) fn spirit_bomb_hook(ctx: &mut HookCtx<'_>) {
142 let cfg = annihilator_config(ctx);
143 let fragments = ctx.aura_stacks(AURA_SOUL_FRAGMENTS);
144
145 if fragments > 0 {
146 let coef = ctx.effect_ap_coefficient(crate::generated::specs::vengeance_demon_hunter::DAMAGE_EFFECT::SPIRIT_BOMB_DAMAGE);
147
148 ctx.deal_damage_ap(
149 crate::generated::specs::vengeance_demon_hunter::DAMAGE_EFFECT::SPIRIT_BOMB_DAMAGE.action(),
150 coef * f64::from(fragments) * cfg.spirit_bomb_mult,
151 DamageFlags::empty(),
152 );
153 ctx.expire_aura(AURA_SOUL_FRAGMENTS);
154 }
155
156 if cfg.dark_matter && ctx.consume_aura(AURA_DARK_MATTER) {
159 for _ in 0..cfg.meteor_shower_ticks {
160 ctx.deal_damage_ap(
161 crate::generated::specs::vengeance_demon_hunter::DAMAGE_SPELL::METEOR_SHOWER_DAMAGE,
162 cfg.meteor_shower_coef,
163 DamageFlags::empty(),
164 );
165 }
166 }
167
168 roll_untethered_rage(ctx, &cfg, fragments);
169 let was_spending = ctx.is_aura_active(AURA_VOIDFALL_SPENDING);
170
171 spend_annihilator(ctx, &cfg);
172
173 if !was_spending {
174 roll_voidfall(ctx, annihilator_state(), annihilator_rules(&cfg), fragments);
175 }
176}
177
178pub(crate) fn metamorphosis_hook(ctx: &mut HookCtx<'_>) {
180 let now_ms = ctx.now().as_millis();
181
182 {
183 let Some(runtime) = ctx.spec_runtime_mut::<AnnihilatorRuntime>() else {
184 return;
185 };
186
187 if runtime.last_meta_ms == now_ms {
188 return;
189 }
190
191 runtime.last_meta_ms = now_ms;
192 };
193 let cfg = annihilator_config(ctx);
194
195 if ctx.is_aura_active(AURA_UNTETHERED_RAGE) {
196 ctx.expire_aura(AURA_UNTETHERED_RAGE);
197 }
198
199 if cfg.dark_matter {
200 ctx.apply_aura(AURA_DARK_MATTER);
201 }
202
203 if cfg.voidfall && cfg.mass_accel_stacks > 0 {
204 gain_voidfall_building(
205 ctx,
206 annihilator_state(),
207 cfg.voidfall_max_stacks,
208 cfg.mass_accel_stacks,
209 );
210 ctx.reset_cooldown(SPELL_SPIRIT_BOMB);
211 }
212}
213
214#[must_use]
216pub(crate) const fn mastery(_ctx: &MasteryCtx<'_>) -> f64 {
217 1.0
218}