Skip to main content

wowlab_engine_content/hooks/shared/
demon_hunter.rs

1use wowlab_engine_combat::{
2    AuraOps as _, DamageFlags, DamageOps as _, HookCtx, LocalAuraIdx, ProcOps as _,
3};
4
5use crate::generated::specs::{
6    devourer_demon_hunter::HERO::ANNIHILATOR::SPELL as DEVOURER_ANNIHILATOR,
7    vengeance_demon_hunter::HERO::ANNIHILATOR::SPELL as VENGEANCE_ANNIHILATOR,
8};
9
10#[derive(Clone, Copy, Debug)]
11pub(crate) struct AnnihilatorState {
12    pub proc_driver: u32,
13    pub building: LocalAuraIdx,
14    pub spending: LocalAuraIdx,
15    pub final_hour: LocalAuraIdx,
16}
17
18#[derive(Clone, Copy, Debug)]
19#[non_exhaustive]
20pub(crate) enum AnnihilatorCoefficient {
21    AttackPower(f64),
22    SpellPower(f64),
23}
24
25#[derive(Clone, Copy, Debug)]
26pub(crate) struct AnnihilatorDamageProfile {
27    pub spell_id: u32,
28    pub coefficient: AnnihilatorCoefficient,
29}
30
31impl AnnihilatorDamageProfile {
32    fn deal(self, ctx: &mut HookCtx<'_>) {
33        match self.coefficient {
34            AnnihilatorCoefficient::AttackPower(coefficient) => {
35                ctx.deal_damage_ap(self.spell_id, coefficient, DamageFlags::empty());
36            }
37            AnnihilatorCoefficient::SpellPower(coefficient) => {
38                ctx.deal_damage_sp(self.spell_id, coefficient, DamageFlags::empty());
39            }
40        }
41    }
42}
43
44#[derive(Clone, Copy, Debug)]
45pub(crate) struct AnnihilatorDamage {
46    pub meteor: AnnihilatorDamageProfile,
47    pub world_killer: AnnihilatorDamageProfile,
48}
49
50#[derive(Clone, Copy, Debug)]
51pub(crate) struct AnnihilatorRules {
52    pub enabled: bool,
53    pub proc_chance: f64,
54    pub building_stacks: i32,
55    pub maximum_stacks: i32,
56    pub meteoric_fall: bool,
57    pub world_killer: bool,
58}
59
60#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
61pub(crate) struct VoidfallSpend {
62    pub meteors: i32,
63    pub world_killers: i32,
64}
65
66impl VoidfallSpend {
67    fn consumed(self) -> i32 {
68        self.meteors + self.world_killers
69    }
70}
71
72pub(crate) fn gain_voidfall_building(
73    ctx: &mut HookCtx<'_>,
74    state: AnnihilatorState,
75    maximum_stacks: i32,
76    stacks: i32,
77) {
78    ctx.apply_aura_n(state.building.raw(), stacks);
79
80    if ctx.aura_stacks(state.building.raw()) >= maximum_stacks {
81        ctx.expire_aura(state.building.raw());
82        ctx.apply_aura_n(state.spending.raw(), maximum_stacks);
83    }
84}
85
86pub(crate) fn roll_voidfall(
87    ctx: &mut HookCtx<'_>,
88    state: AnnihilatorState,
89    rules: AnnihilatorRules,
90    rolls: i32,
91) {
92    if !rules.enabled || rolls <= 0 {
93        return;
94    }
95
96    for _ in 0..rolls {
97        if ctx.is_aura_active(state.spending.raw()) {
98            return;
99        }
100
101        if ctx.roll_accumulating_proc(state.proc_driver, rules.proc_chance) {
102            gain_voidfall_building(ctx, state, rules.maximum_stacks, rules.building_stacks);
103        }
104    }
105}
106
107pub(crate) fn spend_voidfall(
108    ctx: &mut HookCtx<'_>,
109    state: AnnihilatorState,
110    rules: AnnihilatorRules,
111    damage: AnnihilatorDamage,
112) -> VoidfallSpend {
113    let stacks = ctx.aura_stacks(state.spending.raw());
114    let spend = voidfall_spend(
115        rules.maximum_stacks,
116        stacks,
117        VoidfallSpendMode {
118            meteoric_fall: rules.meteoric_fall,
119            world_killer: rules.world_killer,
120        },
121    );
122
123    if spend == VoidfallSpend::default() {
124        return spend;
125    }
126
127    for _ in 0..spend.meteors {
128        damage.meteor.deal(ctx);
129    }
130
131    for _ in 0..spend.world_killers {
132        damage.world_killer.deal(ctx);
133    }
134
135    let consumed = spend.consumed();
136
137    if consumed >= stacks {
138        ctx.expire_aura(state.spending.raw());
139    } else {
140        for _ in 0..consumed {
141            let _ = ctx.consume_aura_stack(state.spending.raw());
142        }
143    }
144
145    ctx.apply_aura_n(state.final_hour.raw(), consumed);
146
147    spend
148}
149
150#[must_use]
151pub(crate) const fn annihilator_meteor_impact(spell_id: u32) -> bool {
152    matches!(
153        spell_id,
154        DEVOURER_ANNIHILATOR::VOIDFALL_METEOR_DAMAGE
155            | DEVOURER_ANNIHILATOR::WORLD_KILLER_DAMAGE
156            | VENGEANCE_ANNIHILATOR::VOIDFALL_METEOR_DAMAGE
157            | VENGEANCE_ANNIHILATOR::WORLD_KILLER_DAMAGE
158            | VENGEANCE_ANNIHILATOR::METEOR_SHOWER_DAMAGE
159    )
160}
161
162#[derive(Clone, Copy, Debug)]
163struct VoidfallSpendMode {
164    meteoric_fall: bool,
165    world_killer: bool,
166}
167
168fn voidfall_spend(maximum_stacks: i32, stacks: i32, mode: VoidfallSpendMode) -> VoidfallSpend {
169    if stacks <= 0 || (mode.meteoric_fall && stacks < maximum_stacks) {
170        return VoidfallSpend {
171            meteors: 0,
172            world_killers: 0,
173        };
174    }
175
176    if mode.meteoric_fall {
177        VoidfallSpend {
178            meteors: stacks - i32::from(mode.world_killer),
179            world_killers: i32::from(mode.world_killer),
180        }
181    } else {
182        VoidfallSpend {
183            meteors: i32::from(!(stacks == 1 && mode.world_killer)),
184            world_killers: i32::from(stacks == 1 && mode.world_killer),
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use googletest::prelude::*;
192    use rstest::rstest;
193
194    use super::*;
195
196    #[gtest]
197    #[rstest]
198    #[case::inactive(0, 3, false, false, 0, 0)]
199    #[case::ordinary_meteor(3, 3, false, false, 1, 0)]
200    #[case::ordinary_meteor_before_world_killer(2, 3, false, true, 1, 0)]
201    #[case::ordinary_world_killer_on_last_stack(1, 3, false, true, 0, 1)]
202    #[case::meteoric_fall_waits_for_full_state(2, 3, true, true, 0, 0)]
203    #[case::meteoric_fall_spends_all_meteors(3, 3, true, false, 3, 0)]
204    #[case::meteoric_fall_finishes_with_world_killer(3, 3, true, true, 2, 1)]
205    fn spend_plan_matches_voidfall_state_machine(
206        #[case] stacks: i32,
207        #[case] maximum_stacks: i32,
208        #[case] meteoric_fall: bool,
209        #[case] world_killer: bool,
210        #[case] meteors: i32,
211        #[case] world_killers: i32,
212    ) -> Result<()> {
213        verify_that!(
214            voidfall_spend(
215                maximum_stacks,
216                stacks,
217                VoidfallSpendMode {
218                    meteoric_fall,
219                    world_killer,
220                },
221            ),
222            eq(VoidfallSpend {
223                meteors,
224                world_killers,
225            })
226        )
227    }
228
229    #[gtest]
230    #[rstest]
231    #[case::meteor(VENGEANCE_ANNIHILATOR::VOIDFALL_METEOR_DAMAGE, true)]
232    #[case::devourer_meteor(DEVOURER_ANNIHILATOR::VOIDFALL_METEOR_DAMAGE, true)]
233    #[case::world_killer(VENGEANCE_ANNIHILATOR::WORLD_KILLER_DAMAGE, true)]
234    #[case::devourer_world_killer(DEVOURER_ANNIHILATOR::WORLD_KILLER_DAMAGE, true)]
235    #[case::meteor_shower(VENGEANCE_ANNIHILATOR::METEOR_SHOWER_DAMAGE, true)]
236    #[case::unrelated(1, false)]
237    fn catastrophe_filter_only_accepts_annihilator_impacts(
238        #[case] spell_id: u32,
239        #[case] expected: bool,
240    ) -> Result<()> {
241        verify_that!(annihilator_meteor_impact(spell_id), eq(expected))
242    }
243}