Skip to main content

wowlab_engine_combat/state/program/effects/
active_aura.rs

1use wowlab_engine_domain::dbc::SpellAuraInterruptFlags;
2
3use super::{
4    ActionSpellModifierBindings, AuraData, EffectPlan, LocalAuraIdx, MODIFIER_PROPERTY_SLOT_COUNT,
5    PlannedAuraEffect,
6};
7
8#[derive(Debug, Default)]
9pub(crate) struct ActiveAuraEffectPlan {
10    pub(super) masks: Box<[(LocalAuraIdx, u16)]>,
11    pub(super) relevant_locals: Box<[u64]>,
12    aura_count: usize,
13}
14
15#[derive(Debug)]
16pub(crate) struct ActiveAuraBreakPlans {
17    action: ActiveAuraEffectPlan,
18    periodic_damage: ActiveAuraEffectPlan,
19    direct_damage: ActiveAuraEffectPlan,
20}
21
22impl ActiveAuraEffectPlan {
23    pub(super) fn from_masks(masks: Vec<u16>) -> Self {
24        let aura_count = masks.len();
25        let masks = masks
26            .into_iter()
27            .enumerate()
28            .filter_map(|(raw_local, mask)| {
29                (mask != 0).then(|| {
30                    (
31                        LocalAuraIdx::new(
32                            u8::try_from(raw_local)
33                                .expect("finalized aura count fits the local identity"),
34                        ),
35                        mask,
36                    )
37                })
38            })
39            .collect::<Vec<_>>();
40
41        if masks.is_empty() {
42            return Self {
43                masks: Box::default(),
44                relevant_locals: Box::default(),
45                aura_count,
46            };
47        }
48
49        let mut relevant_locals = vec![0_u64; aura_count.div_ceil(u64::BITS as usize)];
50
51        for &(local, _) in &masks {
52            let raw_local = local.as_usize();
53
54            *relevant_locals
55                .get_mut(raw_local / u64::BITS as usize)
56                .expect("sparse local belongs to the finalized relevant-word array") |=
57                1_u64 << raw_local % u64::BITS as usize;
58        }
59
60        Self {
61            masks: masks.into_boxed_slice(),
62            relevant_locals: relevant_locals.into_boxed_slice(),
63            aura_count,
64        }
65    }
66
67    pub(super) fn compile(entries: &[PlannedAuraEffect], aura_count: usize) -> Self {
68        let mut masks = vec![0_u16; aura_count];
69
70        for entry in entries {
71            let raw_local = entry.local.as_usize();
72
73            *masks
74                .get_mut(raw_local)
75                .expect("planned aura local belongs to the finalized mask array") |=
76                1_u16 << entry.position;
77        }
78
79        Self::from_masks(masks)
80    }
81
82    pub(crate) fn mask(&self, local: LocalAuraIdx) -> u16 {
83        self.masks
84            .binary_search_by_key(&local, |&(candidate, _)| candidate)
85            .map_or(0, |index| {
86                self.masks
87                    .get(index)
88                    .expect("binary search index belongs to this sparse mask array")
89                    .1
90            })
91    }
92
93    pub(crate) fn masks(&self) -> &[(LocalAuraIdx, u16)] {
94        &self.masks
95    }
96
97    pub(crate) fn relevant_locals(&self) -> &[u64] {
98        &self.relevant_locals
99    }
100
101    pub(super) fn validate(&self, aura_count: usize) -> bool {
102        if self.aura_count != aura_count {
103            return false;
104        }
105
106        if self.masks.is_empty() {
107            return self.relevant_locals.is_empty();
108        }
109
110        if self.relevant_locals.len() != aura_count.div_ceil(u64::BITS as usize)
111            || self
112                .masks
113                .iter()
114                .any(|&(local, mask)| local.as_usize() >= aura_count || mask == 0)
115            || self
116                .masks
117                .iter()
118                .zip(self.masks.iter().skip(1))
119                .any(|(left, right)| left.0 >= right.0)
120        {
121            return false;
122        }
123
124        let relevant_count = self
125            .relevant_locals
126            .iter()
127            .map(|word| word.count_ones() as usize)
128            .sum::<usize>();
129
130        if relevant_count != self.masks.len() {
131            return false;
132        }
133
134        for &(local, _) in &self.masks {
135            let raw_local = local.as_usize();
136            let Some(word) = self.relevant_locals.get(raw_local / u64::BITS as usize) else {
137                return false;
138            };
139
140            if word & (1_u64 << raw_local % u64::BITS as usize) == 0 {
141                return false;
142            }
143        }
144
145        true
146    }
147}
148
149impl ActionSpellModifierBindings {
150    fn plans_are_unbound(&self) -> bool {
151        self.passive_indexes.is_empty()
152            && self.player_aura_plans.is_empty()
153            && self.scoped_passive_indexes.is_empty()
154            && self.scoped_aura_plans.is_empty()
155            && self.damage_passive_indexes.is_empty()
156            && self.damage_aura_plans.is_empty()
157            && self.cost_passive_indexes.is_empty()
158            && self.cost_aura_plans.is_empty()
159    }
160
161    fn plan_counts_match(&self, action_count: usize) -> bool {
162        let property_count = action_count.saturating_mul(MODIFIER_PROPERTY_SLOT_COUNT);
163
164        self.passive_indexes.len() == property_count
165            && self.player_aura_plans.len() == property_count
166            && self.damage_passive_indexes.len() == action_count
167            && self.damage_aura_plans.len() == action_count
168            && self.cost_passive_indexes.len() == action_count
169            && self.cost_aura_plans.len() == action_count
170            && self.scoped_aura_plans.len() == action_count
171    }
172
173    fn validate(&self, aura_count: usize, passive_count: usize) -> bool {
174        let action_count = self.scoped_passive_indexes.len();
175        let counts_match = self.plans_are_unbound() || self.plan_counts_match(action_count);
176        let passive_indexes_are_valid = self
177            .passive_indexes
178            .iter()
179            .chain(&self.scoped_passive_indexes)
180            .chain(&self.damage_passive_indexes)
181            .chain(&self.cost_passive_indexes)
182            .flatten()
183            .all(|index| *index < passive_count);
184        let aura_plans_are_valid = self
185            .player_aura_plans
186            .iter()
187            .chain(&self.scoped_aura_plans)
188            .chain(&self.damage_aura_plans)
189            .chain(&self.cost_aura_plans)
190            .all(|plan| plan.validate(aura_count));
191
192        counts_match && passive_indexes_are_valid && aura_plans_are_valid
193    }
194}
195
196impl ActiveAuraBreakPlans {
197    pub(super) fn compile(auras: &[AuraData]) -> Self {
198        Self {
199            action: break_plan(auras, |flags| {
200                flags.contains(SpellAuraInterruptFlags::ACTION)
201            }),
202            periodic_damage: break_plan(auras, |flags| {
203                flags.contains(SpellAuraInterruptFlags::DAMAGE)
204            }),
205            direct_damage: break_plan(auras, |flags| {
206                flags.intersects(
207                    SpellAuraInterruptFlags::DAMAGE | SpellAuraInterruptFlags::NON_PERIODIC_DAMAGE,
208                )
209            }),
210        }
211    }
212
213    pub(super) const fn action(&self) -> &ActiveAuraEffectPlan {
214        &self.action
215    }
216
217    pub(super) const fn damage(&self, periodic: bool) -> &ActiveAuraEffectPlan {
218        if periodic {
219            &self.periodic_damage
220        } else {
221            &self.direct_damage
222        }
223    }
224
225    fn validate(&self, aura_count: usize) -> bool {
226        self.action.validate(aura_count)
227            && self.periodic_damage.validate(aura_count)
228            && self.direct_damage.validate(aura_count)
229    }
230}
231
232fn break_plan(
233    auras: &[AuraData],
234    matches: impl Fn(SpellAuraInterruptFlags) -> bool,
235) -> ActiveAuraEffectPlan {
236    ActiveAuraEffectPlan::from_masks(
237        auras
238            .iter()
239            .map(|aura| u16::from(matches(aura.interrupt_flags)))
240            .collect(),
241    )
242}
243
244impl EffectPlan {
245    pub(crate) const fn action_break_aura_plan(&self) -> &ActiveAuraEffectPlan {
246        self.break_auras.action()
247    }
248
249    pub(crate) const fn damage_break_aura_plan(&self, periodic: bool) -> &ActiveAuraEffectPlan {
250        self.break_auras.damage(periodic)
251    }
252
253    pub(super) fn optimized_plans_valid(&self, aura_count: usize, passive_count: usize) -> bool {
254        self.static_total_auras.validate(aura_count)
255            && self.dynamic_total_auras.validate(aura_count)
256            && self.break_auras.validate(aura_count)
257            && self.family_auras.len() == super::EFFECT_FAMILY_COUNT
258            && self
259                .family_auras
260                .iter()
261                .all(|plan| plan.validate(aura_count))
262            && self
263                .action_spell_modifiers
264                .validate(aura_count, passive_count)
265    }
266}