wowlab_engine_combat/state/program/effects/
active_aura.rs1use 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}