wowlab_engine_content/hooks/shared/
demon_hunter.rs1use 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}