1use std::ops::Range;
8
9use wowlab_engine_domain::{
10 dbc::{ResolvedGameDataSemanticExt as _, StatModifierKind},
11 rotation::{AuraProjectionSlotHandle, AuraSlotHandle, ContextSchema, SpellSlotHandle},
12};
13use wowlab_types::{
14 data::ResourceCostPercentBasis,
15 game::Attribute,
16 sim::{ActorId, AuraIdx, AuraKey, AuraOn, EnemyIdx, PetIdx, SpellIdx},
17};
18
19use super::{BufferBindings, ConcreteAuraBinding, ProgramConfig};
20use crate::state::{
21 AuraData, BuffEffect, LocalAuraIdx, LocalSpellIdx, SpellData, SpellEffectData, SpellGroupRule,
22};
23
24bitflags::bitflags! {
25 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27 pub(crate) struct AuraDependencies: u16 {
28 const GATES = 1 << 0;
29 const PRIMARY_STAT = 1 << 1;
30 const STAMINA = 1 << 2;
31 const RECHARGE_RATE = 1 << 3;
32 const MAXIMUM_CHARGES = 1 << 4;
33 const AURA_TIME = 1 << 5;
34 const PLAYER_SPEED = 1 << 6;
35 const COMPANION_SPEED = 1 << 7;
36 const MAXIMUM_RESOURCE = 1 << 8;
37 const PET_STATS = 1 << 9;
38 const RESOURCE_REGEN = 1 << 10;
39 const PET_ACTIONS = 1 << 11;
40 const TARGET_RANGE = 1 << 12;
41 }
42}
43
44#[derive(Clone, Debug, Default)]
45struct AuraDependencyEntry {
46 consumers: AuraDependencies,
47 costs: Range<usize>,
48 recharge: Range<usize>,
49 maximum_charges: Range<usize>,
50 gates: Range<usize>,
51 aura_timing: Range<usize>,
52 projections: Range<usize>,
53 exclusive_spells: Range<usize>,
54}
55
56#[derive(Clone, Debug, Default)]
57struct AuraDependencyGraphEntry {
58 consumers: AuraDependencies,
59 costs: Range<usize>,
60 recharge: Range<usize>,
61 maximum_charges: Range<usize>,
62 gates: Range<usize>,
63 aura_timing: Range<usize>,
64 exclusive_spells: Range<usize>,
65}
66
67#[derive(Clone, Copy, Debug)]
68struct UnboundExclusiveSpellDependency {
69 local: LocalSpellIdx,
70 enabled: bool,
71}
72
73#[derive(Debug)]
75pub(crate) struct AuraDependencyGraph {
76 entries: Box<[AuraDependencyGraphEntry]>,
77 cost_spells: Box<[LocalSpellIdx]>,
78 recharge_spells: Box<[LocalSpellIdx]>,
79 maximum_charge_spells: Box<[LocalSpellIdx]>,
80 gate_spells: Box<[LocalSpellIdx]>,
81 aura_timing: Box<[LocalAuraIdx]>,
82 exclusive_spells: Box<[UnboundExclusiveSpellDependency]>,
83}
84
85#[derive(Clone, Debug, Default)]
86struct ConcreteAuraDependencyEntry {
87 exclusive_peers: Range<usize>,
88}
89
90const FIXED_ACTOR_BUCKET_COUNT: usize = 2;
91
92#[derive(Clone, Debug)]
93struct ActorRanges<T> {
94 entries: Box<[T]>,
95 player: Range<usize>,
96 external: Range<usize>,
97 pets: Box<[Range<usize>]>,
98 enemies: Box<[Range<usize>]>,
99}
100
101#[derive(Clone, Copy, Debug)]
102pub(crate) struct ControlPeerDependency {
103 pub(crate) handle: AuraSlotHandle,
104 pub(crate) matches_kind: bool,
105 pub(crate) matches_group: bool,
106 pub(crate) permanent: bool,
107}
108
109impl<T> ActorRanges<T> {
110 fn for_actor(&self, actor: ActorId) -> &[T] {
111 let range = match actor {
112 ActorId::Player => Some(self.player.clone()),
113 ActorId::External => Some(self.external.clone()),
114 ActorId::Pet(pet) => self.pets.get(pet.as_usize()).cloned(),
115 ActorId::Enemy(enemy) => self.enemies.get(enemy.as_usize()).cloned(),
116 };
117
118 range.map_or(&[], |range| {
119 &self.entries[range]
121 })
122 }
123}
124
125impl<T> Default for ActorRanges<T> {
126 fn default() -> Self {
127 Self {
128 entries: Box::default(),
129 player: 0..0,
130 external: 0..0,
131 pets: Box::default(),
132 enemies: Box::default(),
133 }
134 }
135}
136
137#[derive(Clone, Copy, Debug)]
138pub(crate) struct ProjectionDependency {
139 pub(crate) destination: AuraProjectionSlotHandle,
140 source_offset: usize,
141 index: AuraProjectionDependencyIdx,
142}
143
144#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
146pub(crate) struct AuraProjectionDependencyIdx(usize);
147
148impl AuraProjectionDependencyIdx {
149 #[expect(
150 clippy::trivially_copy_pass_by_ref,
151 reason = "C-CONV requires borrowed as_ receivers"
152 )]
153 pub(crate) const fn as_usize(&self) -> usize {
154 self.0
155 }
156}
157
158#[derive(Clone, Copy, Debug)]
160pub(crate) struct ExclusiveSpellDependency {
161 pub(crate) handle: SpellSlotHandle,
162 pub(crate) enabled: bool,
163}
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub(crate) struct SpellConsumerBinding {
168 pub(crate) local: LocalSpellIdx,
169 pub(crate) handle: SpellSlotHandle,
170}
171
172#[derive(Debug)]
174pub(crate) struct AuraDependencyPlan {
175 entries: Box<[AuraDependencyEntry]>,
176 concrete_entries: Box<[ConcreteAuraDependencyEntry]>,
177 cost_spells: Box<[SpellConsumerBinding]>,
178 recharge_spells: Box<[LocalSpellIdx]>,
179 maximum_charge_spells: Box<[LocalSpellIdx]>,
180 gate_spells: Box<[SpellConsumerBinding]>,
181 aura_timing: Box<[LocalAuraIdx]>,
182 aura_timing_bindings: Box<[ActorRanges<ConcreteAuraBinding>]>,
183 control_peers: Box<[ActorRanges<ControlPeerDependency>]>,
184 projections: Box<[ProjectionDependency]>,
185 projection_sources: Box<[Option<AuraSlotHandle>]>,
186 projection_target_count: usize,
187 exclusive_peers: Box<[ConcreteAuraBinding]>,
188 exclusive_spells: Box<[ExclusiveSpellDependency]>,
189}
190
191impl AuraDependencyGraph {
192 pub(crate) fn compile(
196 auras: &[AuraData],
197 spells: &[SpellData],
198 spell_effects: &[SpellEffectData],
199 config: &ProgramConfig,
200 ) -> Self {
201 let mut entries = Vec::with_capacity(auras.len());
202 let mut cost_spells = Vec::new();
203 let mut recharge_spells = Vec::new();
204 let mut maximum_charge_spells = Vec::new();
205 let mut gate_spells = Vec::new();
206 let mut aura_timing = Vec::new();
207 let mut exclusive_spells = Vec::new();
208
209 for (raw_local, aura) in auras.iter().enumerate() {
210 let local = LocalAuraIdx::new(
211 u8::try_from(raw_local).expect("finalized aura count is validated below 256"),
212 );
213 let mut consumers = AuraDependencies::empty();
214 let mut local_costs = Vec::new();
215 let mut local_recharge = Vec::new();
216 let mut local_maximum_charges = Vec::new();
217 let mut local_gates = Vec::new();
218 let mut local_timing = Vec::new();
219
220 for effect in aura.effects.iter().flatten() {
221 classify_consumers(effect, config, &mut consumers);
222 collect_cost_spells(effect, spells, config, &mut local_costs);
223 collect_recharge_spells(effect, spells, config, &mut local_recharge);
224 collect_maximum_charge_spells(effect, spells, &mut local_maximum_charges);
225 collect_gate_spells(effect, spells, &mut local_gates);
226 collect_aura_timing(effect, auras, config, &mut local_timing);
227 }
228
229 if consumers.contains(AuraDependencies::MAXIMUM_RESOURCE) {
230 local_costs.extend(
231 spells
232 .iter()
233 .enumerate()
234 .filter(|&(_index, spell)| spell_uses_maximum_resource_cost(spell))
235 .map(|(index, _spell)| local_spell(index)),
236 );
237 }
238
239 normalize(&mut local_costs);
240 normalize(&mut local_recharge);
241 normalize(&mut local_maximum_charges);
242 normalize(&mut local_gates);
243 normalize(&mut local_timing);
244
245 let costs = append_range(&mut cost_spells, local_costs);
246 let recharge = append_range(&mut recharge_spells, local_recharge);
247 let maximum_charges = append_range(&mut maximum_charge_spells, local_maximum_charges);
248 let gates = append_range(&mut gate_spells, local_gates);
249 let aura_timing = append_range(&mut aura_timing, local_timing);
250 let local_exclusive_spells =
251 collect_exclusive_spell_dependencies(local, aura, auras, spells, spell_effects);
252 let exclusive_spells = append_range(&mut exclusive_spells, local_exclusive_spells);
253
254 entries.push(AuraDependencyGraphEntry {
255 consumers,
256 costs,
257 recharge,
258 maximum_charges,
259 gates,
260 aura_timing,
261 exclusive_spells,
262 });
263 }
264
265 Self {
266 entries: entries.into_boxed_slice(),
267 cost_spells: cost_spells.into_boxed_slice(),
268 recharge_spells: recharge_spells.into_boxed_slice(),
269 maximum_charge_spells: maximum_charge_spells.into_boxed_slice(),
270 gate_spells: gate_spells.into_boxed_slice(),
271 aura_timing: aura_timing.into_boxed_slice(),
272 exclusive_spells: exclusive_spells.into_boxed_slice(),
273 }
274 }
275}
276
277impl AuraDependencyPlan {
278 pub(crate) fn bind(
282 graph: AuraDependencyGraph,
283 auras: &[AuraData],
284 schema: &ContextSchema,
285 bindings: &BufferBindings,
286 enemy_count: usize,
287 ) -> Self {
288 let mut entries = Vec::with_capacity(auras.len());
289 let cost_spells = bind_spell_consumers(graph.cost_spells.iter().copied(), bindings);
290 let gate_spells = bind_spell_consumers(graph.gate_spells.iter().copied(), bindings);
291 let mut projections = Vec::new();
292 let mut projection_sources = Vec::new();
293 let exclusive_spells = graph
294 .exclusive_spells
295 .iter()
296 .map(|dependency| ExclusiveSpellDependency {
297 handle: bindings.spell(dependency.local),
298 enabled: dependency.enabled,
299 })
300 .collect::<Vec<_>>();
301 let projection_target_count = enemy_count.saturating_add(1);
302
303 for (aura, graph_entry) in auras.iter().zip(graph.entries.iter()) {
304 let projection_start = projections.len();
305
306 schema
307 .buffer_finalizer()
308 .visit_aura_projections(|projection, destination| {
309 if projection.aura().as_u32() != aura.aura_id || projection.on() != aura.on {
310 return;
311 }
312
313 let source_offset = projection_sources.len();
314
315 for target_index in 0..projection_target_count {
316 let target = (target_index > 0).then(|| {
317 EnemyIdx(
318 u16::try_from(target_index - 1)
319 .expect("finalized enemy count fits in EnemyIdx"),
320 )
321 });
322 let source =
323 exact_projection_key(projection.aura(), projection.on(), target)
324 .and_then(|key| schema.buffer_finalizer().bind_aura(key));
325
326 projection_sources.push(source);
327 }
328
329 projections.push(ProjectionDependency {
330 destination,
331 source_offset,
332 index: AuraProjectionDependencyIdx(projections.len()),
333 });
334 });
335
336 let projection_range = projection_start..projections.len();
337
338 entries.push(AuraDependencyEntry {
339 consumers: graph_entry.consumers,
340 costs: graph_entry.costs.start..graph_entry.costs.end,
341 recharge: graph_entry.recharge.start..graph_entry.recharge.end,
342 maximum_charges: graph_entry.maximum_charges.start..graph_entry.maximum_charges.end,
343 gates: graph_entry.gates.start..graph_entry.gates.end,
344 aura_timing: graph_entry.aura_timing.start..graph_entry.aura_timing.end,
345 projections: projection_range,
346 exclusive_spells: graph_entry.exclusive_spells.start
347 ..graph_entry.exclusive_spells.end,
348 });
349 }
350
351 let (concrete_entries, exclusive_peers) = compile_exclusive_peers(auras, bindings);
352 let aura_timing_bindings = entries
353 .iter()
354 .map(|entry| {
355 let locals = &graph.aura_timing[entry.aura_timing.clone()];
357 let bucket_count = FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX + enemy_count;
358 let mut buckets = vec![Vec::new(); bucket_count];
359 let mut local_membership = vec![false; auras.len()];
360
361 for &local in locals {
362 local_membership[local.as_usize()] = true;
364 }
365
366 for binding in bindings.aura_bindings() {
367 if !local_membership[binding.local.as_usize()] {
369 continue;
370 }
371
372 if let Some(bucket) = concrete_actor_bucket(binding.key.affected(), enemy_count)
373 {
374 buckets[bucket].push(binding);
376 }
377 }
378
379 flatten_actor_buckets(buckets)
380 })
381 .collect();
382 let control_peers = auras
383 .iter()
384 .map(|aura| {
385 let bucket_count = FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX + enemy_count;
386 let mut buckets = vec![Vec::new(); bucket_count];
387
388 if let Some(control) = aura.crowd_control {
389 for binding in bindings.aura_bindings() {
390 let candidate = &auras[binding.local.as_usize()];
392 let Some(candidate_control) = candidate.crowd_control else {
393 continue;
394 };
395 let matches_kind = candidate_control.kind == control.kind;
396 let matches_group = control.diminishing
397 != wowlab_engine_domain::dbc::DiminishingGroup::None
398 && candidate_control.diminishing == control.diminishing;
399
400 if !(matches_kind || matches_group) {
401 continue;
402 }
403
404 if let Some(bucket) =
405 concrete_actor_bucket(binding.key.affected(), enemy_count)
406 {
407 buckets[bucket].push(ControlPeerDependency {
409 handle: binding.handle,
410 matches_kind,
411 matches_group,
412 permanent: candidate.base_duration_ms == 0,
413 });
414 }
415 }
416 }
417
418 flatten_actor_buckets(buckets)
419 })
420 .collect();
421
422 Self {
423 entries: entries.into_boxed_slice(),
424 concrete_entries,
425 cost_spells: cost_spells.into_boxed_slice(),
426 recharge_spells: graph.recharge_spells,
427 maximum_charge_spells: graph.maximum_charge_spells,
428 gate_spells: gate_spells.into_boxed_slice(),
429 aura_timing: graph.aura_timing,
430 aura_timing_bindings,
431 control_peers,
432 projections: projections.into_boxed_slice(),
433 projection_sources: projection_sources.into_boxed_slice(),
434 projection_target_count,
435 exclusive_peers,
436 exclusive_spells: exclusive_spells.into_boxed_slice(),
437 }
438 }
439
440 pub(crate) fn consumers(&self, aura: LocalAuraIdx) -> AuraDependencies {
441 self.entry(aura).consumers
442 }
443
444 pub(crate) fn cost_spells(&self, aura: LocalAuraIdx) -> &[SpellConsumerBinding] {
445 &self.cost_spells[self.entry(aura).costs.clone()]
447 }
448
449 pub(crate) fn recharge_spells(&self, aura: LocalAuraIdx) -> &[LocalSpellIdx] {
450 &self.recharge_spells[self.entry(aura).recharge.clone()]
452 }
453
454 pub(crate) fn maximum_charge_spells(&self, aura: LocalAuraIdx) -> &[LocalSpellIdx] {
455 &self.maximum_charge_spells[self.entry(aura).maximum_charges.clone()]
457 }
458
459 pub(crate) fn gate_spells(&self, aura: LocalAuraIdx) -> &[SpellConsumerBinding] {
460 &self.gate_spells[self.entry(aura).gates.clone()]
462 }
463
464 pub(crate) fn aura_timing(&self, aura: LocalAuraIdx) -> &[LocalAuraIdx] {
465 &self.aura_timing[self.entry(aura).aura_timing.clone()]
467 }
468
469 pub(crate) fn aura_timing_bindings(
470 &self,
471 aura: LocalAuraIdx,
472 actor: ActorId,
473 ) -> &[ConcreteAuraBinding] {
474 self.aura_timing_bindings[aura.as_usize()].for_actor(actor)
476 }
477
478 pub(crate) fn control_peers(
479 &self,
480 aura: LocalAuraIdx,
481 actor: ActorId,
482 ) -> &[ControlPeerDependency] {
483 self.control_peers[aura.as_usize()].for_actor(actor)
485 }
486
487 pub(crate) fn projections(&self, aura: LocalAuraIdx) -> &[ProjectionDependency] {
488 &self.projections[self.entry(aura).projections.clone()]
490 }
491
492 pub(crate) fn all_projections(&self) -> &[ProjectionDependency] {
493 &self.projections
494 }
495
496 pub(crate) fn projection(&self, index: AuraProjectionDependencyIdx) -> ProjectionDependency {
497 self.projections[index.as_usize()]
499 }
500
501 pub(crate) const fn projection_count(&self) -> usize {
502 self.projections.len()
503 }
504
505 #[expect(
506 clippy::unused_self,
507 reason = "the plan retains authority over extracting its private dependency identity"
508 )]
509 pub(crate) fn projection_index(
510 &self,
511 dependency: ProjectionDependency,
512 ) -> AuraProjectionDependencyIdx {
513 dependency.index
514 }
515
516 pub(crate) fn exclusive_peers(&self, binding: ConcreteAuraBinding) -> &[ConcreteAuraBinding] {
517 let entry = &self.concrete_entries[binding.index().as_usize()];
519
520 &self.exclusive_peers[entry.exclusive_peers.clone()]
523 }
524
525 pub(crate) fn exclusive_spells(&self, aura: LocalAuraIdx) -> &[ExclusiveSpellDependency] {
526 &self.exclusive_spells[self.entry(aura).exclusive_spells.clone()]
528 }
529
530 pub(crate) fn projection_source(
531 &self,
532 dependency: ProjectionDependency,
533 target: Option<EnemyIdx>,
534 ) -> Option<AuraSlotHandle> {
535 let target_index = target.map_or(0, |enemy| enemy.as_usize().saturating_add(1));
536
537 if target_index >= self.projection_target_count {
538 return None;
539 }
540
541 self.projection_sources
542 .get(dependency.source_offset + target_index)
543 .copied()
544 .flatten()
545 }
546
547 pub(crate) fn validate(&self, aura_count: usize, spell_count: usize) -> bool {
548 self.entries.len() == aura_count
549 && self
550 .concrete_entries
551 .iter()
552 .all(|entry| entry.exclusive_peers.end <= self.exclusive_peers.len())
553 && self
554 .cost_spells
555 .iter()
556 .all(|binding| binding.local.as_usize() < spell_count)
557 && self
558 .recharge_spells
559 .iter()
560 .all(|local| local.as_usize() < spell_count)
561 && self
562 .maximum_charge_spells
563 .iter()
564 .all(|local| local.as_usize() < spell_count)
565 && self
566 .gate_spells
567 .iter()
568 .all(|binding| binding.local.as_usize() < spell_count)
569 && self
570 .aura_timing
571 .iter()
572 .all(|local| local.as_usize() < aura_count)
573 && self.aura_timing_bindings.len() == aura_count
574 && self.control_peers.len() == aura_count
575 }
576
577 fn entry(&self, aura: LocalAuraIdx) -> &AuraDependencyEntry {
578 &self.entries[aura.as_usize()]
580 }
581}
582
583fn concrete_actor_bucket(actor: ActorId, enemy_count: usize) -> Option<usize> {
584 match actor {
585 ActorId::Player => Some(0),
586 ActorId::External => Some(1),
587 ActorId::Pet(pet) => {
588 (pet.as_usize() < PetIdx::MAX).then_some(FIXED_ACTOR_BUCKET_COUNT + pet.as_usize())
589 }
590 ActorId::Enemy(enemy) => (enemy.as_usize() < enemy_count)
591 .then_some(FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX + enemy.as_usize()),
592 }
593}
594
595fn flatten_actor_buckets<T>(buckets: Vec<Vec<T>>) -> ActorRanges<T> {
596 let mut entries = Vec::new();
597 let mut ranges = Vec::with_capacity(buckets.len());
598
599 for bucket in buckets {
600 let start = entries.len();
601
602 entries.extend(bucket);
603 ranges.push(start..entries.len());
604 }
605
606 let player = ranges[0].clone();
609 let external = ranges[1].clone();
611 let enemies_start = FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX;
612 let pets = ranges[FIXED_ACTOR_BUCKET_COUNT..enemies_start]
614 .to_vec()
615 .into_boxed_slice();
616 let enemies = ranges[enemies_start..].to_vec().into_boxed_slice();
618
619 ActorRanges {
620 entries: entries.into_boxed_slice(),
621 player,
622 external,
623 pets,
624 enemies,
625 }
626}
627
628fn append_range<T>(target: &mut Vec<T>, values: Vec<T>) -> Range<usize> {
629 let start = target.len();
630
631 target.extend(values);
632
633 start..target.len()
634}
635
636fn bind_spell_consumers(
637 locals: impl IntoIterator<Item = LocalSpellIdx>,
638 bindings: &BufferBindings,
639) -> Vec<SpellConsumerBinding> {
640 locals
641 .into_iter()
642 .map(|local| SpellConsumerBinding {
643 local,
644 handle: bindings.spell(local),
645 })
646 .collect()
647}
648
649fn normalize<T>(values: &mut Vec<T>)
650where
651 T: Ord,
652{
653 values.sort_unstable();
654 values.dedup();
655}
656
657fn compile_exclusive_peers(
658 auras: &[AuraData],
659 bindings: &BufferBindings,
660) -> (
661 Box<[ConcreteAuraDependencyEntry]>,
662 Box<[ConcreteAuraBinding]>,
663) {
664 let mut entries = Vec::with_capacity(bindings.aura_bindings().count());
665 let mut peers = Vec::new();
666
667 for binding in bindings.aura_bindings() {
668 let start = peers.len();
669 let group = auras[binding.local.as_usize()]
671 .spell_group
672 .filter(|group| group.rule == SpellGroupRule::Exclusive)
673 .map(|group| group.group_idx);
674
675 if let Some(group) = group {
676 peers.extend(bindings.aura_bindings().filter(|candidate| {
677 candidate.local != binding.local
678 && candidate.key.source() == binding.key.source()
679 && candidate.key.affected() == binding.key.affected()
680 && auras[candidate.local.as_usize()]
682 .spell_group
683 .is_some_and(|peer| {
684 peer.rule == SpellGroupRule::Exclusive && peer.group_idx == group
685 })
686 }));
687 }
688
689 peers[start..].sort_unstable_by_key(|peer| peer.local);
692 entries.push(ConcreteAuraDependencyEntry {
693 exclusive_peers: start..peers.len(),
694 });
695 }
696
697 (entries.into_boxed_slice(), peers.into_boxed_slice())
698}
699
700fn collect_exclusive_spell_dependencies(
701 applied: LocalAuraIdx,
702 aura: &AuraData,
703 auras: &[AuraData],
704 spells: &[SpellData],
705 spell_effects: &[SpellEffectData],
706) -> Vec<UnboundExclusiveSpellDependency> {
707 let Some(group) = aura
708 .spell_group
709 .filter(|group| group.rule == SpellGroupRule::Exclusive)
710 .map(|group| group.group_idx)
711 else {
712 return Vec::new();
713 };
714 let mut dependencies = Vec::new();
715
716 for (index, spell) in spells.iter().enumerate() {
717 let start = spell.followup_effects.start as usize;
718 let len = usize::from(spell.followup_effects.len);
719 let followups = (start..start.saturating_add(len)).filter_map(|effect_index| {
720 match spell_effects.get(effect_index) {
721 Some(SpellEffectData::ApplyAura { aura_local }) => Some(*aura_local),
722 _ => None,
723 }
724 });
725 let mut belongs_to_group = false;
726 let mut enables_applied = false;
727
728 for local in spell.applies_aura.into_iter().chain(followups) {
729 let same_group = auras[local.as_usize()]
731 .spell_group
732 .is_some_and(|candidate| {
733 candidate.rule == SpellGroupRule::Exclusive && candidate.group_idx == group
734 });
735
736 belongs_to_group |= same_group;
737 enables_applied |= same_group && local == applied;
738 }
739
740 if belongs_to_group {
741 dependencies.push(UnboundExclusiveSpellDependency {
742 local: local_spell(index),
743 enabled: enables_applied,
744 });
745 }
746 }
747
748 dependencies
749}
750
751fn local_spell(index: usize) -> LocalSpellIdx {
752 LocalSpellIdx::new(u8::try_from(index).expect("finalized spell count is validated below 256"))
753}
754
755fn classify_consumers(
757 effect: &BuffEffect,
758 config: &ProgramConfig,
759 consumers: &mut AuraDependencies,
760) {
761 use AuraDependencies as Dependency;
762
763 match effect {
764 BuffEffect::SpellGateFromEffect { .. } => *consumers |= Dependency::GATES,
765 BuffEffect::PrimaryStat(..) | BuffEffect::PrimaryStatPercent(..) => {
766 *consumers |= Dependency::PRIMARY_STAT | Dependency::PET_STATS;
767 }
768 BuffEffect::BonusStatPercent { stat, .. } => {
769 let primary_matches = match config.base_stats.primary_attribute {
770 Some(Attribute::Strength) => *stat == StatModifierKind::Strength,
771 Some(Attribute::Agility) => *stat == StatModifierKind::Agility,
772 Some(Attribute::Intellect) => *stat == StatModifierKind::Intellect,
773 _ => false,
774 };
775
776 if primary_matches || matches!(stat, StatModifierKind::Primary | StatModifierKind::All)
777 {
778 *consumers |= Dependency::PRIMARY_STAT | Dependency::PET_STATS;
779 }
780
781 if matches!(stat, StatModifierKind::Stamina | StatModifierKind::All) {
782 *consumers |= Dependency::STAMINA | Dependency::PET_STATS;
783 }
784 }
785 BuffEffect::OverrideSpellPowerByAttackPowerPercent(..)
786 | BuffEffect::OverrideAttackPowerBySpellPowerPercent(..)
787 | BuffEffect::AttackPowerPercent(..) => *consumers |= Dependency::PET_STATS,
788 BuffEffect::RechargeRatePercent(..)
789 | BuffEffect::CooldownRechargeRateFromEffect { .. }
790 | BuffEffect::ChargeRechargeRateCategory { .. }
791 | BuffEffect::CooldownDurationPercent { .. } => {
792 *consumers |= Dependency::RECHARGE_RATE;
793 }
794 BuffEffect::MaxChargesCategory(..) => *consumers |= Dependency::MAXIMUM_CHARGES,
795 BuffEffect::AuraTimeRateFromEffect { .. } => *consumers |= Dependency::AURA_TIME,
796 BuffEffect::Haste(..)
797 | BuffEffect::HasteMult(..)
798 | BuffEffect::HasteMultFromEffect { .. } => {
799 *consumers |= Dependency::PLAYER_SPEED | Dependency::COMPANION_SPEED;
800
801 if config.haste_regen {
802 *consumers |= Dependency::RESOURCE_REGEN;
803 }
804 }
805 BuffEffect::SpellCastSpeed(..) | BuffEffect::AttackSpeed(..) => {
806 *consumers |= Dependency::COMPANION_SPEED;
807
808 if matches!(effect, BuffEffect::AttackSpeed(..)) {
809 *consumers |= Dependency::PLAYER_SPEED;
810 }
811 }
812 BuffEffect::MaxResourceFlat(..) => *consumers |= Dependency::MAXIMUM_RESOURCE,
813 BuffEffect::SpellTargetingFromEffect { property, .. }
814 if *property == wowlab_engine_domain::dbc::ModifierPropertyKind::Range =>
815 {
816 *consumers |= Dependency::TARGET_RANGE;
817 }
818 BuffEffect::RegenPercent(..)
819 | BuffEffect::DynamicRegenPercent(..)
820 | BuffEffect::ResourceRegenPerFiveSeconds(..)
821 | BuffEffect::InterruptRegen
822 | BuffEffect::InterruptedRegenPercent(..)
823 | BuffEffect::PreventResourceRegen(..) => {
824 *consumers |= Dependency::RESOURCE_REGEN | Dependency::PET_ACTIONS;
825 }
826 _ => {}
827 }
828}
829
830fn collect_cost_spells(
832 effect: &BuffEffect,
833 spells: &[SpellData],
834 config: &ProgramConfig,
835 target: &mut Vec<LocalSpellIdx>,
836) {
837 match effect {
838 BuffEffect::ResourceCostPercent(_, resource) => {
839 for (index, spell) in spells.iter().enumerate() {
840 if (config.base_stats.resource_type == Some(*resource)
841 && spell_has_primary_cost(spell, config))
842 || (config.base_stats.secondary_resource_type == Some(*resource)
843 && spell_has_secondary_cost(spell, config))
844 {
845 target.push(local_spell(index));
846 }
847 }
848 }
849 BuffEffect::SpellCostPercent(_, spell_ids, _)
850 | BuffEffect::SpellCostFlat(_, spell_ids, _) => {
851 for (index, spell) in spells.iter().enumerate() {
852 if spell_ids.contains(&spell.spell_id) {
853 target.push(local_spell(index));
854 }
855 }
856 }
857 BuffEffect::SpellCostFromEffect {
858 source_spell_id,
859 effect_index,
860 cost_entry,
861 ..
862 } => {
863 for (index, spell) in spells.iter().enumerate() {
864 let spell_idx = spell.idx();
865 let matching_entry = config.game_data.cost_entry(spell_idx, false) == *cost_entry
866 || config.game_data.cost_entry(spell_idx, true) == *cost_entry;
867
868 if matching_entry
869 && config.game_data.effect_affects_spell(
870 SpellIdx::from_raw(*source_spell_id),
871 *effect_index,
872 spell_idx,
873 )
874 {
875 target.push(local_spell(index));
876 }
877 }
878 }
879 BuffEffect::AdditionalPowerCostFromEffect {
880 source_spell_id,
881 effect_index,
882 resource_type,
883 ..
884 } => {
885 let matching_resource = config
886 .base_stats
887 .resource_type
888 .is_some_and(|resource| i32::from(u8::from(resource)) == *resource_type)
889 || config
890 .base_stats
891 .secondary_resource_type
892 .is_some_and(|resource| i32::from(u8::from(resource)) == *resource_type);
893
894 if matching_resource {
895 for (index, spell) in spells.iter().enumerate() {
896 if config.game_data.effect_affects_spell(
897 SpellIdx::from_raw(*source_spell_id),
898 *effect_index,
899 spell.idx(),
900 ) {
901 target.push(local_spell(index));
902 }
903 }
904 }
905 }
906 _ => {}
907 }
908}
909
910fn collect_recharge_spells(
911 effect: &BuffEffect,
912 spells: &[SpellData],
913 config: &ProgramConfig,
914 target: &mut Vec<LocalSpellIdx>,
915) {
916 match effect {
917 BuffEffect::Haste(..)
918 | BuffEffect::HasteMult(..)
919 | BuffEffect::HasteMultFromEffect { .. } => {
920 target.extend(
921 spells
922 .iter()
923 .enumerate()
924 .filter(|&(_index, spell)| {
925 spell.cooldown.has_cooldown && spell.cooldown.cooldown_hasted
926 })
927 .map(|(index, _spell)| local_spell(index)),
928 );
929 }
930 BuffEffect::RechargeRatePercent(_, spell_ids) => {
931 target.extend(
932 spells
933 .iter()
934 .enumerate()
935 .filter(|&(_index, spell)| {
936 spell.cooldown.has_cooldown && spell_ids.contains(&spell.spell_id)
937 })
938 .map(|(index, _spell)| local_spell(index)),
939 );
940 }
941 BuffEffect::CooldownRechargeRateFromEffect {
942 source_spell_id,
943 effect_index,
944 ..
945 }
946 | BuffEffect::CooldownDurationPercent {
947 source_spell_id,
948 effect_index,
949 ..
950 } => {
951 target.extend(
952 spells
953 .iter()
954 .enumerate()
955 .filter(|&(_index, spell)| {
956 spell.cooldown.has_cooldown
957 && config.game_data.effect_affects_spell(
958 SpellIdx::from_raw(*source_spell_id),
959 *effect_index,
960 spell.idx(),
961 )
962 })
963 .map(|(index, _spell)| local_spell(index)),
964 );
965 }
966 BuffEffect::ChargeRechargeRateCategory { category, .. } => {
967 target.extend(
968 spells
969 .iter()
970 .enumerate()
971 .filter(|&(_index, spell)| {
972 spell.cooldown.has_cooldown
973 && spell
974 .cooldown
975 .charge_category
976 .is_some_and(|value| value.raw() == *category)
977 })
978 .map(|(index, _spell)| local_spell(index)),
979 );
980 }
981 _ => {}
982 }
983}
984
985fn collect_maximum_charge_spells(
986 effect: &BuffEffect,
987 spells: &[SpellData],
988 target: &mut Vec<LocalSpellIdx>,
989) {
990 let BuffEffect::MaxChargesCategory(_, category) = effect else {
991 return;
992 };
993
994 target.extend(
995 spells
996 .iter()
997 .enumerate()
998 .filter(|&(_index, spell)| {
999 spell.is_charged()
1000 && spell
1001 .cooldown
1002 .charge_category
1003 .is_some_and(|value| value.raw() == *category)
1004 })
1005 .map(|(index, _spell)| local_spell(index)),
1006 );
1007}
1008
1009fn collect_gate_spells(effect: &BuffEffect, spells: &[SpellData], target: &mut Vec<LocalSpellIdx>) {
1010 if matches!(effect, BuffEffect::SpellGateFromEffect { .. }) {
1011 target.extend((0..spells.len()).map(local_spell));
1012 }
1013}
1014
1015fn collect_aura_timing(
1016 effect: &BuffEffect,
1017 auras: &[AuraData],
1018 config: &ProgramConfig,
1019 target: &mut Vec<LocalAuraIdx>,
1020) {
1021 let BuffEffect::AuraTimeRateFromEffect {
1022 source_spell_id,
1023 effect_index,
1024 ..
1025 } = effect
1026 else {
1027 return;
1028 };
1029
1030 target.extend(
1031 auras
1032 .iter()
1033 .enumerate()
1034 .filter(|&(_, aura)| {
1035 config.game_data.effect_affects_spell(
1036 SpellIdx::from_raw(*source_spell_id),
1037 *effect_index,
1038 SpellIdx::from_raw(aura.aura_id),
1039 )
1040 })
1041 .map(|(index, _)| local_aura(index)),
1042 );
1043}
1044
1045fn local_aura(index: usize) -> LocalAuraIdx {
1046 LocalAuraIdx::new(u8::try_from(index).expect("finalized aura count is validated below 256"))
1047}
1048
1049fn spell_has_primary_cost(spell: &SpellData, config: &ProgramConfig) -> bool {
1050 spell.cost.resource_cost != 0.0
1052 || spell.cost.optional_resource_cost != 0.0
1053 || spell.cost.resource_cost_pct.percent != 0.0
1054 || spell.cost.optional_resource_cost_pct.percent != 0.0
1055 || spell.cost.maximum_resource_cost_pct.percent != 0.0
1056 || config.game_data.cost_entry(spell.idx(), false) != 0
1057}
1058
1059fn spell_has_secondary_cost(spell: &SpellData, config: &ProgramConfig) -> bool {
1060 spell.cost.secondary_resource_cost != 0.0
1062 || spell.cost.secondary_optional_resource_cost != 0.0
1063 || config.game_data.cost_entry(spell.idx(), true) != 0
1064}
1065
1066fn spell_uses_maximum_resource_cost(spell: &SpellData) -> bool {
1067 [
1069 spell.cost.resource_cost_pct,
1070 spell.cost.optional_resource_cost_pct,
1071 spell.cost.maximum_resource_cost_pct,
1072 ]
1073 .into_iter()
1074 .any(|cost| cost.percent != 0.0 && cost.basis == ResourceCostPercentBasis::Maximum)
1075}
1076
1077fn exact_projection_key(
1078 aura: AuraIdx,
1079 on: AuraOn,
1080 current_target: Option<EnemyIdx>,
1081) -> Option<AuraKey> {
1082 let (source, affected) = match on {
1083 AuraOn::Player => (ActorId::Player, ActorId::Player),
1084 AuraOn::Target => (ActorId::Player, ActorId::Enemy(current_target?)),
1085 AuraOn::Pet => {
1086 let pet = ActorId::Pet(PetIdx::PRIMARY);
1087
1088 (pet, pet)
1089 }
1090 };
1091
1092 Some(AuraKey::new(aura, source, affected, on))
1093}