Skip to main content

wowlab_engine_content/hooks/vengeance_demon_hunter/
hooks.rs

1use 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
20/// Soul Cleave consumes up to two Soul Fragments.
21const 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];
29/// Untethered Rage uses an approximated 0.75% chance per consumed soul.
30pub(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
96/// Handles Fracture's fragments and Voidfall roll.
97pub(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
111/// Handles Soul Cleave's fragment-consuming riders.
112pub(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    // Meteor travel keeps builder rolls blocked for this cast.
131    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
140/// Handles Spirit Bomb's fragment, Dark Matter, and Voidfall riders.
141pub(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    // Dark Matter's meteor shower is collapsed to impact time.
157
158    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
178/// Handles Metamorphosis's Annihilator riders.
179pub(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/// Keeps Fel Blood's per-hit multiplier neutral because it modifies attack power.
215#[must_use]
216pub(crate) const fn mastery(_ctx: &MasteryCtx<'_>) -> f64 {
217    1.0
218}