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_domain::dbc::ResolvedGameDataEffectExt as _;
6use wowlab_engine_rng::proc_chance;
7
8use super::config::{AnnihilatorConfig, AnnihilatorRuntime, annihilator_config};
9use crate::hooks::shared::demon_hunter::{
10 AnnihilatorCoefficient, AnnihilatorDamage, AnnihilatorDamageProfile, AnnihilatorRules,
11 AnnihilatorState, gain_voidfall_building, roll_voidfall, spend_voidfall,
12};
13
14use crate::generated::specs::vengeance_demon_hunter::{
15 AURA_DARK_MATTER, AURA_SEETHING_ANGER, AURA_SOUL_FRAGMENTS, AURA_UNTETHERED_RAGE,
16 AURA_VOIDFALL_BUILDING, AURA_VOIDFALL_FINAL_HOUR, AURA_VOIDFALL_SPENDING, EFFECT, HERO,
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: HERO::ANNIHILATOR::SPELL::VOIDFALL,
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 spell_id: HERO::ANNIHILATOR::SPELL::VOIDFALL_METEOR_DAMAGE,
57 coefficient: AnnihilatorCoefficient::AttackPower(cfg.meteor_coef),
58 },
59 world_killer: AnnihilatorDamageProfile {
60 spell_id: HERO::ANNIHILATOR::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.raw(), 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.raw());
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.raw());
89 ctx.apply_aura(AURA_UNTETHERED_RAGE.raw());
90 ctx.reset_cooldown(SPELL_METAMORPHOSIS.raw());
91 } else if cfg.ur_seething && !ctx.is_aura_active(AURA_UNTETHERED_RAGE.raw()) {
92 ctx.apply_aura(AURA_SEETHING_ANGER.raw());
93 }
94}
95
96pub(crate) fn fracture_hook(ctx: &mut HookCtx<'_>) {
98 let fragments = ctx
99 .game_data()
100 .effect_base_points(EFFECT::FRACTURE_FRAGMENTS);
101
102 #[expect(
103 clippy::cast_possible_truncation,
104 reason = "DBC base points are bounded to two fragments"
105 )]
106 ctx.apply_aura_n(AURA_SOUL_FRAGMENTS.raw(), fragments as i32);
107
108 let cfg = annihilator_config(ctx);
109
110 roll_voidfall(ctx, annihilator_state(), annihilator_rules(&cfg), 1);
111}
112
113pub(crate) fn soul_cleave_hook(ctx: &mut HookCtx<'_>) {
115 let mut consumed = 0i32;
116
117 for _ in 0..SOUL_CLEAVE_FRAGMENTS {
118 if !ctx.consume_aura_stack(AURA_SOUL_FRAGMENTS.raw()) {
119 break;
120 }
121
122 consumed += 1;
123 }
124
125 let cfg = annihilator_config(ctx);
126
127 roll_untethered_rage(ctx, &cfg, consumed);
128 let was_spending = ctx.is_aura_active(AURA_VOIDFALL_SPENDING.raw());
130
131 spend_annihilator(ctx, &cfg);
132
133 if !was_spending {
134 roll_voidfall(ctx, annihilator_state(), annihilator_rules(&cfg), consumed);
135 }
136}
137
138pub(crate) fn spirit_bomb_hook(ctx: &mut HookCtx<'_>) {
140 let cfg = annihilator_config(ctx);
141 let fragments = ctx.aura_stacks(AURA_SOUL_FRAGMENTS.raw());
142
143 if fragments > 0 {
144 let coef = ctx
145 .game_data()
146 .effect_ap_coefficient(EFFECT::SPIRIT_BOMB_DAMAGE);
147
148 ctx.deal_damage_ap(
149 EFFECT::SPIRIT_BOMB_DAMAGE.0,
150 coef * f64::from(fragments) * cfg.spirit_bomb_mult,
151 DamageFlags::empty(),
152 );
153 ctx.expire_aura(AURA_SOUL_FRAGMENTS.raw());
154 }
155
156 if cfg.dark_matter && ctx.consume_aura(AURA_DARK_MATTER.raw()) {
159 for _ in 0..cfg.meteor_shower_ticks {
160 ctx.deal_damage_ap(
161 HERO::ANNIHILATOR::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.raw());
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.raw()) {
196 ctx.expire_aura(AURA_UNTETHERED_RAGE.raw());
197 }
198
199 if cfg.dark_matter {
200 ctx.apply_aura(AURA_DARK_MATTER.raw());
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.raw());
211 }
212}
213
214#[must_use]
216pub(crate) const fn mastery(_ctx: &MasteryCtx<'_>) -> f64 {
217 1.0
218}